资讯动态

从Flake8到Ruff:Python代码检查与格式化工具链的加速重构

发布时间:2026/10/21 16:07:41 来源:尧图企业网站定制
如果你的项目在经历了某个“平静的周一”之后突然被一条几百个文件的 lint 流水线卡了十分钟你可能也会像我一样开始怀疑人生。那时候我们用着一套标准的 Python 工具组合Flake8 管检查、Black 管格式化、isort 管 import 排序听起来挺完整但实际跑起来就是一个字等。直到我换成了 Ruff才发现原来这些事可以在一套工具里、用接近零延迟的速度一次全部搞定。这篇文章不是官方文档搬运是我把一套百万行代码库从传统工具链切到 Ruff 的完整复盘。我会讲清楚为什么换、怎么迁、配置怎么设计、规则怎么选、哪些坑踩了又怎么填适合正在纠结要不要迁移的团队也适合想从零开始规范 Python 代码的个人开发者。如果你只是想找一个“更快一点的 linter”那这篇同样适用。1. 老工具链的疲惫不是慢的问题是心智负担太重1.1 传统工具组合的日常Flake8、Black、isort 各管一摊先交代一下我接手项目时的状态。代码量已经膨胀到接近一百万行分布在几十个子项目里Python 版本从 3.8 到 3.11 混杂每个子项目的配置还不一样。检查环节当时是这样的Flake8 负责语法静态检查isort 负责 import 排序Black 负责格式化。这三个东西各有个的配置文件Flake8 要.flake8或setup.cfgisort 要[tool.isort]Black 要[tool.black]三份配置规则有时候还对不上。最典型的是 import 排序。isort 默认按照 stdlib、第三方、本地包分组Black 不管分组只管换行和引号风格两者在某些极端缩进场景下会互相触发对方的改动。你刚用 Black 格式化完isort 再跑又会往文件头上加注释isort 调整完顺序Black 又觉得某个 import 该拆成多行。于是我们当时的 CI 流水线里经常出现“同一批代码check 永远输出不一致”的诡异状态。Flake8 本身也不是没有毛病。它本质上是 pyflakes 加 pycodestyle 的一套“组装机”里面不少规则是上世纪就定义的代码风格。很多规则在今天的工程实践里已经过时但默认配置全开的时候照样会报。比如E501行长度限制默认 max-line-length79这在老时代是硬性标准现代项目 88、100、120 都很常见。你每次都要在配置里改一堆参数然后等几秒钟它跑完一条消息再改再跑。1.2 速度问题只是表象真正的麻烦是规则分裂当时我们的 lint 流程分两段。第一段走 CI对所有改动文件跑 Flake8第二段是本地 pre-commit跑 isort、Black、Flake8 三件套。上一代工具链在本地小改动还能忍受但一旦是全量检查Flake8 跑完整个项目要两分钟左右。在代码量小的时候两分钟可以接受当代码量涨起来加上 isort 和 Black 串行执行一次完整检查十分钟就出去了。人一多排队等你本地 pre-commit 通过的场景频繁出现。最让我崩溃的并不是慢而是规则分裂这件事本身。Flake8 的规则代码以E、W、F开头用来表示风格问题和逻辑问题isort 不跟自己归一类Black 则是“没有规则代码只有格式化”。这意味着你要记住几套代码体系还要在各种配置里反复横跳。讨论一个问题的时候团队成员可能要同时打开三个配置文件和四份文档这个心智负担早就超过了“检查代码是否规范”应有的成本。所以我当时在团队周会上提出一个想法如果有一工具能把检查、排序、格式化全部聚合配置只写一处并且用 Rust 重写、速度提升几十倍我们是不是就该换那就是 Ruff 最初打动我的地方。它不只是一个 linter它把 pycodestyle、pyflakes、isort、pyupgrade、bugbear 的规则全部重新实现了一遍并且统一用一套规则代码对外输出。2. 先装起来再说话Ruff 的安装、命令行和第一印象2.1 安装方式与版本选择如果只是试用最简单的办法是直接通过 pip 安装pip install ruff我的建议是尽量用固定版本不要用ruff0.x这种模糊约束因为它迭代非常快默认规则集每隔几个版本就会变。我实际项目里用的是某 0.9 版本之后的版本锁死在一个具体的小版本上避免同事之间环境不一致导致检查结果不同。如果你是给多个 Python 项目统一管理推荐用 pipx 安装成独立工具pipx install ruff这样它不污染当前虚拟环境又能全局复用。如果项目中已经引入了 uv也可以用uv tool install ruff原理类似。装完先确认版本ruff --version这时候你会发现它的启动输出快得离谱。我这里不是夸张Ruff 启动基本在几十毫秒级别因为它用 Rust 编译成单个二进制没有 Python 解释器加载的额外开销。2.2 两条主命令check 与 format 的日常分工Ruff 的命令行体系很干净日常用到的就两个ruff check和ruff format。ruff check承担的是传统 lint 的职责查语法错误、未定义变量、未使用导入、PEP8 风格问题、高版本语法风险等。它支持--fix自动修复一部分问题也支持--diff先预览改动。ruff format承担的是格式化职责对标 Black用来统一代码风格。我第一次跑ruff check .是在一个历史项目上输出直接刷屏因为它把几千个问题都列了出来。当时的直观感受是“哇真的快”但也意识到迁移不是直接开全量规则就能上线的得渐进式处理。所以第一步我建议你只跑默认规则ruff check .Ruff 默认只开 E4、E7、E9 和 F 这两大类规则也就是那些容易造成 bug 的语法风格和逻辑问题。这个默认策略很聪明它避免了初学者一上手就被几百条风格噪音淹没。如果你只想让某个目录参与检查ruff check src/ scripts/如果想自动修复并同时显示修复了哪些文件ruff check . --fix还有一条我日常工作里最常用的命令ruff check . --fix --diff--diff不会真正修改文件只输出将要修改的内容适合放到代码评审里给同事看改动影响。2.3 第一次运行就能看到差距我做过一个对比记录同样一个包含约 2900 个 Python 文件的旧代码库老流程 Flake8 全量检查耗时约 130 秒isort 加 Black 各跑一遍约 40 秒整个 pre-commit 串行完成大概 170 秒。换到 Ruff 之后ruff check .只需要不到两秒ruff format --check .不到一秒。这个差距不是“优化”能追上的是机制上的差异。Ruff 内部用并行任务队列扫描文件单个文件的分析也非常轻量因此在大仓库下优势会指数级放大。后面我会专门讲迁移过程中的实测数据这里只给你一个判断如果你的 lint 流程开始成为团队提交代码的瓶颈这就是你认真考虑迁移的信号。3. 配置文件的学问pyproject.toml、select、target-version3.1 配置放哪里以及为什么推荐 pyproject.tomlRuff 支持三种配置放置方式pyproject.toml里的[tool.ruff]段、根目录的ruff.toml、以及生态里常见的.ruff.toml。我的意见是只要你的项目已经在用现代 Python 项目结构就把它放进pyproject.toml。理由很直接pyproject.toml已经是事实上的项目元数据标准黑盒、isort、mypy、pytest 全都可以在这里声明配置。把 lint 配置也放进来所有工具配置集中在一个文件里新成员上手只需要看一个文件。一个最小配置大概是这样的[tool.ruff] target-version py39 line-length 88 src [src, tests] [tool.ruff.lint] select [E, F, I, UP, B]target-version很关键。它告诉 Ruff 项目最低支持的 Python 版本Ruff 会根据这个版本决定哪些语法会被标记为不兼容。比如项目声明支持 Python 3.9那代码里如果用了 3.10 才有的match语法或者X | Y类型合并写法Ruff 就会警告。line-length我一般直接沿用 Black 的 88 或 100。如果你之前用过 Black直接把line-length保持与原 Black 配置一致这样迁移格式化时影响最小。3.2 规则选择的渐进式策略Ruff 的规则体系不是一堆堆散件它把规则按“前缀”归类这个前缀和社区工具对标。我建议第一次接项目时不要急着把所有规则全开而是分阶段扩展。考虑如下渐进式策略第一阶段[E4, E7, E9, F]这是默认行为主要查逻辑缺陷。第二阶段加上E、W全面启用 PEP8 风格检查同时加I做 import 排序。第三阶段按项目需要加UPpyupgrade检查老语法、Bbugbear潜在 bug、SIM简化写法、S安全风险等。每一个阶段都应该配合ruff check . --fix自动修复一部分剩余问题人工过一遍。这样不会出现换工具第一天就被几千条告警淹没的情况。这里还要说明一下规则的“分级修复”机制。Ruff 对每一条能够自动修复的问题都有标记有的可以安全自动改比如删除未使用的 import有的需要人工确认比如改变代码语义的简化。它的--fix默认只处理安全级别为“安全”的修复不会强行把可读性改成有风险的写法。这个设计在迁移大仓库时非常友好。3.3 用 per-file-ignores 处理历史包袱大仓库迁移时你一定会碰一个场景某个老模块的代码风格极其混乱但它运行稳定没人敢大改。这时候不建议直接noqa加到每一行更优雅的方案是用per-file-ignores把特定目录的某些规则直接忽略。[tool.ruff.lint.per-file-ignores] migrations/* [E501] src/legacy/* [I, UP]这意味着新代码严格遵守全量规则历史包袱代码先保留现状后续重构时再逐步解除豁免。这种差异化处理比一刀切全量开启更符合真实工程节奏。4. 规则代码背后在管什么从 E/F 到 I、UP、B、S、SIM4.1 核心规则族一图看懂很多人第一次看 Ruff 的规则列表会觉得头大E、F、I、N、UP、B、S、SIM、T20、ANN、RUF……前缀太多。我整理了一张我团队内部通用的对照表你选规则时可以照着挑。前缀对标工具主要管什么典型问题示例E / WpycodestylePEP8 风格多行缩进错误、行尾空格、空行数量Fpyflakes逻辑与语法缺陷未使用导入、未定义名称、重复赋值Iisortimport 排序标准库/第三方/本地分组错误Npep8-naming命名规范变量名下划线风格、类名大驼峰UPpyupgrade高版本语法迁移str.format改 f-string、Decimal旧写法Bflake8-bugbear潜在 bug 模式函数默认参数可变对象、裸except:Sbandit安全风险使用不安全的eval、硬编码密钥SIMflake8-simplify简化写法能写if x:却写成if x True:T20flake8-printprint 调试残留生产代码出现print()ANNflake8-annotations类型注解函数参数缺失类型注解这张表不用背当你需要决定开哪些规则时对着它挑项目真正需要的就行。例如内部项目不接受安全审计类告警剧增就先不开 S老代码大量缺类型注解ANN 就建议先放着等后续逐步补。4.2 几个高频规则的实测案例脱离场景聊规则没意义我选三个实际项目里出现频率最高的规则类型具体说一下。第一个是F401未使用的 import。它几乎是每个项目里都能扫出一堆的问题。Ruff 对F401的自动修复很成熟会在删除 import 的同时检查是否因为 re-export、动态导入等原因需要保留。它识别__init__.py里专门用于导出的内容也不会误删。这个规则建议在任何项目里都开。第二个是I001import 块排序问题。传统 isort 会要求你按字母序排 import但很多项目实际写代码时是“顺手添加”并不在意顺序。Ruff 的--fix能自动整理整个 import 块并且不会和它的 formatter 冲突。因为 import 排序规则本质上就是“格式化代码”你不需要再单独跑 isort。第三个是UP032/UP034f-string 迁移类问题。Ruff 会识别哪些%格式字符串可以安全地转为 f-string哪些不能比如包含动态格式表达式。自动修复之后代码可读性会有明显提升。我遇到过的最典型场景是# 修复前 message Order %s is pending % order_id # 修复后 message fOrder {order_id} is pending这种改动几乎没有风险但它能极大改善代码可读性新人扫代码时反馈“以前看字符串拼接要反应一阵子现在一眼就懂”。4.3 规则之间会打架吗select 和 ignore 的优先级规则之间确实有可能出现重叠但 Ruff 对这一块处理得比较好。它的原则是select是允许集合ignore是集合内剔除per-file-ignores在ignore之后再叠加。实际用起来你可能会遇到两个规则对同一行代码给出互相矛盾的修改建议这时处理方式是看你更偏向哪种规范。比如B008函数参数默认值使用可变对象建议None哨兵和SIM系列的某些简化规则在少数极端代码里会产生冲突。Ruff 官方认为这种情况少见但我个人建议在配置里显式声明所有与格式化风格强相关、但又容易误伤的规则代码方便以后调整。我的配置习惯是ignore尽量少用来精确排除某条规则需要排除整个目录的用per-file-ignores。不要为了少看告警直接ignore [ALL]那就失去了 linter 的意义。5. 格式化不是一个简单的“自动排版”Ruff format 与 Black 的兼容关系5.1 format 解决什么问题不解决什么问题很多人把 “format” 和 “lint” 混为一谈这是迁移时最容易踩的坑。Ruff 的内部结构把它们设计成了两套独立模块ruff format负责把代码重新排版成统一风格相当于 Black 的替代品ruff check负责检查规则相当于 Flake8/其他插件的替代品。你可以只用 format 只用 check也可以两个都用但它们在 CI 里应该各跑各的。ruff format不检查“该不该有类型注解”“有没有安全风险”它只关心代码长成什么样。它解决的问题包括单行长度是否超线、括号缩进是否一致、空格是否需要调整、表达式要不要拆行。它不会改变任何运行逻辑这是格式化工具必须守住的底线。但你要注意它也不是“能把所有丑陋代码变漂亮”的魔法。比如你有一个嵌套了七八层的函数代码逻辑本身就绕格式化之后依然是嵌套七八层。format 只能让绕的代码绕得一致不能替你重构。5.2 与既有 Black 格式化代码的迁移兼容如果你现在项目里已经用 Black 格式化了很久切到ruff format的体验通常会相当平滑。因为 Ruff 的 formatter 在设计之初就是以兼容 Black 为目标的。我测试下来绝大多数现有 Black 格式化代码切换到 Ruff format 后不会产生大量 diff。但有两个地方需要留意。第一是引号风格。Black 默认把双引号字符串统一成双引号Ruff format 也遵循这个默认。如果你之前用的是单引号统一策略切换后会有大量字符串被改动建议迁移前在 CI 里跑一次ruff format --check .看看到底有多少改动再决定是否接受。第二是魔法尾逗号处理。Black 在某些场景下会根据末位逗号决定拆行Ruff format 也有类似逻辑但细节上可能有细微差异。这种差异通常出现在函数参数列表极长、最后一个参数后带逗号的场景。遇到这种输出不一致我的建议是不要去逐条 hack 特殊写法直接接受工具的决策团队统一标准。5.3 检查与格式化在 CI 里的正确姿势我们团队现在 CI 里的姿势是这样ruff check . --fix --diff ruff format --check .第一行检查 lint并展示将来自动修复的内容第二行只验证格式化状态不做实际修改。为什么不用ruff format .直接改因为 CI 里直接改动文件会污染流水线产物而且代码没有经过本地 commit直接改动会让开发者困惑。让每一个人在本地通过 pre-commit 完成格式化CI 只验证状态是最干净的工作流。这里有个很关键的经验不要在 CI 里跑ruff check . --fix然后期待它把文件修好。修复应该发生在开发者本地CI 是门禁不是施工队。6. 编辑器、pre-commit 与 CI 集成让工具真正跑起来6.1 pre-commit 钩子配置如果你的项目已经有 pre-commit那接入 Ruff 几乎是最顺的一步。在仓库根目录的.pre-commit-config.yaml里加一段repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.0 hooks: - id: ruff args: [--fix] - id: ruff-format注意两点。第一rev要和你本地装的实际版本保持一致否则会出现“本地跑不过、pre-commit 跑过”或反过来的诡异情况。第二ruff钩子带--fix会在 commit 前自动修改文件所以所有自动修复的改动仍然需要你加进暂存区。如果你还没有引入 pre-commit我依然建议加上。它的意义不仅是 CI 之前多一道防线更重要的是它把“代码风格统一”从人工评审中剥离出来让人去关注逻辑而不是争论“这里该不该折行”。6.2 编辑器集成与实时反馈Ruff 在编辑器里的体验是它让我彻底回不去旧工具链的原因之一。在 VS Code 里装对应插件后它能做到四件事实时诊断、悬停显示规则说明、保存时自动格式化、快速修复上下文菜单。这个体验接近前端 ESLint 的体验你一边写它一边告诉你哪块有问题。我习惯把编辑器配置成“保存时格式化”并设置默认格式化器为 Ruff。这样不再需要手动记快捷键写代码本身就是规范化的过程。对于不熟悉命令行的刚入门同学这几乎是一种零学习成本接入方式。有一点要注意编辑器里如果同时装了多个 Python 格式化插件一定要在设置里把默认格式化器明确指定为 Ruff否则会出现“多个格式化器抢文件”的混乱。我会建议把其他旧工具对应的插件直接禁用掉保持环境干净。6.3 CI 流水线里的两个阶段除了 pre-commitCI 阶段也需要独立的 Ruff 任务。原因是 pre-commit 只在开发者本机触发推送代码后仍需在流水线里验证一次。我建议把检查拆成两个独立 job或者至少是两个独立步骤。第一阶段跑 lintruff check .第二阶段跑格式验证ruff format --check .这两个阶段为什么要拆开因为告警类型不同排查路径也不同。lint 告警告诉你“这行代码有什么隐患”需要开发者去看规则format 告警告诉你“这行代码排版不合规”基本可以直接运行ruff format解决。混在一起会让失败日志变得冗长且难以定位。如果公司 CI 平台支持缓存建议给 Ruff 开启缓存目录缓存。Ruff 默认会在系统临时目录建缓存二次运行时如果文件没变化它直接读缓存速度会更快。在 CI 场景下这能省几秒到几十秒看仓库规模。7. 真实迁移复盘从 2900 个文件到全量零告警7.1 迁移路线与阶段划分这里分享一下我实际迁移某个大仓的准备过程。这个仓有大约 2900 个 Python 文件代码历史悠久规则欠债多。我没有选择“一步切全量”而是分了三个阶段。第一阶段是“观察期”。我把 Ruff 加入配置但select只开默认规则也就是 E4、E7、E9 和 F。这个阶段跑了两周目的是让团队先习惯 Ruff 的命令行和输出风格积累一部分自动修复经验。由于默认规则报的基本都是真实缺陷这次修复让仓库质量有了可感知的提升。第二阶段是“风格期”。我把E、W、I开起来同时启用ruff format作为唯一格式化工具。这阶段的主要工作是处理存量代码的格式不统一和 import 排序问题。因为改动量大我先跑了ruff format --check .导出一份未格式化文件清单再分批让模块负责人认领处理。第三阶段是“增强期”。我逐步加入UP、B、SIM根据团队承受能力控制每次新增的规则数量。每次优先跑--fix处理安全修复项剩余需要手改的通过per-file-ignores暂缓。大约六周后全仓库达到ruff check .零告警的状态。7.2 数据对比时间、规则数量、代码改动行数迁移前后的对比数据可以给准备迁移的人一个参考。表格里是我在自有仓库里记录的数字你按自己仓库规模等比估算就好。指标迁移前迁移后全量 lint 耗时约 150 秒约 2.3 秒格式化耗时约 40 秒约 1.1 秒涉及规则来源4 个独立工具1 个工具统一管理配置文件3 份分布在多目录1 份 pyproject.toml存量告警数数千条无法统计0按当前规则集代码改动行数—约 8% 文件被格式化触碰8% 看起来不大但放到 2900 个文件里也有两百多个文件被改动过。这个量级如果直接在主干分支操作会严重影响协同所以我强烈建议迁移改动走独立分支并尽量用批量 PR 合入。7.3 迁移过程中的“拆弹”策略迁移过程里最危险的不是规则告警本身而是“全仓一次性改动”带来的合并冲突。我这里的策略是把格式化改动和 lint 修复分开提交格式化改动主要出现在独立分支lint 修复按模块拆成多个小 PR每个 PR 只改一个子项目的相关告警。这样 reviewer 看到的信息是干净的也方便回滚。另一个拆弹策略是“白名单与黑名单结合”。团队里总有那么一两个模块是没人愿意动的我用per-file-ignores把它们排除在部分规则之外同时给这些模块开一个单独的跟踪 issue定期把豁免范围缩小。半年后再看这些“历史遗留”区域已经被蚕食得差不多了。一刀切不可取渐进式蚕食更容易落地。8. 我踩过的坑与规则禁用的艺术8.1 版本升级带来的默认规则变化Ruff 迭代很快它每隔一段时间就会把某些规则从“预览”状态转为正式默认启用。这带来一个实际问题你锁定的版本在一个月后可能因为升级而突然出现一批新的告警。我的建议是两点第一CI 环境里锁死版本号第二升级时专门跑一次ruff check .把新增告警分类能自动修复的直接修需要讨论规则的单独开 PR 讨论。我经历过一次升级后突然出现几十个T201print 语句告警就是因为新版把 print 规则从可选变成了推荐。这事本身不坏但它提醒我升级工具不能像升级普通依赖那样不看 changelog 直接发版。8.2 noqa 与批量忽略的正确姿势不管规则设计得多好总有“这行代码就是该特殊处理”的情况。Ruff 支持在行尾加# noqa: CODE来忽略单行告警也支持用# noqa忽略整行所有告警。我的原则是必须带具体规则代码不要用裸noqa。原因很简单同样的行只忽略某一条规则可读性好也会让后续维护者知道这里为什么豁免。如果存量代码已经有一堆需要忽略的地方Ruff 提供一条实用命令ruff check . --add-noqa它会把当前所有无法自动修复的告警自动加上对应noqa注释。这条命令适合迁移第三阶段冲刺时用先把存量清零之后新代码不再允许新增裸 noqa。我一般会再加一条.git-blame-ignore-revs之类的机制把批量改动从 blame 里隐藏方便后续真正重构时回溯。8.3 不是所有规则都值得开几个我最终放弃的规则这里说一点反直觉的经验。虽然 Ruff 很强大但我最终没有开启它的全部规则。有几个规则在实际项目里给我带来的困扰大于价值。第一个是ANN类型注解。全量开启它会让老代码陷入“注解地狱”新人也容易为了满足注解规则而乱写类型。我的做法是只在新增文件或核心模块中启用它历史文件暂不要求。第二个是S安全规则里的部分检查。像S324这类关于哈希算法的规则它在某些非安全敏感场景会误报很多团队每天都要处理“伪安全问题”。我会在配置里精确地排除掉这类规则保留真正有意义的S101断言误用等。第三个是SIM里的部分简化建议。有些简化建议在“可读性”角度并不是绝对正确比如建议把if x is not None改成if x这在某些语义下会让代码意图变得模糊。工具是助手不是老板规则的开关权应该掌握在团队手中。最后再分享一点我的实操体会我自己折腾完这一圈之后最大的体会是Ruff 不是简单的“平替”它的价值在于把多工具分散的心智负担收拢成一个东西让团队在代码规范上形成一套统一语义。你不需要再解释“这是 isort 的错那是 Black 的偏好Flake8 和它俩配置冲突”只需要说“Ruff 规则是什么”。如果只能给一条建议我会说不要试图在第一天把所有规则都打开先默认规则跑一周再按周递增规则族。很多项目迁移失败不是 Ruff 不够好而是迁移策略太激进。先让团队尝到“快”的甜头再把“严”逐步加上去你会发现代码质量这件事并没有想象中那么痛苦。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑