在 Python 工程化这条路上,代码检查工具的选型一直是个磨人的话题,而 Ruff 这两年几乎成了绕不开的答案。早几年大家习惯 flake8 管语法错误和风格告警、black 管格式化、isort 管 import 排列,三个工具三份配置,各有各的脾气,跑一次全量检查往往要等十几秒。Ruff 就是冲这个局面来的——一个用 Rust 写的 Python 检查与格式化工具,一条命令同时接管 lint、自动修复、格式化和 import 排序,官方宣称比传统工具链快 10 到 100 倍。这篇文章不打算写说明书式文档,而是记录我把它接入实际项目后的完整经验:配置怎么规划、规则怎么选、编辑器和 CI 怎么联动,以及那些文档里不会明说的坑。适合所有正在用 flake8/black 组合、或者还没选型的新项目直接参考。
1. 为什么大家都在换Ruff
1.1 传统工具链的痛点在哪
先说清楚旧时代的情况。flake8 本身是 pycodestyle 加 pyflakes 再加 mccabe 的聚合,问题在于它默认只带很少的规则,很多团队习惯再挂一堆第三方插件补规则。black 出来以后,格式化风格算是统一了,但它和 flake8 经常打架:black 默认把行宽折到 88 字符,flake8 的 E501 却按 79 字符告警,于是你得在 setup.cfg 里把 max-line-length 改成 88,两边才消停。isort 又掺和进来,负责把 import 排序,但它和 black 在引号处理、换行格式上也会起冲突,得靠 profile = "black" 这种参数强行对齐。
工具之间互相制约,配置文件分散在 setup.cfg、.isort.cfg、pyproject.toml 各处,新成员上手成本很高。我接手过一个中等规模的模拟项目,lint 全量跑一次要 8 到 10 秒,format 检查还要单独再跑一遍,改一行代码想验证干净,来回折腾小半分钟。CI 里还要把这三样都串起来,每次提交都有一堆慢步骤。这种感觉说白了:工具本身成了工程效率的瓶颈。
1.2 Ruff 的核心优势从哪来
Ruff 的第一步改革是把“检查”和“格式化”做进同一个二进制。它由 Rust 实现,解析、分析、报告都走并行和缓存,很多项目上的实测对比,原有 flake8 加一坨插件的耗时从几秒甚至十几秒降到百毫秒级别。这不是挤牙膏式的优化,而是把整个 lint 过程从“提交后再等”变成“写的时候随时跑”。
第二步改革是规则兼容。Ruff 内置了超过 800 条规则,覆盖 pycodestyle、pyflakes、isort、pydocstyle、pyupgrade、bugbear、comprehensions、bandit 安全检查等一大批常见来源,规则前缀沿用 flake8 生态的习惯,比如 E 系列、F 系列、W 系列。这意味着老项目迁移不用重学规则,把配置里的前缀平移到 [tool.ruff.lint] 的 select 里就能获得九成体验。
第三步是把插件链路的复杂度收掉。传统 flake8 每加一个插件就是一次依赖安装和版本兼容测试,Ruff 把这些能力内置后,项目的工具依赖从一个集合收敛到一个包。配合预提交钩子和编辑器扩展,团队内只需要统一一个工具版本,不需要再互相核对“你那边 flake8 多了哪个插件”。
| 能力项 | flake8 + black + isort 组合 | Ruff |
|---|---|---|
| 独立程序数量 | 至少3个 | 1个二进制 |
| 配置文件 | 分散在 setup.cfg、.isort.cfg 等多个文件 | 统一在 pyproject.toml |
| 全量检查耗时 | 秒级到十秒级 | 通常百毫秒级 |
| 规则扩展方式 | 需要安装第三方插件并维护版本 | 内置800多条规则 |
| 自动修复能力 | 插件零散,覆盖不均 | 常用规则原生支持 --fix |
| import 排序 | 依赖 isort 单独执行 | 内置 I 系列规则 |
2. 五分钟把Ruff接进现有项目
2.1 安装方式和基础命令
Ruff 的安装很简单,不用考虑 Python 解释器版本,它是独立二进制。最常见的安装方式:
pip install ruff如果项目已经用上了 uv,那更省事,直接用 uvx 临时跑,或者作为项目统一工具链安装:
uv tool install ruff uvx ruff check .装完先别急着写配置,直接用默认行为跑一遍代码库,看看存量问题到底有多少:
ruff check .第一次跑,看到的结果通常有两种:干净,或者密密麻麻一片。干净说明你以前的工具链已经比较严;密密麻麻也正常,说明你现在用的规则比原来多。这时候不要慌,先把报告里的文件数、问题数记下来,作为迁移基线。想修的可自动修的直接来一轮:
ruff check . --fix--fix 只会动那些 Ruff 判定为可安全修改的问题,比如未使用的 import、可以合并的 import 块、某些自动升级语法的场景。它不会碰有歧义的修改,那些会留在报告里让你人工决定。
格式化的命令独立于检查命令:
ruff format . # 只检查格式不动文件,用于 CI: ruff format --check .注意:check 和 format 是两条独立链路。check 负责语法规则和可修复项,format 负责文件排版。刚上手时最常犯的错就是把它们混在一起,以为跑完 format 就等于跑完 lint。实际上 format 只处理排版问题,像未定义变量、未使用的 import 这些它根本不管,必须靠 check 兜底。
2.2 pyproject.toml 配置怎么规划
Ruff 支持在 pyproject.toml 里写 [tool.ruff] 段,也支持独立的 ruff.toml。对于新项目我强烈建议放进 pyproject.toml,让工程配置只认一个文件。
一段可以直接抄的基线配置:
[tool.ruff] target-version = "py311" line-length = 88 src = ["src", "tests"] exclude = ["migrations", "venv", ".venv"] [tool.ruff.lint] select = ["E", "F", "W", "I", "UP", "B", "C4"] ignore = ["E501"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "tests/**/*.py" = ["S101"] [tool.ruff.lint.isort] known-first-party = ["myproject"]逐项说明。target-version 影响规则建议的语法下限,比如你写 py311,Ruff 会推荐更现代的写法;如果设太低,可能推荐风格偏保守。line-length 必须和 formatter 保持一致,Ruff 的 formatter 默认按 88 折行,所以 lint 这边 E501 只管不超这个值。src 告诉它代码根在哪,isort 的 first-party 识别就用这个。exclude 排除生成代码和虚拟环境,避免检查一堆机器生成文件。
select 是白名单思路,只开你想要的规则前缀。新手建议从 ["E", "F", "I"] 起步,E 是风格错误,F 是逻辑错误,I 是 import 排序,这三类噪音小、价值高,团队接受度也高。跑顺了再加 UP(语法升级)、B(常见 bug 隐患)、C4(推导式优化)这些。ignore 用来关掉少数噪音规则。E501 我在这个基线里选择忽略,因为 formatter 已经负责折行,再报一遍长行纯属重复劳动。
per-file-ignores 特别实用。最常见的场景是init.py 里的 F401:包暴露接口时习惯在init.py 里统一 import 再导出,此时未使用的 import 不是错误而是设计。把 F401 只对init.py 忽略,既保留其他文件里的检查,又不误伤包的公共入口。测试目录里经常用 assert 和调试写法,S101 是 bandit 对 assert 的告警,测试文件里忽略它是合理选择。
isort 段用于让 import 排序符合项目内部命名。known-first-party 告诉它哪些模块是自己项目的,排序时放到第三方包后面。如果项目里有一些私有组织包,也在这里登记。这一段配好了,整个团队的 import 风格会变得非常统一。
3. 规则体系拆解与自动修复实战
3.1 规则前缀不是装饰,是一套索引
Ruff 的规则前缀继承自 flake8 生态,但比 flake8 更有体系。看到前缀就能猜出规则来源和检查方向:
| 前缀 | 主要来源 | 检查内容 | 起步建议 |
|---|---|---|---|
| F | pyflakes | 未使用 import、未定义变量、不可达代码 | 强烈建议 |
| E/W | pycodestyle | 缩进、行宽、空行、空白符 | 强烈建议 |
| I | isort | import 排序、合并 | 建议 |
| N | pep8-naming | 命名风格 | 看团队 |
| UP | pyupgrade | 旧语法升级到现代写法 | 稳定后开 |
| B | flake8-bugbear | 可变默认参数、异常处理隐患 | 建议 |
| C4 | flake8-comprehensions | 推导式最佳实践 | 建议 |
| SIM | flake8-simplify | 表达式简化 | 谨慎 |
| S | bandit | 安全问题 | 逐步开 |
| D | pydocstyle | 文档字符串规范 | 看团队 |
这张表对我做选配置非常有帮助。团队如果刚接触 Ruff,我建议把 F、E、I、B 打开,这几个组合能挡住大多数真实的错误。N 和 D 看团队风格,如果文档习惯还没建立,过早开 D 容易引发大量噪音。S 建议逐步打开,先看它报什么再决定是否接收。SIM 这类“写法优化”规则会经常给出风格建议,适合老手内审,新手容易觉得被冒犯。
遇到一条具体规则不知道什么意思,不用查网页。Ruff 自带规则文档:
ruff rule F401这条命令会在终端输出该规则的完整说明、触发示例、修复示例和建议方案。我常用它来判断某条规则要不要 ignore,比去翻在线文档快得多。
3.2 自动修复和格式化的顺序安排
自动修复不是万能药,但它能覆盖绝大多数机械性工作。我实际项目里的流程基本固定成四步:
- 先跑 ruff check . --fix,让工具处理能安全自动改的问题。
- 再跑 ruff format .,统一排版。
- 再跑一遍 ruff check .,确认没有新引入的问题。
- 剩余的未修复项,逐个看报告决定手动改还是 ignore。
为什么要先 fix 再 format?因为 import 排序和代码行重组可能会改变文件的整体排版,先让 lint 把内容问题处理干净,再让 formatter 做最终排版,结果最稳定。反过来的话,formatter 排好的版可能被 lint 的自动修改再次打散。
注意:check 链路只做规则修复,不负责排版;format 链路只做排版,不负责规则。在 pre-commit 和编辑器里,这两个动作也要分开绑定,不要指望一个命令同时完成。
还有一个容易忽略的点:自动修复不等于无脑接受。有些规则虽然标记为可修复,但修复结果可能影响可读性。我一般会在代码审查时留意 diff 里来自 --fix 的改动,特别是 SIM 系列的简化,偶尔会把一行拆成多行的清晰写法“简化”成一串三元表达式,反而难读。这种情况就两行配置把该规则从 select 里摘掉,或者用行内 noqa 压掉。
4. 编辑器、提交钩子和CI联动
4.1 VS Code 里实现保存即修复
编辑器联动是 Ruff 体验最好的部分。VS Code 装好官方 Ruff 扩展后,不用再装 flake8、black 那套扩展,直接把 Python 文件的默认格式化器换成 Ruff,再把保存动作绑定为“修复全部”和“整理 import”。
打开 settings.json,写入如下配置:
{ "[python]": { "editor.defaultFormatter": "Ruff", "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit", "source.organizeImports.ruff": "explicit" } } }这里 defaultFormatter 的值要写成扩展实际注册的名称,可以通过命令面板搜索 “Format Document With” 手工选择,VS Code 会自动记住。设置保存动作后,每次 Ctrl+S,未使用的 import 会被清掉,import 顺序会被重排,缩进空格会被纠正,这些改动只作用于你当前编辑的文件,不会误伤整个仓库。实测下来,这个组合让团队里关于代码风格的讨论量几乎降为零。
扩展还有一种 “Ruff: Fix all” 命令,对应更完整的自动修复,可以绑定到自定义快捷键。不过我建议日常保存只开 fixAll 和 organizeImports,不要把完整修复绑定到保存,因为部分修复改动面较大,在关键时刻会打断思路。
4.2 pre-commit 和 CI 的集成方式
如果团队用了 pre-commit,Ruff 官方仓库提供了两个 hook。在 .pre-commit-config.yaml 里注册 id 为 ruff 的检查钩子和 id 为 ruff-format 的格式化钩子,并把 rev 固定到当前使用的版本:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.0 hooks: - id: ruff - id: ruff-format这段配置里两个钩子都不带额外参数,意思是 pre-commit 阶段只做检查,不自动修改。如果你想在提交时自动修复,给 ruff 钩子加 args: ["--fix"] 即可。我自己的项目是让 pre-commit 负责检查、编辑器负责修复,这样提交阶段的输出更稳定,不会出现提交过程里文件被悄悄改动的情况。
CI 流水线里的写法本质上就是两条命令:
ruff check . ruff format --check .任何一步非零退出,流水线就失败。这里有几个细节值得注意。
第一,CI 里的 ruff 版本必须锁死。Ruff 迭代很快,按月更新都是常态,本地 0.9 和 CI 0.11 的规则集合可能差出一截,导致“本地干净 CI 报错”。解决方案很朴素:用一个统一的依赖锁文件,或者直接固定版本号安装。
第二,CI 里不要加 --fix,除非你有后续步骤消费修改后的文件。在 lint 步骤加 --fix 会产生工作区变动,如果后面还有测试步骤,很可能出现“测试跑的是修改前的代码”这种诡异问题。检查就是检查,修复留给本地。
第三,缓存能显著提速。Ruff 会缓存解析结果,CI 里把缓存目录保存在流水线的任务缓存里,全量检查基本能做到秒级。Ruff 的缓存目录默认放在系统缓存目录,具体位置随操作系统变化,可以通过 RUFF_CACHE_DIR 环境变量指定到流水线缓存路径。
5. 踩坑记录与排查技巧
5.1 我实际遇到的典型问题
第一批踩坑多数来自老项目迁移。列几个高频问题。
E501 和 formatter 打架。这个前面说过,本质是 line-length 配置不一致。症状是 ruff format 一切正常,但 ruff check 持续报一堆 E501。解法是把 lint 的 line-length 和 formatter 对齐到同一个值,或者直接 ignore E501。我个人选择 ignore,因为 formatter 已经保证折行,再检查一次行长度没有意义。
init.py 被 F401 刷屏。老项目里init.py 经常是 re-export 的集中地,Ruff 默认把它当“未使用 import”处理。不要直接全局 ignore F401,那会漏掉真正的垃圾 import。正确做法是用 per-file-ignores 只对init.py 放行。如果项目想更严谨,可以在init.py 里定义all,Ruff 会识别all中出现的命名为“已使用”。
自定义 flake8 插件规则迁移不掉。这是最需要耐心的地方。Ruff 覆盖了绝大部分常用规则,但总有长尾插件没并入。我的经验是先按原配置逐前缀对比,把差异列出来评估:规则是否还在维护、价值是否大于迁移成本。如果某个冷门规则确实必不可少,可以暂时保留 flake8 只跑那一部分,其余全部交给 Ruff。过渡期双轨跑不丢人,确认覆盖到位后再摘掉旧工具。
ruff format 和 black 在边缘案例上输出不同。Ruff 的 format 目标是“black 兼容”,但不是逐字节相同,某些括号表达式、隐式换行的场合会不一样。首次引入 format 时,最好单独开一个 commit,把全仓库的格式统一刷一遍,再让后续改动基于新基线,避免每份 PR 都混入大量格式差异。
版本碎片化。团队里有人用 pip 全局装了一遍,有人用 pre-commit,有人用编辑器扩展,版本不一致时报告会互相对不上。解决思路是把 ruff 版本写进项目级配置:pre-commit 固定 hook 版本,编辑器扩展读取项目内安装的 ruff,CI 用锁文件。做到“代码库自带工具版本”,换机器也不用担心。
5.2 几个能明显提升体验的小技巧
最后分享几个我用了很久才沉淀下来的习惯。
用ruff rule查文档代替浏览器。任何一条规则,在终端跑ruff rule <CODE>就可以看到完整说明和触发示例,写 ignore 注释时也用它确认理解对不对。
合理使用 noqa。自动修复解决不了的问题,人工改完,以及那些确实不想遵守规则的场景,用行内注释精准压掉:
result = process_all() # noqa: B023我要求团队在 noqa 后必须写规则前缀,方便后续 review。全局 ignore 是最后的选项,每次全局 ignore 都要有可追溯的理由。
新规则先走 preview。Ruff 会把新增规则放进 preview 模式,想提前尝试可以在配置里打开:
[tool.ruff.lint] preview = truepreview 开启后启用所有候选规则,但风险是规则未稳定、下个版本可能调整默认行为。个人经验:小项目可以开 preview 尝鲜,大仓库建议等 preview 规则转正后,再按正常流程逐步 select。
保持扫描边界干净。建议在配置里显式写清 exclude,尤其是 vendor、生成目录和迁移脚本,避免每次扫描都带上无关文件。扫描范围小,速度快,误报也少。
最后一个小习惯:把ruff check和ruff format --check写进项目 README 的快速验收清单里,新人 clone 下来跑一遍就知道自己代码能不能提交。
我个人走完这一轮迁移最大的体会是:工具链的复杂度本身也是一种技术债。Ruff 并没有发明什么神乎其神的新概念,它做的就是把已经被社区验证过的规则和风格,用更快的速度和更整齐的工程形态重新交付了一遍。如果你还在用三件套,我的建议是别追求一步到位,先把 ruff check 以纯检查模式接进来,跑一两个迭代、把存量问题消化干净,再上 ruff format 统一排版。这个过程比想象中平滑,而省下来的时间,足够你多写不少业务代码。