1. 为什么我劝你把 Ruff CLI 真正用起来
Ruff 这个工具刚出来的时候,我其实没太当回事——Python 生态里格式化有 Black,静态检查有 Flake8,导入排序有 isort,类型检查有 mypy,各管一摊,虽然配置起来烦,但好歹用习惯了。直到有一次接手一个历史项目,光是把 Flake8、isort、pylint 三套配置对齐就花了大半天,跑一遍全量检查要等将近两分钟,CI 上更是慢得让人想砸键盘。后来抱着试试看的心态把 Ruff 接进去,同样的代码库,全量检查加自动修复不到两秒跑完,那一刻我就知道这玩意儿回不去了。
Ruff 是用 Rust 写的 Python 静态分析和代码格式化工具,定位很明确:把过去分散在多个工具里的活,用一个二进制、一套配置、一次扫描全部干完。它兼容 Flake8 的大量规则集、isort 的导入排序逻辑、pyupgrade 的语法升级建议,还自带格式化能力,对标 Black。对日常写 Python 的人来说,它解决的核心痛点就三个:速度慢、配置散、工具链长。这篇文章我打算把 Ruff CLI 从最基础的安装调用,到配置文件怎么写、规则怎么选、CI 里怎么接、踩过的坑怎么绕,完整捋一遍。适合刚听说 Ruff 想上手的同学,也适合已经在用但只停留在ruff check层面的朋友——命令行里藏着的细节比想象中多。
我写这篇东西的出发点很简单:官方文档虽然全,但它是按功能罗列的,缺少一条“从零到日常使用”的主线。我把自己这一年多在不同项目里用 Ruff 的经验按使用顺序重新组织,配上实际参数和配置片段,你照着抄就能跑起来。
2. Ruff CLI 的整体设计与命令结构拆解
2.1 一个二进制搞定检查与格式化,这个设计取舍值不值
Ruff 把所有能力收进一个可执行文件,通过子命令区分功能。核心就两个:ruff check负责静态分析和自动修复,ruff format负责代码格式化。这种“单二进制多子命令”的设计,和 Git、Cargo 是一个路子,好处很直接——安装一个东西,记住一套参数风格,不用在多个工具之间切换心智模型。
但这里有个容易被忽略的取舍:Ruff 的检查(lint)和格式化(format)是两套独立的规则体系。ruff check管的是代码质量规则,比如未使用变量、导入顺序、复杂度告警;ruff format管的是纯排版,比如缩进、引号风格、换行位置。两者互不干扰,你可以只用其中一个,也可以两个都用。我见过有同学以为跑了ruff format就等于做了代码检查,结果提交上去一堆未使用导入,这就是没搞清楚职责边界。
为什么这么设计?因为格式化和静态检查的诉求本质不同。格式化追求确定性——同样的代码进去,永远同样的排版出来,不应该有“建议”这种模糊地带;静态检查则要给出可配置的规则集,不同项目对严格程度的要求天差地别。把两者拆开,各自演进,配置也分开管理,逻辑上更干净。实际用下来,我建议两个都开,但要在 CI 里分两步跑,后面会细说。
2.2 子命令全景:除了 check 和 format 还有哪些值得关注
大多数人日常只用check和format,但 Ruff CLI 还有几个子命令在特定场景下很有用。ruff rule用来查某条规则的具体说明,比如你想知道F401到底管什么,直接ruff rule F401就能看到详细解释和反例正例,比翻文档快。ruff linter列出所有支持的 linter 前缀,ruff clean清缓存,ruff version看版本。还有一个ruff analyze在较新版本里提供更细粒度的分析能力,不过日常用得少。
我特别想提ruff rule,这是排查“这条规则为什么报”的神器。有次同事问我为什么他的代码被标了个B006,我直接让他跑ruff rule B006,输出里写得明明白白:可变默认参数的问题,还给了错误写法和正确写法对照。这种“规则即文档”的设计,省掉了大量搜索时间。
命令的通用参数也值得说。--config指定配置文件路径,--isolated忽略所有配置只按默认跑(排查配置冲突时好用),--no-cache禁用缓存,--verbose输出详细日志。这些参数在check和format上都通用,记住一次到处能用。
2.3 退出码与输出格式:CI 集成的关键约定
Ruff 的退出码约定很清晰:0表示没有发现问题,1表示发现了可修复或不可修复的问题,2表示 Ruff 自身出错(比如配置写错了)。CI 里判断构建是否通过,靠的就是这个退出码,所以千万别在脚本里把ruff check的返回值吞掉。
输出格式方面,默认是人类可读的文本,带文件名、行号、列号、规则码和说明。但 CI 里更常用的是--output-format参数,支持concise、full、json、junit、github等。接 GitHub Actions 时用github格式,问题会直接以注解形式标在 diff 上,review 体验好很多;接其他 CI 平台用json自己解析也行。这个细节很多人不知道,白白浪费了 Ruff 和 CI 平台的集成能力。
3. 安装与基础命令实操:从零跑通第一条检查
3.1 安装方式怎么选:pip、pipx 还是独立二进制
安装 Ruff 有三条主流路径,选哪条取决于你的使用场景。项目内依赖用pip install ruff写进requirements-dev.txt或pyproject.toml的 dev 依赖组,保证团队每个人版本一致;全局工具用pipx install ruff,隔离环境不污染系统 Python;追求极致启动速度或者 CI 镜像精简,直接下独立二进制,解压即用,连 Python 都不需要。
我个人的习惯是:项目里锁版本,本地开发用项目虚拟环境里的那个,CI 里也用同一个版本。为什么强调锁版本?因为 Ruff 迭代很快,规则集和默认行为在不同版本间可能有变化,今天 CI 过了明天挂掉,八成是版本漂移。在pyproject.toml里写死ruff==0.x.y,比什么都稳。
# 项目内安装(推荐) pip install ruff==0.6.9 # 全局工具安装 pipx install ruff # 验证安装 ruff --version注意:不要同时用 pip 全局装一个、项目虚拟环境里又装一个,然后搞不清
ruff命令到底调的是哪个。用which ruff确认路径,或者干脆统一走虚拟环境。
3.2 第一次运行 check:默认规则集能查出什么
装好之后,进到任意 Python 项目根目录,直接跑ruff check .。默认情况下 Ruff 启用的是E(pycodestyle 错误)和F(Pyflakes)两组规则,覆盖了最常见的语法级问题:未使用导入、未定义变量、重复定义、f-string 占位符不匹配等等。这个默认集是刻意保守的,保证开箱即用不会满屏红。
第一次跑大概率会看到一堆F401 imported but unused。别慌,这正是 Ruff 的价值——它帮你把历史遗留的垃圾导入一次性揪出来。先别急着--fix,用ruff check . --statistics看一眼按规则码聚合的统计,心里有个数:哪类问题最多,是不是集中在某几个文件。这个统计视图在评估一个陌生代码库的健康度时特别好用。
# 基础检查 ruff check . # 按规则聚合统计 ruff check . --statistics # 只看某个目录 ruff check src/3.3 自动修复的正确姿势:--fix 与 --unsafe-fixes 的边界
ruff check --fix会自动修复所有被标记为“安全可修复”的问题,比如删掉未使用导入、调整导入顺序、把== None改成is None。这些修复不改变代码语义,放心跑。但有一类修复被标记为“不安全”,比如把dict()调用改成{}字面量——大多数情况等价,但如果你重载了dict这个名字,语义就变了。这类修复默认不执行,需要显式加--unsafe-fixes。
我的经验是:日常开发只开安全修复,--unsafe-fixes只在明确知道后果时用,且必须配合代码审查。曾经有同事图省事直接--fix --unsafe-fixes一把梭,结果一个模块里自定义了同名函数,被改出了隐蔽 bug,测试没覆盖到,上线才发现。安全修复和不安全修复的边界,是 Ruff 团队反复权衡过的,尊重这个边界就是尊重自己的睡眠。
# 安全修复 ruff check . --fix # 包含不安全修复(谨慎) ruff check . --fix --unsafe-fixes # 修复后重新检查确认 ruff check .3.4 format 子命令:和 Black 的兼容性与差异
ruff format .的定位是 Black 的替代品,官方宣称在绝大多数情况下输出与 Black 一致,速度却快一个数量级。实测下来,对于一个中等规模项目,Black 格式化要几秒,Ruff 基本是瞬间完成。差异主要出现在少数边缘 case,比如某些复杂表达式的换行策略、魔术尾逗号的处理。如果你是从 Black 迁移过来,建议先在一个分支上跑一遍ruff format,用git diff看看改动范围,确认没有意外再合并。
ruff format --check是 CI 里的标准用法,只检查不修改,有不符合格式的文件就返回非零退出码。--diff参数更直观,直接打印出会做的修改,适合在 review 时给人看。这两个参数配合使用,CI 里既能把关又不产生副作用。
# 格式化整个项目 ruff format . # CI 中只检查 ruff format --check . # 查看会改什么 ruff format --diff .4. 配置文件与规则选择:让 Ruff 贴合你的项目
4.1 pyproject.toml 里的 ruff 配置段怎么写
Ruff 的配置统一放在pyproject.toml的[tool.ruff]段下,或者独立的ruff.toml。我倾向放pyproject.toml,一个文件管所有工具配置,清爽。最基础的配置长这样:
[tool.ruff] line-length = 100 target-version = "py311" exclude = [".venv", "build", "dist", "migrations"] [tool.ruff.lint] select = ["E", "F", "I", "UP", "B"] ignore = ["E501"] [tool.ruff.format] quote-style = "double" indent-style = "space"line-length设成 100 还是 88(Black 默认)看团队习惯,关键是和格式化、检查保持一致,别检查按 100 格式化按 88,那就自相矛盾了。target-version告诉 Ruff 你的最低 Python 版本,它据此决定哪些语法升级建议是安全的——比如你写py38,它就不会建议你用py310才有的 match 语句。exclude把虚拟环境、构建产物、数据库迁移文件排除掉,迁移文件通常是自动生成的,没必要检查。
4.2 规则集选择策略:从保守到激进的渐进路线
select里放哪些规则前缀,直接决定 Ruff 的严格程度。我的建议是分阶段来,别一上来就全开。第一阶段用E、F、I(导入排序)、UP(语法升级),这四组基本无争议,修复也安全。第二阶段加B(bugbear,抓常见陷阱)、SIM(简化建议)、C4(推导式优化)。第三阶段再考虑ANN(类型注解强制)、D(文档字符串)、PL(pylint 规则集)这类主观性强的。
为什么强调渐进?因为一次性开太多规则,历史代码会瞬间爆出几千条告警,团队直接放弃治疗。正确做法是先把select设成目标集合,然后用per-file-ignores或者ignore把暂时不想管的规则压下去,随着代码逐步清理再放开。这个过程我在三个项目里走过,每次都是“先止血、再治病”。
| 阶段 | 推荐规则前缀 | 适用场景 |
|---|---|---|
| 保守 | E, F, I, UP | 新项目起步、历史项目初次接入 |
| 进阶 | 加 B, SIM, C4, RUF | 代码质量有一定基础 |
| 严格 | 加 ANN, D, PL, PT | 对类型和文档有强要求的团队 |
4.3 per-file-ignores 与 noqa:局部豁免的正确用法
再严格的规则集也有需要豁免的地方。测试文件里大量assert会触发S101,__init__.py里的未使用导入是故意的(为了暴露公共 API),这些都需要局部豁免。Ruff 提供两个层次:配置级的per-file-ignores和代码级的# noqa注释。
per-file-ignores写在配置里,按文件路径模式匹配,适合成批的、有规律的豁免:
[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "tests/**/*.py" = ["S101", "ANN201"]# noqa写在代码行尾,适合单点的、临时的豁免。# noqa: F401精确豁免某条规则,# noqa裸写豁免该行所有规则。我强烈建议永远写具体的规则码,裸# noqa等于关掉了这一行的所有保护,后患无穷。还有个细节:# noqa只对同一行的告警生效,如果告警报在上一行(比如多行表达式的起始行),注释位置要放对,否则不生效——这个坑我踩过不止一次。
4.4 缓存机制与 --no-cache 的使用时机
Ruff 默认开启缓存,把文件内容和检查结果的映射存在.ruff_cache目录。第二次跑同样的检查,没改过的文件直接读缓存,速度飞快。这个机制在本地开发时体验极好,但在两种情况下会坑人:一是你改了配置文件但缓存没失效(极少见,Ruff 会检测配置变化),二是 CI 里缓存目录被错误地跨分支复用。
排查“明明改了配置却不生效”这类玄学问题时,第一反应应该是ruff check . --no-cache。如果加了--no-cache结果就对了,那问题出在缓存。CI 里我一般建议把.ruff_cache加进.gitignore,并且不要跨 job 共享,宁可慢一点也要结果确定。
5. 进阶用法与 CI 集成实战
5.1 pre-commit 钩子配置:提交前拦截问题
把 Ruff 接进 pre-commit,能在代码提交前就拦下问题,比等到 CI 失败再回头改高效得多。配置写在.pre-commit-config.yaml:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.9 hooks: - id: ruff args: [--fix] - id: ruff-format注意rev要和项目里锁的 Ruff 版本对齐,否则本地钩子和 CI 用的版本不一致,会出现“本地过了 CI 挂”的尴尬。ruff钩子带--fix自动修复,ruff-format钩子负责格式化。两个钩子的顺序有讲究:先 lint 修复(可能改动导入顺序),再 format(统一排版),顺序反了可能 format 完又被 lint 改动,导致需要二次提交。
提示:pre-commit 钩子只检查暂存区的文件,全量检查还是得靠 CI。别以为本地钩子过了就万事大吉。
5.2 CI 流水线里的两步走:check 与 format 分开跑
CI 里我坚持把检查和格式化分成两个独立步骤,而不是合并成一条命令。原因有二:一是失败时能一眼看出是质量问题还是格式问题,排查路径清晰;二是格式化检查可以用--check只读模式,不产生任何文件改动,流水线更干净。
# 伪代码示意,适配任意 CI 平台 steps: - name: Lint run: ruff check . --output-format=github - name: Format check run: ruff format --check .--output-format=github让问题直接以注解形式出现在 PR 的 diff 上,reviewer 不用点进日志就能看到哪行哪列有问题。如果你的 CI 平台不支持 github 格式,用json输出再自己解析,或者退回默认文本格式也行,关键是别把退出码吞掉。
5.3 增量检查与全量检查的取舍
大项目全量跑 Ruff 虽然快,但也不是没有成本。日常开发我建议只检查改动的文件,pre-commit 钩子天然就是增量的。CI 里则应该全量跑,因为增量检查会漏掉“改动 A 文件导致 B 文件出问题”这种跨文件影响——虽然 Ruff 的规则大多是文件内分析,但导入相关的规则确实可能受其他文件影响。
有个折中方案:CI 里对 PR 做增量检查加速反馈,对主分支合并做全量检查兜底。具体实现依赖 CI 平台的能力,核心思路是“快速反馈”和“完整把关”分开。这个策略我在一个上万文件的项目里用过,PR 检查从几分钟降到几十秒,体验提升明显。
5.4 与编辑器集成:保存即格式化的配置要点
VS Code 里装 Ruff 官方扩展,把默认格式化器设成 Ruff,开启保存时格式化,基本就齐活了。关键配置项是editor.formatOnSave和editor.codeActionsOnSave,后者可以配置保存时自动执行ruff check --fix的安全修复。这样写代码的过程中,导入自动排序、未使用变量自动清理,几乎感觉不到工具的存在。
这里有个细节值得说:编辑器集成用的是 Ruff 的语言服务器模式,和命令行共享同一份配置。所以你在pyproject.toml里改的规则,编辑器里立刻生效,不用重启。但如果配置写错了,编辑器可能静默失败,表现是“格式化没反应”。遇到这种情况,先在命令行跑一遍ruff check .,配置错误会直接报出来,比在编辑器里瞎猜快得多。
6. 常见问题与排查技巧实录
6.1 规则冲突与误报:怎么判断该改代码还是改配置
Ruff 报的告警不是圣旨,有些规则在特定上下文里就是误报。判断标准很简单:如果这条告警指向的代码确实有隐患,改代码;如果代码是刻意为之且合理,改配置豁免。比如B008报“函数调用作为默认参数”,大多数情况是真问题(默认参数在定义时求值一次),但如果那个调用是Depends()这类框架的依赖注入标记,就是合理用法,该豁免。
我见过有人为了消掉一条误报,把代码改得面目全非,这就本末倒置了。Ruff 是工具,工具服务于代码,不是反过来。遇到拿不准的规则,先ruff rule <code>看官方解释和示例,再决定。
6.2 格式化后 diff 爆炸:迁移时的分批策略
从 Black 或 autopep8 迁移到ruff format,第一次全量格式化可能产生巨大的 diff,review 根本没法看。我的做法是:单独开一个“纯格式化”提交,提交信息写清楚“仅格式化,无逻辑改动”,让 reviewer 知道这个 commit 不用逐行看。然后在这个提交之上再做功能改动。这样 git blame 的时候也能一眼区分格式改动和逻辑改动。
如果项目特别大,还可以按目录分批格式化,每次一个子目录,逐步推进。虽然麻烦点,但每次 diff 可控,出问题容易回滚。
6.3 版本升级导致的规则变化:锁定与灰度
Ruff 版本升级偶尔会带来默认行为变化,比如某条规则从默认关闭变成默认开启,或者修复策略调整。这就是为什么我一直强调锁版本。升级时不要直接改版本号了事,先在本地跑一遍全量检查,对比升级前后的告警数量差异,评估影响面。如果差异大,看看是不是新规则导致的,用ignore临时压住,再安排时间逐步清理。
| 常见现象 | 可能原因 | 排查动作 |
|---|---|---|
| 改了配置不生效 | 缓存未失效 | 加--no-cache重跑 |
| CI 过了本地挂 | 版本不一致 | 对比ruff --version |
| 格式化没反应 | 配置语法错误 | 命令行跑ruff check .看报错 |
| 告警数量暴涨 | 版本升级引入新规则 | 对比升级前后--statistics |
| noqa 不生效 | 注释位置不对 | 确认注释在告警所在行 |
6.4 性能调优:大仓库下的检查加速手段
Ruff 本身已经很快,但在超大仓库里还是有优化空间。第一,确保exclude配置到位,虚拟环境、构建产物、第三方 vendored 代码统统排除,别让 Ruff 去扫不该扫的东西。第二,善用缓存,本地开发别动不动--no-cache,那是排查问题用的,不是日常用的。第三,CI 里如果支持并行 job,可以按目录拆分检查任务,虽然 Ruff 单进程已经够快,但超大仓库拆分后反馈更快。
还有一个容易忽略的点:target-version设得越准确,Ruff 能做的优化越多。如果你实际用 py311 但配置写的 py38,Ruff 会保守地不用一些新版本才有的分析能力。把这个配对准,算是免费的性能提升。
7. 我踩过的坑和几条实在建议
说几个只有真用过才会知道的细节。第一,ruff check --fix和ruff format的执行顺序会影响最终结果,我的习惯是先check --fix再format,因为 check 的修复可能引入新的格式问题(比如删掉导入后留下空行),format 收尾正好抹平。第二,# noqa注释在格式化后可能位置漂移,如果某行同时有长表达式换行和 noqa,格式化后 noqa 可能跑到错误的行上,导致豁免失效——这种 case 建议把表达式拆短,别依赖 noqa 的位置。
第三,团队协作时,配置文件一定要进版本控制,而且要在 README 或者贡献指南里写清楚“提交前请跑 pre-commit”。我见过项目配了钩子但没人装,等于白配。第四,别迷信“规则开得越多越好”,规则是为代码服务的,一个团队连E和F都没清理干净就上ANN强制类型注解,只会制造抵触情绪。循序渐进,让工具成为助力而不是负担。
最后分享一个我常用的组合命令,日常开发收尾时跑一遍,基本能保证提交质量:
ruff check . --fix && ruff format . && ruff check .先修复、再格式化、最后复查确认干净。三步串起来,一条命令搞定。这个习惯坚持下来,代码库的整洁度会有肉眼可见的提升。