如果你也和我一样,接手过那种“半年没人动”的Python项目,大概率有类似的体验:代码风格混乱、print残留、import失效。近几年我的标准动作,是先给项目接入代码检查与格式化工具Ruff。它同时覆盖静态检查、自动修复、统一格式化,能替代过去Flake8、Black、isort、pyupgrade等一堆工具的组合。无论你是刚接触Python的新手,还是长期维护老项目的工程师,接下来这份使用经验,应该能帮你少走不少弯路。
很多朋友第一次看Ruff,都会问一句话:它和Flake8、Black有什么区别?我的回答是:Ruff不是又造了一个轮子,而是把这些轮子焊到了一辆车上。你用ruff check做静态检查,用ruff format做格式化,并且大部分常见检查规则已经被内置,不再需要为了一个插件提醒去折腾依赖和版本。换句话说,它更适合作为Python工程化的统一入口。
1. 为什么我把 Flake8、Black、isort 一起换成了 Ruff
1.1 一个工具包办检查与格式化两件事
先讲清楚背景。传统Python项目的标配大概是这样的:Flake8做语法和风格检查,再装一堆插件补充规则,比如flake8-bugbear查潜在bug、pyupgrade提示新语法、bandit做安全扫描;isort整理导入顺序;Black统一格式。这些工具单独拎出来都不差,但组合在一起很折磨人——每个工具一套配置,插件版本要互相迁就,isort和Black在某些魔法逗号、括号换行的处理上还需要专门对齐,否则格式化后的导入块会被另一个工具再改回去。
我现在的做法是只保留一个Ruff。ruff check负责静态检查和大部分原来靠Flake8插件才能实现的规则,ruff format负责代码格式化,项目里的pyproject.toml只多一个[tool.ruff]配置块。工具链变少之后,成员的学习成本、升级成本、出问题时的排查成本都同步降下来了。这不是为了追求极简而极简,而是少一个工具就少一个出问题的位置。
1.2 性能不是“快一点”,是“快一个数量级”
Ruff让我最直观感受到差异的,是执行速度。它用Rust编写,命令启动时间很短,文件扫描又是并行加增量缓存。我之前在约五万行的Python项目上做过对比:Flake8带着若干插件首次跑要六七秒,Ruff首次跑基本在一秒上下,常规改动后再跑通常只有零点几秒。这个差异在编辑器里更明显,保存文件时不再有工具带来的卡顿感,输入过程中就能看到红线出现,这才是“检查工具应该有的反馈速度”。
速度带来的不只是个人体感。在CI里,每次PR都要跑lint和format检查,速度越快,反馈越早;在pre-commit里,hook执行时间直接决定开发者会不会嫌烦而跳过检查。我一直觉得,工具被团队嫌弃,很多时候不是规则不对,而是“太慢”——当一次检查要磨蹭好几秒,人就会下意识地关掉它。Ruff在这方面的体验确实属于用了就回不去的那种。
1.3 不只是第二个linter,而是一套规则生态
另一个让我换过去的原因,是Ruff把社区里重要的插件规则都内置了。以前想多检查一点东西,需要在开发依赖里加flake8-bugbear、flake8-comprehensions、flake8-simplify、pep8-naming、flake8-bandit这些插件,然后祈祷它们别和主工具产生版本冲突。Ruff的做法是把这些规则按前缀分类,一个开关就能启用,本地、CI、新同事的环境表现完全一致。
换句话说,Ruff解决的不仅是单条规则问题,更多是“规则分发”问题。以前规则散落在一堆插件里,每个项目组合不一样;现在一份pyproject.toml就能把整个团队的检查标准固化下来。这也是后来我在团队落地时阻力很小的原因——不需要让每个人装相同的插件集合,只需要共享一个配置文件。
2. 从安装到跑通:配置文件与第一条命令
2.1 安装方式:pip、uv、conda还是brew
安装Ruff没有太多门道。最直接的做法是在虚拟环境里执行pip install ruff,装完就能用ruff命令。如果你已经在用uv管理环境,可以uv add --dev ruff把Ruff作为开发依赖写进项目,或者uv tool install ruff装成全局工具。conda用户执行conda install -c conda-forge ruff,macOS上也可以brew install ruff。不同安装方式的规则行为完全一样,差别只在依赖管理的归属,所以不用太纠结。
我个人的建议是:项目级开发依赖用pip或uv,CI里锁一个明确的版本;不要把Ruff装成全局工具后依赖全局环境,否则换电脑、换CI镜像都可能因为版本差异出现不同结果。Ruff本身做得很干净,二进制分发几乎没有额外依赖,被打进部署镜像也不会撑大体积,这一点在CI里很有价值。
2.2 在pyproject.toml里写下最小配置
Ruff的配置推荐放在pyproject.toml的[tool.ruff]下,这也是目前Python社区最主流的配置入口。一个能跑起来的最小配置大概是这样的:
[tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I"]line-length是团队的行宽约束,我用100而不是Black默认的88,因为现代显示器很宽,100能减少不少无意义换行,但完全看你团队习惯。target-version告诉Ruff“代码要兼容哪个Python版本”,这会影响它敢不敢推荐某些新语法替换,比如对py37的项目,它不会强制把老写法改成更新但需要高版本解释器的等价形式。select是规则开关列表,这里选了E(风格错误)、F(pyflakes实际错误)和I(导入排序),后面会再展开。这个配置已经足够让一个新项目跑起来。
如果不想用pyproject.toml,也可以用单独的ruff.toml,但既然现在绝大多数Python项目都有pyproject.toml,合在一起维护更省事。配置写好后,在项目根目录跑ruff check .,Ruff会读取配置并开始检查。看到输出里没有任何报错,就说明这个项目的底线已经被接住了。
2.3 第一批命令:check、format、rule
第一次接触Ruff,不需要把命令手册背下来,掌握下面四个命令就足够应付绝大多数日常场景,其他命令都是在这些基础上加参数。
ruff check .:检查当前目录下所有Python文件,输出违规位置和规则编号。ruff check . --fix:自动应用安全的修复,例如删除未使用的导入。ruff format .:统一格式化整个项目。ruff rule E501:查看某条规则的详细文档和示例,比翻网页查更快。
其中ruff rule是个很容易被忽略的好功能。看到一条报错,不用去搜索引擎,直接在终端里查看这条规则的说明、为什么建议这样改、以及自动修复是否安全。对初学者来说,这是理解规则成本最低的一条路。另外可以试试ruff linter浏览所有可用规则,再用ruff check . --statistics按规则统计问题数量。这些命令不需要刻意记,用到的时候ruff --help就能看到,但前几分钟把它们跑一遍,会直观感受到工具的能力边界。
2.4 编辑器联动:VS Code与PyCharm
命令行只解决了“能检查”,开发体验的大头在编辑器。VS Code用户建议直接装扩展charliermarsh.ruff,然后在settings里把默认格式化器指到Ruff,顺手开启保存时格式化:
{ "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnSave": true }这样保存文件时会自动执行Ruff的格式化,lint提示会在编辑过程中实时出现,未使用的导入会显示灰色或淡色,按下保存可能就会被自动清理。PyCharm用户去插件市场搜索Ruff并安装官方插件,同样可以把它的格式化动作挂到保存快捷键上。编辑器这层做好之后,日常开发几乎不需要主动跑命令,规则已经变成了一种环境反馈。
3. 规则前缀怎么读:三个档位的推荐规则集
3.1 规则前缀其实是一张地图
Ruff一个比较劝退新人的地方是规则太多。你要是直接打开ruff linter,会看到上千条规则,瞬间不知道从哪开始。但这些规则并不是随机编号,每个前缀代表一个规则来源或主题,看懂前缀就掌握了大半。下面是我最常用到的几个前缀,先用这张表把框架搭起来:
| 前缀 | 规则来源/主题 | 典型规则示例 |
|---|---|---|
| E/W | pycodestyle,代码风格 | E501行太长、W291行尾空白 |
| F | pyflakes,真实错误 | F401未使用导入、F821未定义名称 |
| I | isort,导入排序 | I001导入块未排序 |
| UP | pyupgrade,Python语法升级 | UP032用f-string替代格式化拼接 |
| B | flake8-bugbear,潜在错误 | B006可变对象作为默认参数 |
| S | bandit,安全问题 | S101使用assert |
| SIM | flake8-simplify,简化代码 | SIM108简化条件表达式 |
| ANN | 类型注解规则 | ANN001缺参数类型注解 |
| RUF | Ruff自定义规则 | RUF100存在无效noqa注释 |
这个表只列了我常用的几个前缀,完整分类需要查官方文档。但读到F401时,只要知道“这是pyflakes的未使用导入”,定位问题的速度就会完全不一样。具体编号不用背,关键时候用ruff rule查一下即可。
3.2 三个档位的规则集配置
基于我的经验,可以把规则集分成三档来配置。第一档是最小可用档:["E", "F", "I"]。E管风格,F抓真实错误,I让导入排序不乱。这个组合噪音很少,适合给一个完全没做过检查的老项目打底,也适合Ruff新手第一次体验,不会一上来就报几百条让人绝望的问题。
第二档是团队默认档:["E", "F", "W", "I", "UP", "B", "S"],必要时补上RUF100用于清理无效noqa。这一档会多出语法升级、潜在bug和安全提醒,属于日常开发比较有价值的覆盖范围。注意S里包含“禁止直接使用assert”这类安全规则,对业务项目有意义,但测试代码通常要按目录豁免,否则会非常吵。第三档是全量档:直接用["ALL"]开启所有内置规则。
全量档看起来很酷,但我不建议新建项目第一版就这么选。全量规则里有很多是审美偏好甚至互相矛盾的取向,比如docstring强制要求、命名风格要求,会逼你在代码里写大量解释性noqa。全量规则更适合对Ruff已经很熟悉、愿意花时间做规则精简的团队。我自己现在的主力配置接近第二档,额外加了SIM和一些RUF专属规则,这些都是长期跑下来的体感取舍。配置没有标准答案,关键是团队能达成共识并执行。
3.3 别把规则数量当成KPI
这里想多说一句容易踩的坑:规则开得多,不等于代码质量高。我以前也犯过“反正Ruff支持ALL,那就全开”的毛病,结果代码库里充满了# noqa,而且很多noqa是为了压掉与业务无关的规则写的,真正的错误反而淹没在噪音里。后来我给自己定的取舍标准很简单:一条规则如果能在PR评审阶段提前暴露真实问题,就值得开;如果它只是让代码看起来更“规整”,但对可读性和正确性没有明显帮助,就要慎重。
比如ANN要求每个函数写类型注解,在小团队里如果没人真正校验注解内容,开它只会制造一堆假装有类型的代码。规则应该服务于明确目标,而不是用来表演自律。当你觉得某条规则在当前项目里是负担,正确做法是把它从规则集拿掉,或者用豁免手段控制范围,而不是硬撑着开下去。
3.4 preview规则怎么做选择
Ruff的版本迭代非常快,新规则会先进preview模式,然后随着版本更新逐渐稳定。你可以在配置里打开preview = true提前体验,但我不推荐在团队CI里开,因为Ruff一旦升级,新的preview规则可能直接在旧代码上爆出大量问题,让CI突然变红。我的做法是:本地开发开preview,看新增规则有没有价值;团队配置文件保持preview = false,等规则转正并跑过全量检查后,再决定要不要纳入select。
升级Ruff前,先在分支上跑一遍ruff check . --statistics,对比新增规则带来的报错量,心里有数再合入主干。这样既不会被版本绑架,也能持续吃到新能力。
4. --fix 和 ruff format 的边界与配合
4.1 --fix的自动修复分安全与不安全
Ruff的自动修复并不是无脑把所有问题都改掉。它把修复动作分成“安全”和“不安全”两类,执行ruff check --fix时只应用安全修复,例如删除确认无用的导入、把可读性更好的写法替换成推荐写法。这些修复不会改变程序执行结果,合入PR时不需要太多心理负担。而不安全修复需要显式加--unsafe-fixes才会执行,例如某些会改变代码语义的重构或转换。实际使用中,我建议本地清理时可以用ruff check . --fix --unsafe-fixes快速生成一批改动,然后仔细过一遍git diff;但CI里不要加--unsafe-fixes,甚至可以考虑只跑ruff check .不做自动修复,让所有代码变更都经过review。
很多团队在CI里自动改代码,结果就是一个PR里混入了大量没人仔细看过的变更,这其实是在制造新的技术债。修复动作本身没问题,问题是它必须发生在“改动还能被开发者看到并确认”的阶段——比如本地开发,而不是发生在合并门禁之后的无人区。
4.2 ruff format与Black的微妙关系
Ruff官方对ruff format的定位是“与Black兼容但保持独立的发展路线”,这也是很多Black用户最关心的问题。绝大多数代码在两种格式化器下的输出是相同的,少数场景比如长表达式括号内的换行策略、magic trailing comma的处理会有差异。所谓魔术尾逗号,指的是在最后一个元素后面保留逗号来强制多行展开,Black和Ruff对这种细节的处理并不总是一致。
我的建议是:新项目直接选ruff format,老项目如果已经全量使用Black,不要在同一仓库里让两个工具打架。我见过有人为了“保险”同时装Black和Ruff扩展,结果VS Code保存时两个格式化器轮流改文件,git diff来回抖动,体验非常痛苦。选定一个工具后,格式化标准就是那一个,别贪多。ruff format --check .则用于CI判断代码是否已经格式化,用法和black --check一致。
4.3 典型工作流:先format再check
在具体操作顺序上,我习惯的流程是:
ruff format . ruff check . --fix先格式化,再跑lint和自动修复。因为格式化可能改变行的长度和结构,影响E501这类行宽规则;格式化之后再fix,报错位置更准确,也避免修完的代码又被格式化改乱。如果项目没有历史包袱,可以在pre-commit钩子里同时挂ruff和ruff-format,提交前两条都会跑。
这个顺序看似简单,但很多人会反过来,先check修了一堆,再format,结果E501之类又冒出来,还要再跑一次check。虽然多跑一次也不是大事,但养成顺序习惯后,整个流程会顺很多。后续如果发现ruff format又改变了一些文件,再跑一遍ruff check也是正常的,先前的步骤已经把绝大部分问题清理掉了。
4.4 在pre-commit和CI里固化
把Ruff放进版本控制钩子,是让检查真正落地的关键。用Ruff官方维护的pre-commit仓库,配置大概是这样的:
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.9 hooks: - id: ruff args: [--fix] - id: ruff-formatpre-commit里挂--fix,意味着本机提交时自动清理可安全修复的问题,但如果你不重新git add改过的文件,提交会失败,这是设计好的提醒。CI里我一般跑得更严格:ruff check . --output-format=github,这样在GitHub Actions的PR页面上可以直接按行看到问题;另跑ruff format --check .确保全仓库格式统一。这些配置都可以直接抄,真正需要注意的是版本锁定,避免pre-commit和CI中的Ruff版本漂移。
5. 老项目迁移实录:5万行代码和400个报错的处理过程
5.1 别幻想一步到位,分阶段开规则
讲一个真实的迁移案例。我处理过一个内部数据处理系统,代码量约五万行,过去几乎没有lint和format约束,风格非常自由。如果一开始就把规则开满,ruff check .的报错会直接上千条,这个数字足以让任何团队放弃工具。我的做法是分阶段推进,而不是幻想一个晚上全部搞定。
第一阶段只开["F", "E4", "E7", "E9"],这几个规则的共同点是“看起来像真实问题”:未定义名称、未使用导入、未使用变量、语法层面的冗余。第一次检查大约报了400多条,其中大部分通过--fix自动清理,剩下一百来条手工处理也很快。第二阶段再开I和UP,让导入排序和语法升级在一次独立PR里完成;第三阶段才把B、S等规则引入,并用per-file-ignores排除测试目录。关键是让每一阶段的diff可控,一个几千行的PR没法被认真review,但一次几百行、主题明确的“清理未使用导入”PR,每个开发都能看明白。
5.2 noqa和per-file-ignores的正确用法
迁移过程中一定会遇到“这条规则我知道,但这段代码就是需要这么写”的情况。行内豁免用# noqa,但如果只写# noqa而不写规则号,等于把这一行的所有检查都屏蔽了,非常危险。正确写法是# noqa: F401,多个规则就用逗号分隔,比如# noqa: F401, F841。这一个习惯,能帮你在未来省掉很多莫名其妙的问题。
目录级豁免用per-file-ignores,我常用的写法是:
[tool.ruff.lint.per-file-ignores] "tests/**" = ["S101", "ANN"]这里把测试代码里的assert和注解要求豁免掉,因为测试里assert是核心语法而非安全隐患。需要提醒的是,豁免必须是例外,而不是逃生通道。如果某个目录被加进per-file-ignores后规则数量大幅下降,要问问自己是不是在用豁免掩盖问题。另外建议在select里加入RUF100,它能识别出那些已经不再触发的noqa注释,帮你在重构后清扫垃圾注释。
5.3 格式化全库时,先处理“git diff海啸”
从一个没有格式化的项目切换到Ruff,ruff format .会把几乎每个文件都改一遍。这种情况本身不是大事,但如果你把格式化diff和业务需求混在同一个pull request里,同事review时会在数千行空白、换行变化里挣扎,最后要么草草通过,要么在评论区吵起来。我通常建议先单独提交一个“chore: apply ruff format”的PR,仓库里所有人把分支rebase到这个提交之上,再继续做业务开发。
合并之后的分支冲突会集中出现一次,但只要挺过这轮,后续每个文件的diff都会干净很多。团队里的沟通比工具本身更重要——提前告诉所有人“马上会有一个全仓格式化提交”,比合并后让大家自己发现冲突要体面得多。格式化导致的冲突大多是机械性冲突,解决起来不算难,真正怕的是大家都没有心理预期。
5.4 Ruff和mypy到底怎么分工
老项目在迁移Ruff时经常遇到的问题还有:Ruff都能提示类型注解了,还要mypy干什么?我的理解是,Ruff的检查是基于源码结构和AST的“表面层”,它能看到“这里缺注解”“这里导入没用”“这里默认参数是可变量”,但看不到“这里传入的类型和函数定义不一致”。mypy是专门做类型推导的,两者的目标完全不同。
所以我的工作流通常是:先ruff format统一格式,再ruff check处理规则问题,最后用mypy做类型校验。在pre-commit里把mypy放在Ruff之后执行,可以让报错分层:Ruff负责机械性问题,mypy负责类型一致性问题,互不干扰。如果项目还没有mypy,不必为了配合Ruff硬上,先把lint和format管住,收益已经很大,类型检查可以等团队准备好再说。
6. 把Ruff变成团队习惯:我建议的协作配置
6.1 用一份项目配置统一所有人
工具落地最大的阻力不是工具本身,而是“每个人本地环境不一样”。我见过一个团队,有人用Black,有人用autopep8,有人装Flake8加一堆插件,最后的检查结果完全取决于谁最后改了文件。用Ruff后,最简单的办法就是让项目的pyproject.toml成为唯一事实来源,所有人在项目根目录跑命令,VS Code和PyCharm会自动读取这份配置。
配置里尽量少依赖全局设置,行宽、规则选择、忽略项都写在[tool.ruff],新成员克隆仓库后不需要再安装额外插件,也不需要手工配置。如果发现编辑器没有自动使用项目配置,先检查一下VS Code或PyCharm是否以项目根目录打开,这通常比在个人设置里复制配置更靠谱。把配置固化进仓库,团队才不至于出现“我本地是绿的,CI是红的”这种尴尬。
6.2 CI只做检查,不做自动修复
我在前面提到过,CI里不要用--unsafe-fixes,这里想进一步明确:即使是安全修复,我也不建议CI自动改完直接产出。本地pre-commit可以修复,因为修复结果还在你的工作区,会被你看到并纳入提交;CI里的自动改代码,则容易变成“无人review的变更”,违背了CI作为门禁的初衷。
更好的做法是CI只跑ruff check .和ruff format --check .,发现问题就让CI失败,开发者回到本地跑--fix。把“检查”和“修改”分开,能保证每一次代码变更都被显式提交和review,团队也更容易对质量指标达成共识。如果你确实需要自动化修复,也应该让CI生成补丁,由开发者在本地确认后重新提交,而不是直接推到分支上。
6.3 用统计结果反向调整规则
Ruff提供了一个很适合定期使用的统计命令:
ruff check . --statistics它会按规则统计当前代码库里有多少处违规。我每隔一两个月会在主力项目上跑一次,不是为了给谁打绩效,而是观察哪些规则在反复触发。如果某条规则在代码库里出现几百次且都不像真bug,说明它对团队来说就是噪音,应该考虑从select里移除;反过来,如果某条规则每次触发都指向真实问题,那它值得保留甚至加大力度。规则配置不是一次定死,而是跟着项目状态持续迭代的。
为了让统计结果更有意义,我会在迁移初期记录一次基线,比如接受某些规则在一个模块内的历史欠账,然后逐步清理。当统计数字从几百降到个位数时,工具才算真正融入了项目,而不是悬在代码上方的抽象标准。
6.4 版本升级:锁版本不等于不升级
Ruff更新频率很高,新规则、规则编号调整都可能影响CI结果。我的升级策略是:pre-commit锁rev,CI锁安装版本,保证大多数人不被版本漂移骚扰;然后每隔一段时间手动升级,具体周期看项目活跃度。升级前先在分支上跑ruff check . --statistics和ruff format --check .,心里有数后再合入主干,通常比跟着最新版本无脑升级稳妥得多。
如果升级后冒出一批新报错,不要急着大改,先看新增规则属于哪个前缀,判断是不是值得为它付出一次全仓修改的代价。不值得就用ignore暂时压掉,值得就当作一次独立的清理PR。Ruff的规则体系还在快速成长中,保持更新能吃到新能力,但没必要被版本绑架,更没有理由因为升级太频繁就放弃一个明显提高效率的工具。
回到开头那个“半年没人动”的项目。我接手后做的第一件事不是重构业务模块,而是花两个晚上把Ruff配置好,全库格式化,把未使用导入和明显的问题清了个遍。之后改需求的速度明显不一样了,读代码时不会再被参差不齐的格式和随处出现的print干扰,大脑能更专注在逻辑本身。如果你也想给项目做一次“体检”,我的建议是从最小规则集开始,先跑一遍ruff check . --fix,体会一下机器替你收拾屋子的效率,再决定要不要把更多规则纳入日常。工具本身不产出代码质量,真正的价值是你愿意把代码保持在一种“拿起来就能读”的状态,而Ruff只是让这件事变得不那么痛苦。