最近把一个中型 Python 项目的 lint 工具链从 Flake8 那套老配置迁到了 Ruff,VSCode 里装上对应插件之后,体验确实不一样。以前保存文件要等半秒到一秒才看到波浪线,现在几乎是按键刚抬起来就出结果,而且之前让我很头疼的 Flake8 报错规则在 Ruff 里基本都能对应上,连# noqa注释都没有改,原样就能用。如果你手上还有项目在用 Flake8 或者刚想给项目配代码检查,这篇文章就围绕 VSCode 里的 Ruff 插件、Flake8 功能兼容、# noqa取消检测这几个点展开,把所有配置、规则对应关系、踩过的坑一次说清楚。
1. 为什么Ruff插件能在VSCode里替代Flake8
1.1 Flake8到底包含什么,Ruff又是什么关系
Flake8 本身不是单一个工具,它是把几个 Python 静态检查工具打包在一起的封装层:pyflakes 负责找未导入的变量、未使用的导入这类逻辑问题,pycodestyle 负责审查代码风格、缩进、行长度等,再挂上 mccabe 计算圈复杂度。三个工具各有各的规则代码前缀,Flake8 整合后用 E、W、F、C90 这些代码开头来统一输出。你用 flake8 敲命令行时,实际上是在同时运行这几样东西。
Ruff 做的事情更像"重写一遍"。它用 Rust 实现了大量与这些工具等价的检查逻辑,规则代码完全沿用 Flake8 的命名方式,F 系列对应 pyflakes,E 和 W 系列对应 pycodestyle,C90 系列对应 mccabe,甚至 Flake8 生态里流行的插件规则也被收编,比如 B 系列(bugbear)、SIM 系列(flake8-simplify)、S 系列(bandit)等。所以从使用者角度来看,Ruff 提供的检查结果和你习惯的 Flake8 报错是对得上的,迁移成本不高。
这也是为什么说"Ruff 包含 Flake8 功能"。它并不是在内部调用 Flake8,而是自己实现了同类规则的检测,并且规则代码保持了 Flake8 的兼容性。统一的规则命名,让老 Flake8 用户几乎不需要重新学习就上手。
1.2 相比原版Flake8,Ruff在编辑器里的真实优势
编辑器场景下,原版 Flake8 最大的问题是速度。它每次都要启动 Python 解释器,逐文件扫描,文件多了延迟会越来越明显。我自己用 VSCode 配过 Flake8 插件,保存时经常要等个好一会儿才出 lint 结果,而且因为 pyflakes 和 pycodestyle 是分开跑的,输出顺序偶尔还会乱。Ruff 是 Rust 编译的单个二进制,启动速度快得多,而且插件一次扫描就能把全部规则跑完,它还会做增量缓存,改一个文件只需要重新检查这一个文件的增量内容,不是整个项目从头来一遍。
Ruff 的自动修复能力也比 Flake8 强得多。Flake8 本身基本不提供自动修复,能 fix 的规则非常有限。Ruff 很多规则带 autofix,比如未使用的导入(F401)、未使用的变量(F841)、无用的括号(E711)这些,保存时可以直接一键修掉。很多静态检查工具是"发现问题就完了",Ruff 是发现问题之后尽量帮你顺手解决,这对日常开发效率的提升是肉眼可见的。
我还注意到一个细节:Ruff 插件对# noqa的处理在体验上比 Flake8 更高效。不仅识别速度快,而且如果你拼写了不存在的规则代码,它会给出明确提示,Flake8 是直接忽略不报。这个在后面讲 noqa 的坑时还会展开。
2. VSCode里的安装与配置
2.1 安装步骤与环境前提
在 VSCode 里使用 Ruff,需要先装两个插件:Python 扩展(提供 Pylance 语言服务和 Python 环境识别能力)和 Ruff 扩展。Ruff 插件在扩展商店直接搜 "Ruff" 即可,作者是 Ruff 官方维护者,打开任意 Python 文件后会要求在项目中安装 ruff 包,一般通过 pip 或 uv 安装即可。我通常会把它加到项目的开发依赖里而不是全局,这样大家一起开发时 lint 版本一致,结果也就一致。
需要留意 Python 环境的选择。VSCode 右下角选定的解释器必须和项目里安装 ruff 的环境一致,如果解释器指向了另一个没有安装 ruff 的环境,插件会提示找不到 ruff 命令,而不是静默失败。插件默认允许使用全局环境的 ruff,但跟着项目走得踩坑更少。
Ruff 插件同时具备 lint 和 format 功能。如果你之前用了 Black 或者 autopep8 来做格式化,并且准备改用 Ruff formatter,那需要把原来那些格式化插件禁用掉,或者设置它们的激活状态为 false,避免保存文件时两个格式化器互相打架。VSCode 里一个文件只能有一个 formatter 在保存时生效,多个格式化器同时启用会触发弹窗问你要用哪个,很烦,还容易把代码改乱了。
2.2 settings.json完整配置参考
我直接把一份比较完整的配置贴出来,这套配置是实际跑过一段时间的,可以直接复制改。
{ "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": "explicit", "source.organizeImports": "explicit" } }, "ruff.lint.enable": true, "ruff.lint.run": "onType", "ruff.lint.args": ["--config", "ruff.toml"], "ruff.format.args": ["--config", "ruff.toml"] }逐项解释一下我的配置意图。editor.defaultFormatter设成 Ruff 插件,这样保存时如果有格式化需求不会触发 Black 等工具的干扰。formatOnSave开启保存格式化,而source.fixAll这里控制的是 lint 的自动修复,两者是不同层面的事:一个是格式化排版,一个是针对代码问题做修改。source.organizeImports会自动整理导入排序,这个动作依赖 ruff 的 isort 规则,如果配置里没有启用 isort,这步不会生效。
ruff.lint.run我选的onType,表示输入时实时检查,按键延迟基本没有。如果你的项目非常大,全员实时检查会对 CPU 有压力,可以改成onSave。ruff.lint.args指定了配置文件路径,不写的话插件会按树向上查找 pyproject.toml 或 ruff.toml,能找到就不用写,但如果项目里同时存在多个配置来源,显式指定才最稳妥。
2.3 验证插件是否真正生效
配置完不能光看设置界面,需要实际验证三步。
第一步,打开一个 Python 文件,右下角状态栏应该能看到 Ruff 的字样或相关图标。点击它可以确认当前的 ruff 二进制路径和版本。
第二步,故意写一行格式错误的代码,比如超过行宽的一行 import,看是否有波浪线提示。再写一个未使用的变量,比如x = 1后面不引用,确认 F841 是否被报出来。
第三步,测试自动修复。Ctrl+S 保存时,未使用的 import 会自动被删掉。如果保存后没有变化,大概率是 codeActionsOnSave 配置没生效,或者 ruff 的 lint 规则里没有启用 F 系列规则。检查配置里有没有ruff.lint.select或ignore做过裁剪,很多人会在这一步发现规则被自己关掉了。
小节总结一下:插件的配置文件放在项目里并加入版本控制,Ruff 插件读的是项目配置而非编辑器 UI 里设置的那些零散项,这能保证命令行执行和编辑器的检测结果一致。
3. 规则体系拆解:Flake8那一套在Ruff里对应什么
3.1 规则代码对照表
刚迁移的时候最容易懵的就是规则代码。你打开 Flake8 的报错,里面会有 E501、W291、F401、C901 这些,Ruff 的规则也都保留同样前缀。我整理了一张常用对照表,每次查规则时挺有用。
| 前缀 | 对应Flake8来源 | 检查内容 | 典型例子 |
|---|---|---|---|
| E | pycodestyle Error | 代码风格错误 | E501 行超长、E711 与 None 比较、E302 缺少空行 |
| W | pycodestyle Warning | 代码风格警告 | W291 行尾空白、W503 换行时运算符位置 |
| F | pyflakes | 逻辑与未使用问题 | F401 未使用导入、F841 未使用局部变量、F821 未定义名称 |
| C90 | mccabe | 圈复杂度 | C901 函数复杂度过高 |
| B | flake8-bugbear | 常见代码陷阱 | B006 可变默认参数、B008 函数调用作为默认参数 |
| SIM | flake8-simplify | 可简化代码 | SIM108 三元表达式替代 if-else、SIM105 上下文管理器 |
| I | isort | 导入排序 | I001 导入未排序 |
| UP | pyupgrade | 旧语法升级 | UP006 使用新式类型注解 |
| S | bandit | 安全风险 | S101 断言使用、S311 随机数不安全 |
| RUF | Ruff专用 | Ruff自定义规则 | RUF100 未使用的noqa注释 |
3.2 如何配置规则开关
Ruff 支持非常细粒度的规则控制,这在迁移时很重要。我用的是 pyproject.toml 里配置,基本结构和 Flake8 的 setup.cfg 类似,但更清晰。
[tool.ruff] target-version = "py311" line-length = 88 [tool.ruff.lint] select = [ "E", "W", "F", "B", "SIM", "I", "UP", "C90", "RUF" ] ignore = [ "E203", # 与black/ruff formatter冲突的空格规则 "E501", # 行超长交给formatter处理,不作为lint报错 ] [tool.ruff.lint.per-file-ignores] "tests/**" = ["S101"] # 测试里允许使用assertselect是启用哪些规则,ignore是排除,per-file-ignores可以针对特定目录单独用。注意 E203 和 E501 这两条:E203 在 Black 格式化器活着的时代就建议忽略,Ruff formatter 也是同样情况,留着他会一直报被格式化的代码不符合风格,属于无用报错;E501 和 formatter 的 line-length 冲突,超长行为交给 format 处理即可。这些坑都是实际项目里遇到过的。
3.3 如何快速查看规则文档
规则代码太多记不住的时候,推荐直接跑命令行查看。ruff check --help下面没有列出所有规则,但可以用ruff rule加代码查具体文档,比如ruff rule F401,会输出该规则的解释、错误代码示例、正确写法和可修复性。想列出所有启用的规则,可以执行:
ruff check --select E,F,W,B,SIM,I,UP,C90,RUF --show-files .还有两个命令很好用:ruff check --explain F401和ruff check --select ALL --show-files。前者帮你理解某一规则的逻辑,后者可以看到当前项目所有会被报告的文件和规则。在编辑器里开发时,鼠标悬停在波浪线上也会弹出规则代码和简要说明,点击规则代码可以直接跳转线上文档,这条文档链接是官方维护的。真正使用下来,我发现让同事适应规则代码前缀的意义,比让他们背一堆规则名有用得多。
4. # noqa深入解析:从行级到文件级
4.1 基本语法:单行取消检测
# noqa是项目中常见的注释,表示"这一行我故意写成这样,不要给我报错"。Ruff 完全兼容 Flake8 的语法,用法一模一样:把# noqa放在那行代码的结尾。
import os # noqa: F401这是最基础的用法,含义是该行所有 lint 规则都被忽略。但无规则代码时,Ruff 会从头到尾跳过这一行,任何问题都不报告。如果你写# noqa: F401,那就只忽略未使用导入这条规则,其他规则仍然生效。
实际开发中我强烈建议不要裸用# noqa。原因很简单,它相当于把整行所有潜在问题都豁免了,后续重构时这行代码明明出现更严重的问题,注释还是会沉默地屏蔽掉。显式写出规则代码,比如# noqa: E501,意图清楚,别人看代码时也知道你为什么豁免。尤其 code review 时,看到# noqa不带说明,一般我都会打回去让补充注释描述原因。
4.2 精确忽略指定规则
# noqa: 规则代码1, 规则代码2可以同时忽略多条规则,逗号后面要加空格,这是 Flake8 流传下来的风格要求,Ruff 同样遵守。
result = defaultdict(lambda: defaultdict(list)) # noqa: B008, E501这行的意思是忽略 B008(函数调用作为默认参数)和 E501(行超长)两条规则,其他规则照常检查。一个常见的坑是规则代码写了但拼错或者不存在,Flake8 不会提示,Ruff 却会专门报一条 RUF100 规则,指出# noqa注释里有未使用的代码(即该行根本没有触发你指定的规则)。比如你写了# noqa: F841,但这一行其实没有未使用变量,Ruff 就会告诉你这个 noqa 是多余的。这个特性刚开始有点烦,习惯之后反而喜欢,因为会发现很多项目里积攒了成堆的无效 noqa,删掉后代码干净不少。
4.3 文件级忽略与按文件名忽略
# noqa默认只对当前行生效,但如果想忽略整个文件,Flake8 的做法是在文件顶部加一行特殊注释:
# flake8: noqa import os from pathlib import Path ...这行表示当前文件所有行都不做检测。Ruff 同样兼容这个语法。不过需要提醒,如果启用 RUF100 规则,在文件级 noqa 的场景下,其它 noqa 行会被认为是多余的,因为整个文件都已经豁免了。这种时候要删除文件内的所有单行 noqa,避免冗余。
如果你不想在源码文件里加魔法注释,还有一个更工程化的选择,就是在 pyproject.toml 里面配置按文件名忽略规则:
[tool.ruff.lint.per-file-ignores] "migrations/**" = ["E501"] "scripts/*.py" = ["S311"]我比较推荐这种做法,因为规则设置跟着项目管理走,不会出现在业务代码里,不至于让代码里堆满注释。它和支持文件级忽略的最大区别是:per-file-ignores 仍然会在那文件运行其他规则,只是对指定规则豁免;而# flake8: noqa是整文件完全不做检查,二选一的时候要想清楚。
4.4 使用# noqa常见误区
用# noqa最典型的误区有三个,我全踩过。
第一个误区是注释位置不对。# noqa必须跟在代码的同一行末尾,中间可以有一个或多个空格,但不能换行。如果我用多行方式表达一个逻辑,比如一个很长的函数调用分成多行,noqa 必须放在实际报错的那一行。比如:
some_function( arg1, arg2, # noqa: E501 )如果报错其实在some_function(那行,而我把 noqa 写在 arg2 行,等于完全没效果。遇到这种多行报错不明确时,最快的方式是把 noqa 放在第一行开头那个函数调用处,同时保留在行尾。
第二个误区是# noqa被 Ruff formatter 移动位置。如果你同时启用了 Ruff formatter,保存格式化的时候它会将多条 import 挤到一行,或者把长行重新拆行,那么 noqa 注释的物理行位置就会变。格式化之后 noqa 注释可能落在错误行上,规则被错误豁免或者变成冗余。解决方案很简单:保存文件后跑一遍ruff check --fix,RUF100 会揪出失效的注释,然后git diff看一下格式化和注释的变动是否合理,再提交。
第三个误区是大小写和空格。Ruff 对 noqa 的解析比较严格,# NOQA不会生效,# noqa:E501(冒号前不加空格)在 Flake8 里能被识别,在 Ruff 里不会有问题,但为了和其他插件兼容还是统一写成# noqa: E501更稳。末尾多了空格没有影响,但注释里写了规则代码的同时又写了解释性文字,比如# noqa: F401 - 暂时不用, 中间的破折号部分会被解析进规则列表里,导致规则代码无效。这也是常见翻车点,要保持注释格式是单纯的# noqa: F401,解释原因写在这行的上面一行。
5. 常见问题与排查记录
5.1 插件不生效
VSCode 里 Ruff 插件装好了,但打开文件没有任何反应,这是最常见的开局栽跟头。排查顺序一般是这样:先看 VSCode 输出面板里有没有选到正确的输出通道,选择 "Ruff" 通道;然后点右下角 Python 解释器,确认当前环境确实安装了 ruff。在终端里手动敲ruff --version是最直接的测试,如果命令都不存在,插件自然什么都干不了。
还有一种隐蔽情况是插件读取的 ruff 路径和命令行不是同一个二进制。我用 uv 管理环境时遇到过,终端里which ruff指向的是 uv 缓存的全局路径,但 VSCode 用的解释器虚拟环境里又单独装了一份旧版本。版本不一致会导致配置项行为不同,比如旧版不认识新版配置文件里的某些键值,插件就会默默降级或者报错。解决方式是统一用项目环境安装,并且在设置里指定插件的诊断路径,确保两者一致。
如果配置里写的是ruff.lint.args,检查一下参数是否真的指向了正确的配置文件。路径写错时插件会报"failed to load config"之类的提示,这个信号很明显。确认配置没问题之后,重载窗口(命令面板输入 Reload Window)让插件重新读取配置文件,变更配置文件后不重载有时不生效。
5.2 与Pylance或其他工具冲突
Pylance 本身提供类型检查、错误提示、import 补全,它也会根据自己的规则报告一些问题,比如 unreported import。Ruff 是代码风格和逻辑检查,两者经常在同一条线上给出不同严重程度的提示,视觉上会产生混淆。这不是 Bug,也不要想着把 Pylance 关了来解决,而是搞清楚每条波浪线来自哪里。Pylance 的报错在状态栏显示的语言服务提示里能看到来源,Ruff 的提示里则会带规则代码。团队里那个“红色波浪线强迫症”如果受不了,可以在 settings.json 里把 Pylance 的特定诊断类型关掉:
"python.analysis.diagnosticSeverityOverrides": { "reportUnusedImport": "none" }这是因为未使用导入的检测原来有 Pylance 的一份,Ruff 也有一份,两处同时点亮看着很乱。这种叠加只保留一处来源就好,Ruff 的修复能力远比 Pylance 强,所以我会建议关掉 Pylance 的,保留 Ruff 的。
格式化工具的冲突也值得多说一步。Ruff formatter 出现之前很多项目用 Black,VSCode 里默认格式化器可能还留着 Black。即使把默认 format 换成了 Ruff,保存时的 hook 或 pre-commit 配置里如果还在跑 Black,那就出现了黑箱格式化后 Ruff 再检查的捉迷藏局面。项目统一用 Ruff formatter 时,删除所有 Black 相关配置和 pre-commit 条目,别让两个风格标准同时存在于仓库里,不然代码会反复横跳。
5.3 noqa不生效的排查
# noqa写了但依然报错,这是我会反复遇到的一个问题,别慌,走一遍下面的流程就好。
先检查是否启用了 RUF100 规则,它会告诉你哪些 noqa 其实没起作用。比如你写了# noqa: F401,但那一行压根没有未使用导入,Ruff 会报 RUF100 (Unusednoqadirective),意思是注释是冗余的。但这种提示也反过来说明:如果没提示,那这个 noqa 是被认可的。
再看看 noqa 注释位置。对于多行的语法构造,比如字典推导、多行函数定义,报错的定位行可能和你预想的不一样。打开问题面板,点报错条目能看到它定位在第几列,然后去核对 noqa 是否定在了那一行。还有一种常见情况是启用了某些规则会在"文件所有行"水平上报告问题,比如文件里所有行都存在某问题,或者该规则本身是"整行协议",这时候 noqa 不起作用的可能性更高。遇到这种情况,改用 per-file-ignores 来豁免比逐行加 noqa 更合理。
最后,极少数情况是 Ruff 版本新加了一些规则,而插件内缓存的 lint 结果还没刷新。在命令面板执行 "Ruff: Restart server" 可以强制重扫。版本更新后规则行为有变化的情况并不罕见,保持项目里的 ruff 版本固定并定期升级,可以避免很多莫名其妙的回归。
5.4 团队配置同步
单人项目怎么配都行,多人协作才是真正考验配置管理的地方。配置文件的入口必须统一,我建议只保留一个仓库根目录下的 ruff.toml 或 pyproject.toml 里的 [tool.ruff] 段,不要有多份配置散落在子目录里。Ruff 查找配置时会向上遍历目录,子目录里如果有额外配置坑人会比较隐蔽。
pre-commit 里再挂一份 ruff 检查,保证所有提交都跑同一套规则:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.0 hooks: - id: ruff args: [--fix] - id: ruff-format编辑器体验和 CI 的一致性很重要。如果你在一台机器上插件报 A 规则,而 CI 上根本不报,先检查两处读取的 ruff 版本是否一致。版本不一致带来的规则差异是团队协作里最容易出"灵异事件"的地方。我在某公司接过一个项目时就遇到过:本地插件显示某一行有 F401 报错,但 CI 却通过了,查下来是 CI 拉取的 ruff 版本比较旧,这条规则还没引入。最后锁定了 ruff 版本号才解决问题。
Ruff 的配置还有一个有趣的层面是规则演进。它每个版本都会新增规则,也会调整部分规则的默认行为。如果你想平滑升级,在 CI 配置里加上--output-format的记录,升级时先用ruff check --preview预览新规则可能带来的报错,再决定要不要把规则加进 select。这样既不会被新版本报错淹没,也能逐步享受新规则带来的收益。我个人经验是每个季度固定花一天时间做规则梳理和升级,比被动应对报错要省心得多。
6. 几个实用建议与经验补充
Ruff 插件在 VSCode 里的体验已经非常完善了,但我还是想补充几个不常用到但很有用的配置。一个是通过ruff.lint.ignore在编辑器中临时忽略项目配置里的规则,这个和项目配置文件里的 ignore 不是一回事,适合个人开发时的临时调整。另一个是ruff.trace.server,设置为verbose时可以看到插件与 ruff server 的交互日志,排查插件问题巨好用,不过这个设置会输出大量日志,平时不要开着,排查完记得关掉。
还推荐去做一件事:在 lint 规则里开启 RUF100。既然说到 noqa,顺带提一句这种规则的代码洁癖价值,它会主动清理掉项目里所有用不到的 noqa 注释,让你对代码里到底哪几行是被真正豁免非常清楚。配合# noqa: E501这种显式写法,整个项目的禁用意图会非常可审计。
如果有同学还在用老 Flake8 配置,迁移到 Ruff 的步骤建议是先保留原来的 Flake8 配置,在 Ruff 的 select 里启用对应的规则代码,跑一遍对比两边的报错差异。因为 Ruff 对部分规则的实现细节和 Flake8 略有差异,比如某些边界情况处理方式不同,直接切换一定会有一些小噪音。先并行跑,等差异收敛到零,再去掉 Flake8,整体迁移风险就低很多。这个策略在那个迁移项目里帮了大忙,从 Flake8 全家桶换成 Ruff 前后用了一周,全程没有因为检查工具不一致导致偶发回归。