Ruff 把默认规则加到 413 条,这是冲着 AI 写的代码来的

小岛AI 2026 / 07 / 26

59 条,413 条。

这是 Ruff v0.16.0 默认启用的 lint 规则数量,改版前和改版后。七倍。中间没有过渡版本,没有为期半年的 deprecation warning,一个 minor 版本号,直接跳过去了。

而且这是 v0.1.0 之后 Ruff 第一次动默认规则集。中间这么多个版本,规则总数从 708 条长到了 968 条,默认开着的一直是那 59 条。等于说这个工具九成的能力一直躺在那儿,你不主动去 select,它就装作不存在。

Ruff 默认启用规则数从 59 条跳到 413 条,而规则总数是 968 条

我一直觉得,工具的默认值是个被严重低估的东西。

你想想看,一个团队新起一个 Python 项目,pyproject.toml 里关于 lint 到底会写什么。我的感受是,绝大多数情况下就是一行 [tool.ruff],后面什么都不填,或者干脆连这行都没有,全靠编辑器插件自动跑。真正坐下来逐条讨论「我们团队要开哪些规则」的项目,有,但是少数,而且通常是有专职平台团队的公司。

所以默认值就是事实标准。Astral 改的不是一个配置项,是全网所有没写配置的 Python 项目对「什么算问题代码」的定义。

坦率讲这才是这次发布值得聊的地方。新增了哪些规则家族,官方 changelog 里写得很清楚,B 开头的 flake8-bugbear 进来了,UP 开头的 pyupgrade 进来了,Ruff 自家的 RUF 也进来了,具体到规则的话,F401(import 了但没用)、N803(函数参数名不符合命名规范)这些以前得手动开的现在都是开箱即用。想看全量清单的话官方专门给了一页 default rules 文档

这些信息你在任何一个技术资讯号都能刷到。有意思的是后面这些。

官方在发布说明里给这次变更定性为 breaking change,理由是这么写的,很多新启用的规则捕获的是严重问题,包括语法错误和会立刻触发的运行时错误,但此前默认没开。

这句话我读了两遍。它其实在承认一件有点尴尬的事,Ruff 这些年一直有能力拦住一批会当场炸的错误,但因为默认没开,大量项目根本没享受到。不是工具不行,是默认值太保守了,保守到工具在自我阉割。

这种自我阉割其实有它的历史包袱。Ruff 早年最大的卖点是「flake8 的替代品,而且快 100 倍」,那个阶段它必须做到跟 flake8 行为一致,不然迁移成本就上去了,没人会换。59 条默认规则基本就是对齐 pycodestyle 加 pyflakes 的那点交集。等它把 isort、pyupgrade、pydocstyle、bugbear 全吃进来变成一个 968 条规则的巨物之后,还守着当年为了兼容 flake8 定下的默认值,就有点说不过去了。

默认启用规则占规则总数的比例,从 8.3% 提到 42.7%

厉害了的是,Astral 给的回退路径挺体面。你要是完全不想接受新默认值,往配置里塞四行就回到过去,

[lint]
select = ["E4", "E7", "E9", "F"]

就这。没有 feature flag,没有环境变量,没有「请等待 v0.17 我们会提供迁移工具」。这一手我是真的觉得干净。工具作者敢改默认值,同时把回退成本压到一行配置,这是有担当的做法。相比之下我见过太多项目改默认行为的时候,一边说这是为了你好,一边把回退路径埋在三层文档下面。

真正值得点进去读一读的,是 GitHub 上那个反馈 discussion

Astral 不是拍脑袋就发的,改默认值之前先开了个帖子征求意见,提案里是 436 条,最后落地 413 条。中间被砍掉的二十来条,恰好落在整件事最难的那条线上。

先说几个被要求踢出去的。

RET504,unnecessary-assign,说的是你算完一个值先赋给变量再 return,中间那步多余。有人在帖子里的反驳很实在,这种赋值虽然技术上多余,但它无害,而且在你打算后面继续改这个函数的时候,留个具名变量反而更清晰。

PERF401,manual-list-comprehension,让你把手写 for 循环攒 list 改成列表推导式。有开发者说,对业务逻辑和带新人来说,显式的 for 循环比密集的推导式更好读。这条我站他,一个嵌套两层带 if 的推导式,写的时候很爽,三个月后自己回来看要愣半天。

BLE001,blind-except,抓的是你 except Exception 这种宽泛捕获。反对理由是对接三方库的时候,或者做防御性边界的时候,宽泛捕获经常就是唯一的选择,你也不知道那个库到底会抛出什么。

然后是我看到笑了一下的那条。SIM115,管的是文件句柄没用 with 打开。听着完全正确对吧,教科书级别的正确。结果有人举了个 Django 的例子,FileResponse 这个东西会自己负责关文件,你要是老老实实套个 with,文件在响应发出去之前就被关了,反而出 bug。规则是对的,框架的约定不是这么玩的,于是它变成误报。

维护者 ntBre 在帖子里的回应我觉得是整件事的核心。他承认了一部分意见,同时强调要把一些规则归类为「风格上限制性的」而不是「严格必需的」。针对 SIM115 那个 Django 场景,他说这可能是规则作用域本身的问题,得单独查一下。

这条线就是所有 linter 默认值的死穴,正确性和风格的边界在哪。

F401 那种没人会吵,import 了不用就是垃圾,删掉没有任何人受损。但 PERF401 就不一样了,它背后是一个审美判断,推导式比 for 循环好。RET504 更是纯审美。这些东西放进默认集,就等于 Astral 借着默认值的位置,替所有不写配置的团队定了一次代码风格的调子。

所以社区吵的从来不是规则数量,是这个定调子的权力落在谁手里。

好,聊到这我得说说我真正想说的那件事了。

这一批新进默认集的规则,如果你把它们摆在一起看,会发现一个挺微妙的重合。BLE001 抓的是宽泛异常捕获,UP 系列抓的是过时写法,PERF401 抓的是手写循环代替推导式,F401 抓的是没用的 import。

这几样,恰好是大模型写 Python 时最爱产出的东西。

except Exception: pass 这种写法,模型给你兜错的时候几乎是条件反射。过时写法更明显,模型的训练数据里躺着大量 2019 年的 Stack Overflow 答案,typing.List 而不是 listos.path 而不是 pathlib% 格式化而不是 f-string,你不专门在 prompt 里叮嘱,它就按老写法给你。至于没用的 import,那是模型改代码时最经典的残留物,删掉一段逻辑,上面那行 import 忘了收。

我不是说 Astral 挑规则的时候是照着「大模型爱犯什么错」这张单子选的,我没有任何证据这么讲。我说的是时机。

在人手写代码的年代,linter 默认开 59 条还是 413 条,差别是「工具帮我省了多少 code review 的口水」。审代码的人本来就在那儿,规则没拦住的,同事在 PR 里会拦。默认值保守一点,无非是把判断权留给人。

现在人手写代码的比例在快速下降。我说的不是那种「AI 替代程序员」的爽文叙事,是很具体的日常,你开一个 Claude Code 或者 Codex,描述一下要什么,它给你二百行 diff,你扫一眼,逻辑对,测试过了,merge。这个「扫一眼」的动作,跟以前逐行读同事代码的动作,密度是不一样的。你在扫的时候不会去数它有没有滥用 except Exception,不会去查它用的是不是过时 API。

那这一层谁在把关。

linter 在把关。而且在很多项目里,它是唯一一个每次提交都必然被执行、且不会因为赶版本而被跳过的把关者。你能说服自己「这次先 merge 回头补测试」,你说服不了 CI 里那条红色的 lint。

代码源源不断上传送带,中间只剩一道筛子在拦

所以默认 59 条和默认 413 条,在今天的差别跟三年前不是一个量级。59 条的时候它只管语法错和没用的变量,413 条的时候它开始管异常处理、命名、过时写法、性能反模式。前者是拼写检查,后者才开始像个 reviewer。

我的判断是,Astral 这一手方向上是对的,而且接下来这类工具都会往同一个方向走。生成代码的成本在往零掉,审代码的人力没有跟着涨,中间那个缺口只能靠自动化的规则去填。默认值会变成这个时代最后一道不需要谁额外操心就能生效的人类共识。

说实话我也不确定这个判断能站多久。也许两年后模型自己就不犯这些错了,UP 系列变得毫无意义,linter 退回去当拼写检查。也可能反过来,规则集继续膨胀,最后长成一个没人读得完的东西,大家一律 --add-ignore 糊过去。这两条路我都能想象出来。反正我觉得,在缺口最大的这个当口把默认值抬上去,比等着看要好。

Simon Willison 在他 那条笔记 里也把这次改动记了一笔,Hacker News 那边 的讨论标题直接就写着 413 default rules up from 59。这个数字本身就是传播点,因为它太不像一次常规更新了。

回到实际操作。你要是手上有个跑了几年的 Python 项目,明天升上去大概率会被糊一脸。给几个我认为该做的动作,都是今天就能跑的。

第一件,先别急着 ruff --upgrade,把版本 pin 住。老项目在 CI 里被几千条新诊断炸醒不是什么愉快的早晨,尤其是当你正准备发版的时候。pin 完再挑个没有 deadline 的下午来处理。

第二件,跑一次 ruff check --statistics。它会按规则码给你聚合出每条规则命中多少次,你一眼就能看出这几千条里有多少是同一类问题。通常前三条规则能占掉七成,剩下的都是零星。先修那前三条,性价比最高。

第三件,别一次全开。用 select 分批往上加,一个规则家族一个 PR。B 系列单独一个 PR,UP 系列单独一个 PR。这样 review 的人能看懂你在干什么,出问题也容易 revert。一个改了八百个文件的 PR,谁都不会真读。

第四件,v0.16 顺手带了套新的抑制注释体系,ruff: ignore 管单行,ruff: file-ignore 管整个文件,还有范围式的 ruff: disableruff: enable 管一段。另外有个 --add-ignore 的 CLI flag 会自动帮你插抑制注释。这个东西在迁移期真挺有用,先把存量问题全部抑制住让 CI 变绿,再按模块慢慢清。但记住抑制是债,不是解决方案,别让 ruff: file-ignore 在你代码库里长成一片森林。

对了,v0.16 还稳定了一个我个人挺喜欢的小功能,它现在能格式化 Markdown 里 fenced code block 中的 Python 代码,pythonpypy3pyipycon 这些 info string 都认。写文档和教程的人应该会喜欢这个,以前文档里的代码块格式全靠手动对齐,不一致得很。不想被格式化的地方用 <!-- fmt: off --> 挡掉。

另外有个容易踩的小坑,JSON 输出的格式变了。filenamelocationend_location 还有 fix.edits[] 里的位置字段,现在可能返回 null,而不是以前的空字符串和 row 1 column 1。官方说影响的诊断很少,但如果你有脚本在解析 Ruff 的 JSON 输出往内部质量看板上推数据,去加个 null 判断,不然某天看板莫名少一批数据你得查半天。

写到这我想起 unix 那套老哲学里的一条,做一件事并且做好。Ruff 现在明显已经不是「做一件事」了,它是 linter,是 formatter,是 import 排序器,往边上看 Astral 还有 uv 和 ty,整个工具链在往一个方向合并。这跟 unix 那套小工具管道拼装的路子基本是反的。

但我不确定这算不算背离。unix 哲学诞生的时候,写代码的是人,人擅长把小工具拼起来。现在拼装这件事本身也在被自动化,一个能一次性给你 413 条一致判断的巨物,可能比十个需要你自己配一遍的小工具更适合当下。这个变化我还没想明白,先记下来。

好家伙,一个 linter 的默认值改动,写着写着变成了「谁在替我们审代码」这个问题。

59 到 413。数字是 Astral 改的,但真正在变的是那个更早的前提,代码是谁写的,又是谁在看。