写 Python 的人大多默认一条规矩:函数签名写清楚类型,函数里面的局部变量交给 IDE 和检查器去猜。开发者 matheuzgomes 刚刚开源的 Rust 工具 Gawain(当前 pre-commit 钩子版本 v0.3.0,仓库仅 14 次提交),直接把这张默契的桌子掀了。它在 974,586 行代码上跑完一遍严格检查只要 0.36 秒,目标只有一个:把代码里所有没写类型注解的变量、类属性和 Any 全部揪出来报错。

这件事最反常的地方,在于它撕开了 Python 静态类型生态里一个存在已久的盲区。很多团队以为开了 mypy --strict、Pyright 严格模式或者配了 Ruff 的 ANN 规则,代码就已经是全量强类型了。实际上,主流工具的底线都停留在函数边界,而 Gawain 把防线硬生生推到了每一行赋值语句。

97 万行代码跑进 360 毫秒,它到底拦下了什么

先用最简练的事实把 Gawain 的工作机制讲清楚:

  • 发生了什么.Gawain 采用两阶段检查。第一阶段(presence)用 Astral 的 ruff_python_parser 解析语法树,检查变量(GW001)、参数(GW002)、返回值(GW003)、类属性(GW004)和常量(GW010 要求 Final[T],GW011 禁止裸 Final)是否写了类型注解,同时封杀注解里的 Any(GW020)。只要有一处漏写,直接拦截并跳过第二阶段;全部通过后,第二阶段(types)才调用底层检查器 ty(默认锁定 0.0.100)或 Meta 的 pyrefly(锁定 1.4.1)去校验类型对不对。
  • 为什么重要.在 AMD Ryzen 7 3700X(WSL2)上测试 CPython 3.14 标准库(1,847 个文件、974,586 行代码),取 30 次运行中位数,Gawain 开启 --no-types 跑完 strict 模式只要 0.36 秒,默认的 obvious 模式只要 0.46 秒。这个速度和只查函数签名的 ruff --select ANN(0.35 秒)几乎持平,而同样只查函数签名的 flake8-annotations(5 次运行中位数)需要 11.4 秒。
  • 谁受影响.被 dict[str, Any] 折磨的维护者、在 GitHub 网页端脱离语言服务器(LSP)审代码的架构师,以及重度使用 AI 生成代码却苦于类型偷懒的工程团队。
CPython 3.14 标准库(974,586 行代码)注解检查耗时对比 Ruff (--select ANN) 仅查参数与返回值 0.35 s Gawain (strict 模式) 查变量 / 参数 / 返回 / 属性 0.36 s Gawain (obvious 模式) 含字面量与构造豁免分析 0.46 s flake8-annotations 仅查参数与返回值 (5次中位数) 11.4 s

这里有一处容易被忽略的细节。mypy 的 --strict 模式和 Pyright 的 strict 模式,从来不要求所有局部变量写类型注解。只要检查器能顺着上下文自动推导出来,哪怕你写一行 count = count_items(),它们也完全放行;只有在类型不明或推导卡壳时才会报错。同样,Ruff 的 ANN 规则集和 flake8-annotations 也把职责严格圈定在函数参数与返回值上,局部变量根本不在它们的管辖范围内。

但在 Gawain 眼里,只要 count = count_items() 没写 : int,无论是默认的 obvious 模式还是 strict 模式,统统算违规。唯一能逃过默认 obvious 模式的,只有从未被二次赋值的字面量(如 retries = 3)、字面量容器(如 ids = [1, 2, 3])以及直接调用的构造函数(如 user = User("ada"))。哪怕是 PORT = 8080 这种字面量,只要名字是大写常量,也必须老老实实标上 Final[int]。

跑分背后的隐蔽前提与技术押注

把话说透一点,Gawain 根本不是一个新的类型检查器,它是一个横在 AST 语法检查与类型检查器之间的代码策略守门员。

很多人第一眼看到“几毫秒找出缺失类型”和 0.36 秒跑完 97 万行代码的成绩单,会以为它取代了 mypy。但细看基准测试的条件就会发现,0.36 秒和 0.46 秒的数据都是在加上 --no-types 参数的前提下测出来的。也就是说,这个时间只算了第一阶段扫描语法树找注解的开销,完全不包含调用 ty 或 pyrefly 进行真正类型推导和校验的时间。

更耐人寻味的是,为什么管得更严、要求每一处赋值都写注解的 strict 模式(0.36 秒),反而比放宽了条件的 obvious 模式(0.46 秒)快了整整 100 毫秒?

原因就在语法树的计算成本上。strict 模式极其简单粗暴,看到赋值语句左边没挂类型直接报错收工;而 obvious 模式为了体谅开发者,还得去检查等号右边到底是不是字面量、是不是构造函数,还得追踪这个变量在后文有没有被重新赋值。心软一点,机器就要多跑 100 毫秒的语义分析。

Gawain 两阶段拦截管线与 --no-types 边界 1. Python 源码 ruff_python_parser 极速生成 AST 2. 阶段一:Presence 查 GW001-GW020 注解存在性 0.36s 跑分仅测到此步为止 3. 阶段二:Types 全部有注解才放行至此 调 ty (beta) 或 pyrefly 验真伪 注:若阶段一发现漏写注解,直接返回退出码 1 并跳过阶段二;开启 --no-types 则完全剥离阶段二。

而在第二阶段的技术选型上,作者做出了一个非常激进的决定:完全甩开老牌的 mypy 和 Pyright,只绑定同样用 Rust 编写的新一代检查器 ty 和 pyrefly。甚至连配置 pre-commit 钩子从源码编译时,都硬性要求环境里装有 Rust 1.97 或更高版本。

  • 风险.Gawain 默认绑定的 ty(0.0.100)目前还处在 beta 阶段,连作者自己都在文档里警告其诊断结果随时可能随版本变动;如果团队想在生产环境尝鲜这套全量注解规范,最稳妥的做法是加上 --no-types 参数只跑第一阶段,后面继续接现有的 mypy 或 Pyright 流水线。

为什么有人宁可把 Python 写成八股文

看完功能,我更在意的是它背后的观念冲突。作者在文档里写了一句相当刺耳的话:他做这个工具,纯粹是因为痛恨 dict[str, Any];如果谁觉得 Python 或 IDE 已经能自动推导类型了,那请别用它,小学徒(little padawan)。

站在主流 Python 社区的立场上,Gawain 的做法简直是历史倒退。Python 引入类型提示(Type Hints)十几年,之所以能温和落地,靠的就是“渐进式类型”和“局部类型推导”的折中哲学。把函数签名当契约锁死,函数内部保留动态语言的清爽,这是社区好不容易达成的平衡。如果强制要求每个中间变量都写上类型,不仅满屏都是视觉噪音,一旦重构某个底层函数的返回类型,所有调用它的地方哪怕逻辑没变,局部变量注解也得跟着改一遍,纯属给自己找罪受。

但如果你看一眼这两年真实的软件工程现场,就会明白为什么这种看似偏执的工具会冒出来。

《商君书》里讲过一句很冷酷的治理逻辑,叫“蠢愚知肖,皆守一法”。当一个系统无法信任执行者的自觉时,就会用最死板的明文规定去替代高阶的灵活裁量。今天的软件开发团队,正在遭遇两场信任危机。

消灭局部类型推导,防的不是编译器算不出,而是 AI 偷懒与人眼看漏。

第一场危机来自 AI 编码助手。大模型生成 Python 代码时有一个臭名昭著的习惯:只要遇到稍微复杂一点的字典结构或跨模块数据,为了哄骗 Linter 闭嘴,随手就会糊弄一个 dict[str, Any] 上去,或者干脆不写局部变量类型,把烂摊子留给运行时。Gawain 专门设立 GW020 规则见 Any 就杀,本质上就是在给 AI 生成的草台班子代码立规矩。

第二场危机来自代码审查(Code Review)的场景割裂。诚然,写代码的人在 VS Code 或 PyCharm 里把鼠标悬停在 total = compute() 上,半秒钟就能看到语言服务器推导出的类型。但审代码的人呢?绝大多数团队是在 GitHub 或 GitLab 的网页端看 PR diff 的。网页上没有悬停提示,看到一行 result = process_data(payload),审查者根本不知道 result 是个对象、字典还是列表,只能被迫跳出去翻 process_data 的定义。

这时候,把代码写得像二十年前的 Java 一样繁琐,要求每一处变量、每一个类属性都在类体里显式声明(GW004),反而成了帮审查者省脑子的防呆手段。作者是在用写代码时的十秒钟啰嗦,换取审代码时的三秒钟确定性。

Python 类型哲学分野:主流边界契约 vs Gawain 全显式防呆 主流共识 (mypy / Pyright / Ruff) • 仅强制函数参数与返回值签名 • 函数内部依赖局部类型推导 • 优势:代码简洁,重构牵连少 • 痛点:网页端 CR 需翻定义,难防 Any 极端显式派 (Gawain v0.3.0) • 变量、属性、常量全量强制标注 • GW020 直接封杀注解中的 Any • 优势:脱离 IDE 一眼看清,防 AI 糊弄 • 代价:样板代码膨胀,重构连带报错

小工具的宿命与真变量

当然,清醒的团队不会因为一个 0.36 秒的跑分就立刻全盘推翻现有规范。对于绝大多数业务频繁变动的数万行级项目,直接上 Gawain 的 strict 模式只会引发开发者的集体反弹。真正能把这套规则吃下去的,往往是对数据契约有洁癖的核心基础设施库,或者代码大部分由 AI 批量产出、必须靠死板门禁卡质量的团队。

况且,Gawain 本身就站在 Astral 的肩膀上——它的语法解析直接用了 ruff_python_parser,默认检查器用的也是 Astral 的 ty。在 Python 工具链全面 Rust 化的今天,这种只做一件事的垂直小工具面临一个很现实的生存拷问:一旦 Ruff 官方决定在 ANN 规则集之外,顺手加几条针对局部变量和类属性的强制检查规则,Gawain 的核心价值就会在一夜之间被吞并。

即便如此,这 14 次提交的代码依然值得一读。它像一面不太客气的镜子,照出了今天 Python 工程化的真实尴尬:当我们在享受动态语言的自由和 AI 写码的速度时,最终买单的,往往是那个在周五下午对着满屏隐式变量和 dict[str, Any] 发呆的审查者。