说实话,以前我每逢接到一个有点年头的Python项目,前半个小时基本都耗在配置代码检查工具上。装Flake8、配Black、再弄isort,等这三个家伙全部调好,想看的代码早就被忘了。更别提Flake8那一套插件生态,装多了彼此冲突,装少了检查项缺失,每次开会都被其他同事追问“你那个lint配置到底怎么搞的”。直到后来在某个模拟项目X里换成了Ruff,才觉得这类工具本来就该长这样:一个二进制文件,一个配置文件,linter、formatter、import排序全搞定,跑起来还快得离谱。今天就把我这段折腾经历和踩过的坑完整写出来,给正在做Python工程化改造的读者一份可照做的参考。
Ruff是一个用Rust编写的Python静态检查与代码格式化工具,核心定位是替代Flake8、Black和isort这类传统工具的日常工作流。它的执行速度比传统工具快一到两个数量级,这也是多数人第一次用它时最直观的感受。这篇文章适合从零接触Ruff的新手,也适合在团队里推工程化但被工具链复杂度折腾到崩溃的开发者参考。
1. 为什么放弃Flake8 + Black,改用Ruff
1.1 传统工具链的痛点:插件冲突与速度短板
先说说我原来那条工具链的工作方式。Flake8本身是一个聚合器,真正干活的是它底下的pyflakes和pycodestyle,前者负责逻辑错误检查,后者负责风格规范检查。为了让代码风格更严格,我又装了一堆插件,比如检查命名规范的、检查复杂度指标的、强制类型标注的。每个插件都往Flake8里塞自己的规则,但它们的配置项各搞一套,有的认setup.cfg,有的只认tox.ini,有的还读取环境变量。有一次我在某跨平台系统里把一条规则在三个配置文件里各配了一遍,改了一下午才发现某个插件只看其中一个,简直想摔键盘。
Black的问题倒不在配置,而在于速度。纯Python写的格式化器在中等规模代码库上跑一次要十几秒,在CI里每次提交都要等这么久,体验非常割裂。isort单独用还行,但它要解析Python语法来做import分类,本质也是一个解析器加遍历器,速度同样不理想。三个工具加在一起,本地跑一轮lint加format的时间,够我喝半杯咖啡了。
1.2 Ruff的破局思路:一个二进制解决所有问题
Ruff的设计思路很直接:既然都是解析Python源码,那就用Rust写一个解析器,把安全检查、风格检查、import排序、格式化全部装进同一个二进制里。这么做有个天然优势——源码只解析一次,后面的所有检查都建立在同一棵语法树之上,性能消耗自然比每个工具都重新解析一遍要低很多。
另外一个务实的设计是规则兼容。Ruff没有重新发明规则体系,而是直接实现了Flake8系列、pyflakes、pycodestyle、isort乃至pyupgrade的规则逻辑。我原来项目里用的Flake8配置,迁移到Ruff时大部分规则代码不用变,只需要改一下配置文件的写法即可。这一点对存量项目极其友好,也是我在某公司代码库上能快速落地的关键原因。
1.3 适用场景的判断标准
Ruff并不是在所有场景下都能直接替换传统工具。如果你只是写一些一次性脚本,那用不用Ruff都不影响结果。但当你的项目满足下面这几个特征时,换到Ruff收益非常明显:
- 代码库规模中等以上,文件数量超过一两百个,原有的lint时间已经让人不适。
- 团队对代码风格有强制要求,希望把检查步骤放进CI和pre-commit流程里。
- 项目正在从Python 2向Python 3迁移,或者准备升级Python版本,Ruff的pyupgrade规则可以帮助自动处理很多语法层面的替换。
- 你在用VS Code或类似编辑器,希望保存文件时立即看到检查结果,而不是等外部工具慢悠悠跑完。
反过来讲,如果你的项目高度依赖某个Flake8冷门插件,而这个插件暂时没有对应的Ruff规则,那就要权衡迁移成本。我个人的做法是:先把支持范围内的规则全部启用,冷门需求单独保留一个轻量检查脚本,等Ruff规则补全后再彻底切过去。
2. 环境准备与快速上手
2.1 安装Ruff的几种方式及选择
Ruff的安装方式很丰富,我试过四种,各有适用场景。
用pip安装是最通用的做法,适合大多数Python项目:
pip install ruff安装完成后建议验证一下版本:
ruff --version如果你已经在用uv管理Python依赖,可以用uv添加Ruff:
uv add --dev ruff用Cargo安装适合那些本来就在Rust工具链里的开发者:
cargo install ruff在一些无网环境的服务器上,也可以直接下载官方预编译的二进制文件,解压后放到PATH目录里就能用。我当时做某图像处理Demo的CI流水线时,就是直接下载二进制,省去了pip依赖解析时间。
注意:Ruff版本迭代很快,不同版本的规则行为和默认配置会有差异。强烈建议在项目里锁定版本,比如用
ruff==0.11.2这样的精确版本号,避免某次升级后规则行为变化导致CI突然失败。
2.2 第一条命令:ruff check
安装好之后,进入项目根目录,直接运行:
ruff check .Ruff会扫描当前目录下所有Python文件,输出检查结果。我第一次在某个老项目上跑这个命令时,输出结果直接刷满了一个终端窗口。但注意,这里只是“检查”,不会修改任何文件。所以你可以放心大胆地先摸清项目的家底。
如果确认Ruff报的问题都是你想修的,加上--fix参数让它自动修复:
ruff check . --fixRuff能自动处理的问题包括未使用的导入、未使用的变量、import排序、不规范的引号、多余的括号等。但注意,不是所有问题都能自动修,比如某些逻辑层面的错误,它只能报出来,需要你手动改。
2.3 第一条命令:ruff format
接下来是格式化命令:
ruff format .这条命令会对所有Python文件做代码格式化,默认遵循的代码风格与Black基本一致。我的经验是,第一次跑格式化时最好把改动单独提交一次,不要混在其他功能修改里,否则后续review代码时很难分辨哪些是格式化改动,哪些是业务逻辑改动。
2.4 解读输出信息
Ruff检查时输出的格式非常直观,以文件名:行号:列号: 规则代码 描述的方式展示。例如:
src/utils.py:12:5: F401 `os` imported but unused这里F401是规则代码,它表示这是Flake8规则中的“未使用导入”。看到这类信息,你需要做的判断是:这个导入是确实没用,还是因为某种动态调用方式而让工具误判。如果是动态调用,可以在导入语句后面加一行# noqa: F401进行豁免。
3. 核心配置解析
3.1 配置文件的位置与格式选择
Ruff默认会按顺序查找pyproject.toml、ruff.toml和.ruff.toml。如果项目里已经有pyproject.toml,我建议把Ruff配置写进去,因为这样可以统一管理工具配置。如果你不想碰pyproject.toml,单独创建一个ruff.toml也完全没问题。
配置的基本结构长这样:
[tool.ruff] line-length = 88 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "UP"] ignore = ["E501", "E203"]这里的line-length是每行最大长度,Ruff默认是88,和Black一致。target-version表示目标开发Python版本,Ruff会根据这个参数决定参数兼容性检查的规则行为。
3.2 select、ignore与extend-select的区别
初学者最容易搞混的就是这三个配置项。
select用来指定启用哪些规则集合。如果完全用select,会把之前选定的规则全部覆盖掉。
extend-select是在已有规则基础上追加规则,通常和代码库中预先存在的select配合使用:
[tool.ruff.lint] select = ["E", "F"] extend-select = ["B", "SIM"]ignore用来忽略指定规则。我经常看到有人把ignore当作extend-select的反向操作来用,实际上它的优先级最高,被忽略的规则无论怎么select都不会生效:
[tool.ruff.lint] select = ["ALL"] ignore = ["E501", "D203"]3.3 规则类别对照表
Ruff的规则代码体系很庞杂,它复用了pydocstyle、pyupgrade、flake8-bugbear、flake8-simplify等许多生态的规则前缀。我在这张表里整理了自己最常用的规则类别,方便按需选择。
| 前缀 | 来源 | 主要检查内容 |
|---|---|---|
| E / W | pycodestyle | 风格错误与警告,如E501行太长、W291行尾空格 |
| F | pyflakes | 逻辑错误,如F401未使用导入、F841未使用变量 |
| I | isort | import排序与分组 |
| N | pep8-naming | 命名规范,如变量命名、函数命名风格 |
| UP | pyupgrade | 语法升级建议,如旧式类型写法转为新写法 |
| B | flake8-bugbear | 常见编码陷阱,如可变默认参数 |
| SIM | flake8-simplify | 代码简化建议,如判断条件冗余 |
| C4 | flake8-comprehensions | 列表推导式相关建议 |
| D | pydocstyle | 文档字符串规范 |
| ANN | flake8-annotations | 类型标注检查 |
| RUF | Ruff专属 | Ruff特有的规则,如无效的noqa注释 |
| PL | pylint | 移植自pylint的部分检查 |
如果不想一个个去查每条规则的含义,可以在项目里运行:
ruff rule F401Ruff会把这条规则的详细说明、示例代码和修复方式打印出来,比在线查文档方便很多。
3.4 按文件区分规则的per-file-ignores
实际项目中,经常需要针对不同目录使用不同的规则标准。比如__init__.py里常见的F401未使用导入,在常规文件里应该警告,但在__init__.py里可能只是为了重新导出模块,可以单独豁免。
Ruff用per-file-ignores解决这个问题:
[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "tests/*" = ["D", "ANN"]我一般在迁移老项目时,会用per-file-ignores先豁免掉所有历史遗留问题比较集中的目录,只在新代码目录上执行完整规则检查。等存量问题逐步清理干净,再把豁免范围慢慢缩窄,这种渐进式迁移方式对团队非常友好。
3.5 formatter的额外配置
Ruff的check和format共用同一套[tool.ruff]配置,但各自也有独立配置段。格式化器常用配置如下:
[tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false docstring-code-format = truequote-style控制字符串的引号风格,默认双引号。indent-style控制缩进是用空格还是制表符。docstring-code-format会格式化文档字符串里的示例代码块,这个功能我一开始没注意到,后来发现它能把docstring里凌乱的代码示范也统一格式,非常贴心。
提示:
skip-magic-trailing-comma这个参数值得特别说一句。Python的格式化器遇到列表或函数参数末尾有逗号时,通常会把这个结构展开成多行。如果你希望保留手工换行的方式,就把这个参数设为true。
4. 实操过程与性能体验
4.1 在中等规模代码库上迁移的真实体验
我在某跨平台系统上做了一次完整迁移,代码库大约有600多个Python文件,总行数在20万上下。原来的Flake8跑完全量检查需要接近40秒,isort单独跑需要15秒,Black格式化需要25秒左右。换用Ruff后,ruff check全量检查耗时大约2.8秒,ruff format全量格式化耗时大约3.5秒。这个差距在本地开发时感知非常明显,保存文件后的即时检查几乎感觉不到延迟。
如果只是检查单个文件,Ruff的反应速度基本是毫秒级。我在VS Code里打开任意Python文件,保存后底部立刻就能看到检查结果弹出来,而不是像以前那样还要等个一两秒。
4.2 缓存与增量检查
Ruff默认会做缓存,第二次检查相同文件时会直接从缓存中读取结果,速度更快。缓存默认存在~/.cache/ruff目录。如果你改了某些无关紧要的配置文件,想强制重新检查,可以用:
ruff check . --no-cache在CI环境中,我建议不要开启--no-cache,因为CI会创建临时目录,有了缓存反而能加快重复执行速度。但要注意,如果CI的环境每次都是全新的,缓存反而没有意义。这种情况可以在CI里把Ruff的缓存目录指向固定路径,让同一台机器上的多次构建共享缓存。
4.3 用pyproject.toml做统一配置管理
在推进团队工程化时,工具配置统一是一件大事。原来项目里Flake8配置写在.flake8,Black配置写在pyproject.toml,isort配置写在setup.cfg,三个位置跨越了三种格式。换到Ruff之后,全部内容收敛到一个文件里:
[tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = [ "E", "F", "W", "I", "N", "UP", "B", "SIM", "C4", "D", "ANN", ] ignore = [ "E501", ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401", "D100"] "tests/*" = ["D", "ANN"] "migrations/*" = ["E402", "F401", "F811"] [tool.ruff.format] quote-style = "double"一个配置文件同时管住了代码风格检查和格式化行为,新人加入团队后只需要看这一个文件就能理解全部规则约定。
4.4 与pre-commit集成
团队使用pre-commit管理git提交钩子时,Ruff的接入非常简洁。新建.pre-commit-config.yaml,写入:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.2 hooks: - id: ruff args: [--fix] - id: ruff-format这样每次git commit时,Ruff会先自动修复能修复的问题,再检查剩余问题;然后ruff-format会格式化所有改动的文件。这两步都通过后,代码才能进入提交流程。
我个人的建议是:在pre-commit里让Ruff只处理“本次提交涉及的改动文件”。这样不会因为存量问题阻塞正常提交,团队推行时阻力会小很多。具体做法是在args里加上--diff参数?其实不需要,pre-commit默认只会把变更过的文件传给hook,所以直接保持默认行为就行。
4.5 与编辑器(VS Code)的集成细节
VS Code里装好Ruff扩展后,在settings.json里加上:
{ "python.analysis.enable": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true } }配置完成后,保存文件时Ruff会自动格式化,并在“问题”面板中显示所有lint错误。我在实际使用中发现,把editor.codeActionsOnSave里的source.fixAll.ruff设为true还能让保存时自动修复部分问题,这个组合在大多数项目里都能明显减少手动修改量:
"editor.codeActionsOnSave": { "source.fixAll.ruff": true }不过我建议把formatOnSave和source.fixAll.ruff都打开后,花一天时间观察一下改动幅度。有些人可能不习惯自动修改代码,如果觉得改动太多,可以先只开检查,不开自动修复。
5. 常见问题与排查技巧
5.1 迁移后报错数量过多怎么办
几乎所有人第一次在存量项目上跑Ruff,都会面对刷屏级别的错误输出。这时候不要慌,也不要试图一次性修完。我的做法是分三步走:
首先用select = ["F", "E", "I"]只启用最核心的规则集,先清掉未使用导入、明显语法级风格问题和import排序问题。这三类问题大多是机械性的,Ruff的自动修复能很好地处理。
接着把UP、B、SIM这些规则加上,这轮会涉及一些重构操作,比如简化循环、删除冗余分支。先跑--fix自动修复,剩余的手动处理。
最后再把D(文档字符串)、ANN(类型标注)这类侵入性强、需要写大量新代码的规则开启。这两类规则不建议在旧代码库上强制推行,否则容易引起团队情绪反弹。我在某公司的项目里,就是先让全量代码通过核心规则,然后把文档相关规则限定在新代码文件上,靠per-file-ignores逐渐收口。
5.2ruff check和ruff format的执行顺序
我见过不少团队在pre-commit里同时添加ruff和ruff-format,但执行顺序反了,导致either格式化或检查反复失败。正确的顺序是:先做格式化,再做lint检查。因为格式化会调整代码布局,有些lint规则(如行长度、行尾空字符)在格式化前和格式化后的结果不同。如果先检查后格式化,格式化结束后可能需要再跑一次检查才能确认。
实际在pre-commit的hook顺序上,把ruff-format放在ruff之后即可,或者反过来也基本能过。但为了保证一致体验,我在脚本里统一采用先ruff format再ruff check的顺序。
5.3 格式化后代码被大量改动,慌了怎么办
ruff format一跑,几百个文件同时发生变化,这种情况几乎每个初次使用者都会遇到。但请相信,Black风格的格式化策略是非常确定的:同样的输入永远产生同样的输出。这意味着格式化的改动是一次性的,后续只要人们不再手动写出风格迥异的代码,Ruff格式化的差异就不会重复出现。
我在实际项目中看到,有些同事因为看到大量代码变动而感到不安,我劝他们做这件事:用git blame检查改动行,逐行对比会发现大部分改动只是引号、缩进和换行位置的变化,业务逻辑完全没有受影响。真正需要担心的反而是那些“格式化后语义变化”的情节,但Ruff的格式化器在设计上非常保守,不会触发这类问题。
5.4 规则冲突的处理:以E203和W503为例
有些规则本身是互相矛盾的。最典型的例子是E203(冒号前不应有空格)和W503(折行运算符应位于行首)。Black格式化后的代码有时会在切片操作的冒号前加空格,从而触发E203;同时Black的行尾运算符又将折行运算符放在行首,触发W503。黑格式自己的文档里就明确建议禁用E203和W503。
在Ruff里,绝大多数上游规则集合都默认忽略了E203,但如果你手动用select = ["ALL"]把所有规则打开,这些冲突就会回来咬你一口。我通常在完整开启规则时,会系统性地忽略掉这些与格式化器默认行为冲突的规则。
5.5 Ruff的行为与Black存在哪些差异
虽然Ruff官网宣称ruff format在设计上兼容Black,但两者在一些边缘场景上确实存在差异。我遇到过几个例子:
- Black对某些嵌套数据结构的分行处理更激进,Ruff相对保守。
- 对于
yield语句里的括号处理,两者换行逻辑不完全一致。 - 对于逗号结尾的列表,两者在“是否展开成多行”的判断上存在少量细节差异。
如果你从Black切换到Ruff,建议在新的格式化器上先跑一遍全量格式,确认改动范围可控后再提交。如果团队对格式化风格有特别强的一致性要求,可以在Ruff文档的格式差异列表里逐项确认。
5.6 让Ruff检查import类型与范围
最后分享一个我经常被问到的技巧。有些项目禁用了某些标准库模块或第三方库,想在代码里强制不出现这些import。Ruff没有直接提供“禁用特定模块”的规则,但可以用flake8-tidy-imports的规则来检查相对导入,或用RUF里的一些规则针对性检查导入方式。
如果要检查“禁止导入某个模块”,可以用per-file-ignores反着思考:在被禁止的模块路径上触发规则,然后单独忽略某些例外文件。实际操作上我更推荐用自定义规则脚本配合Ruff来做这件事,Ruff只负责风格与正确性,禁止导入这种语义性检查最好还是交给人来设计规则。
6. 一些值得保留的Ruff CLI技巧
6.1 只显示错误,忽略警告
运行检查时,如果提示信息太多,可以加--quiet参数只显示检查结果不显示额外日志。如果只想关注错误级问题,可以用--output-format concise或者配合--statistics统计错误分布:
ruff check . --statistics输出结果会按照规则代码分组统计出现次数,这样你一眼就能看出哪些规则被违反得最多,据此确定修复优先级。我在老项目迁移时,第一次跑--statistics看到某个规则出现几百次,就能提前和同事沟通好,这类问题统一按批量自动修复处理。
6.2 在CI中只检查新增的改动行
如果你希望Ruff在PR检查中只检查新增代码,可以使用--diff相关流程。常规做法是让Ruff从git diff中提取新增的行,针对这些内容做检查。例如:
ruff check $(git diff --name-only --diff-filter=AM -- '*.py')这个方案只检查本次修改涉及的文件,比全量扫描要快,也让CI更聚焦于当前提交的质量。但要注意,如果新增的代码依赖某个之前定义但未使用的import,检查结果可能会有关于“重新定义未使用名称”的误报。这种情况在日常开发中并不常见,我在遇到时通常会微调规则或增加局部豁免。
6.3 利用ruff rule和ruff linter查询规则
Ruff内置了两个命令帮助用户理解规则:
ruff linter这条命令会列出所有可用的规则集。想了解某条规则的具体行为,用:
ruff rule B006输出会包含该规则的描述、错误示例和修复后的示例代码。这两个命令组合使用,基本不需要频繁翻网络文档。
7. 计量检查与团队推广的一点心得
前面讲的都是技术细节,最后聊一点非技术方面的经验。工具再强,如果团队不买账,推行阻力会非常大。我在某跨平台系统里推广Ruff时,专门做过一个简单的操作:先在一个相对独立的子项目里跑通Ruff,展示从Flake8+Black切换到Ruff后本地检查时间从一分钟缩短到几秒的实测对比,再让团队成员亲自体验保存文件即反馈的编辑器集成效果。实际反馈普遍是“确实回不去了”。
另外要注意的是,切换格式化工具这件事,最好不要和代码重构、bug修复混在一个PR里提交。Ruff的大规模格式化是一次性动作,单独提交一次大改动,后续再走增量提交流程会顺畅很多。我见过混着提交的惨痛教训,review的人根本分不清哪一行是业务改动哪一行是格式化改动,最后不得不用二分法重新拆提交,浪费了大量时间。
还有一个小经验:Ruff默认的88字符行宽对大多数项目来说已经足够,但如果你所在的团队长期使用100或120字符的行宽,在配置文件里显式声明line-length比默认值更合理。不同项目没必要强行统一到一个数值,我在个人项目和团队项目里就分别用了88和100。行宽不只是一个风格参数,它直接影响代码的布局、表达空间和review时的换行习惯,因此值得在团队内部认真讨论一次并记录到贡献指南里。
从我自己的体验来看,Ruff最大的价值是它省掉了Python工具链中大量无意义的等待时间,把代码检查从“跑一次就烦一次”的负担,变成一种实时反馈的流畅体验。如果你目前还在用Flake8加Black加isort的组合,或者正在为CI里不断增长的lint时间发愁,我建议直接在当前项目里装上Ruff,跑一次对比实验。用数据说话,比我在这里写一万字都管用。