用Ruff统一Python代码检查与格式化:告别Flake8与Black组合
如果你也和我一样接手过那种“半年没人动”的Python项目大概率有类似的体验代码风格混乱、print残留、import失效。近几年我的标准动作是先给项目接入代码检查与格式化工具Ruff。它同时覆盖静态检查、自动修复、统一格式化能替代过去Flake8、Black、isort、pyupgrade等一堆工具的组合。无论你是刚接触Python的新手还是长期维护老项目的工程师接下来这份使用经验应该能帮你少走不少弯路。很多朋友第一次看Ruff都会问一句话它和Flake8、Black有什么区别我的回答是Ruff不是又造了一个轮子而是把这些轮子焊到了一辆车上。你用ruff check做静态检查用ruff format做格式化并且大部分常见检查规则已经被内置不再需要为了一个插件提醒去折腾依赖和版本。换句话说它更适合作为Python工程化的统一入口。1. 为什么我把 Flake8、Black、isort 一起换成了 Ruff1.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 ruffmacOS上也可以brew install ruff。不同安装方式的规则行为完全一样差别只在依赖管理的归属所以不用太纠结。我个人的建议是项目级开发依赖用pip或uvCI里锁一个明确的版本不要把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风格错误、Fpyflakes实际错误和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/Wpycodestyle代码风格E501行太长、W291行尾空白Fpyflakes真实错误F401未使用导入、F821未定义名称Iisort导入排序I001导入块未排序UPpyupgradePython语法升级UP032用f-string替代格式化拼接Bflake8-bugbear潜在错误B006可变对象作为默认参数Sbandit安全问题S101使用assertSIMflake8-simplify简化代码SIM108简化条件表达式ANN类型注解规则ANN001缺参数类型注解RUFRuff自定义规则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-formatgithub这样在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海啸”从一个没有格式化的项目切换到Ruffruff 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锁revCI锁安装版本保证大多数人不被版本漂移骚扰然后每隔一段时间手动升级具体周期看项目活跃度。升级前先在分支上跑ruff check . --statistics和ruff format --check .心里有数后再合入主干通常比跟着最新版本无脑升级稳妥得多。如果升级后冒出一批新报错不要急着大改先看新增规则属于哪个前缀判断是不是值得为它付出一次全仓修改的代价。不值得就用ignore暂时压掉值得就当作一次独立的清理PR。Ruff的规则体系还在快速成长中保持更新能吃到新能力但没必要被版本绑架更没有理由因为升级太频繁就放弃一个明显提高效率的工具。回到开头那个“半年没人动”的项目。我接手后做的第一件事不是重构业务模块而是花两个晚上把Ruff配置好全库格式化把未使用导入和明显的问题清了个遍。之后改需求的速度明显不一样了读代码时不会再被参差不齐的格式和随处出现的print干扰大脑能更专注在逻辑本身。如果你也想给项目做一次“体检”我的建议是从最小规则集开始先跑一遍ruff check . --fix体会一下机器替你收拾屋子的效率再决定要不要把更多规则纳入日常。工具本身不产出代码质量真正的价值是你愿意把代码保持在一种“拿起来就能读”的状态而Ruff只是让这件事变得不那么痛苦。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →