类型注解写了一半,Python 侧 mypy 官方入门先接哪几步?

几个模块已经写上 str、list[str]、Optional,本地解释器照跑,传错类型也不会在提交前拦住。想接静态检查,又怕一把 --strict 把 CI 刷红。风格和导入排序若已经在看 Ruff 能替掉哪一层,那条管的是 lint / format。这里只谈类型,按 mypy Getting started 渐进接上,不把 Ruff FAQ 再讲一遍。

先装上,对单个文件跑一次

文档开篇写明:跑 mypy 需要 Python 3.10 或更高。安装和最小调用是:

python3 -m pip install mypy
mypy program.py

它只做静态检查,不会替你执行代码。报错可以先放着,解释器照样能跑。这是官方默认姿态,不是安装坏了,也不是必须清零才能合并。

直接对存量仓库扫一遍,常常几乎零报错。文档把这当成特性:没注解的函数默认当动态类型,mypy 基本不查。注解没铺开之前,「跑了等于没跑」是正常现象,别据此判断工具没用。更常见的原因是:你点名的文件里,绝大多数函数根本还没有注解。

第一次接入,先挑一个刚改过、注解相对完整的小模块跑通,确认命令、Python 版本、虚拟环境都对,再扩到包路径。环境不一致时,先把解释器对齐,否则争论的是安装问题,不是类型问题。

注解一加上,检查才真正开始

Getting started 用 greeting 举例:函数没有注解时,把 int 或 bytes 传进去,运行期会炸,mypy 却默不作声。写成 def greeting(name: str) -> str: 之后,同样调用就会报 incompatible type。函数体里乱用运算符,也会被标出来。动态与静态可以按函数粒度切换,迁老代码或写原型时特别省事。

更复杂的形状对照 Type hints cheat sheet:list[str]、dict[str, float]、固定长度 tuple[int, str]、可变长度 tuple[int, ...],以及 int | str、str | None。只要「能 for 遍历」而不强求 list,用 collections.abc.Iterable。只要只读映射,用 Mapping,别随手写 dict。Python 3.9+ 集合用内置泛型即可。3.8 及更早才需要 typing.List 那一套。注解习惯是 from typing import …,不要 import typing as t 满天飞,也别 from typing import *。

第三方库缺 stub 时,mypy 会提示装 types-…(例如 types-requests、types-PyYAML)。这是类型信息缺失,不是业务逻辑错了。装上 stub 再查,噪音会小很多。发行名和 import 名不一致时(stub 包叫 types-PyYAML,代码里却是 import yaml),以官方提示为准。库本身若声明了 typing 支持,mypy 会直接读它的注解。

函数体一旦进入静态检查,mypy 会尽量做局部推断:空列表要写成 output: list[float] = [] 这类,否则它不知道元素类型。isinstance 分支里,它通常能收窄联合类型。cheat sheet 里的 reveal_type(...) 只适合临时探路,提交前记得删掉。推断和现实不符时,再用 cast 或带说明的 # type: ignore,别养成「整文件忽略」的习惯。前向引用可用 from __future__ import annotations,或把类型写成字符串。

存量仓库别一上来 --strict

Using mypy with an existing codebase 写得很直:大仓先挑一小块(文档举例大概几千到几万行量级)让 mypy 干净通过,再扩大,先看到「有一条绿的检查」。--strict 会打开一串更严检查(含 disallow_untyped_defs 一类),对「注解覆盖还很稀」的现况往往过猛。更稳的是配置文件里按模块收紧,或先开 strict = True 再关掉个别暂时做不到的项。

Getting started 也提醒:--disallow-untyped-defs、--check-untyped-defs 都是后面加严的旋钮,不是第一天必须全开。第三方库 stub 不齐时,--ignore-missing-imports 可暂时用,但更建议按模块配置,而不是全局永久关闭。

官方反复提到几步:

  • 把 mypy 调用、同一套选项、同一批文件钉进脚本、tox 或 CI,版本也钉住,避免每人本机各跑各的。
  • 暂时不打算动的包,可用配置段 ignore_errors = True。也可以全局先忽略、只对准备好的模块打开,先保证「已接管的模块」有信号。
  • 缺 stub / 找不到实现的导入,优先装 stub 包。到处 # type: ignore 会变难维护,宁可按模块设 ignore_missing_imports。全局一把关掉导入相关错误码,文档认为可能把后面的问题藏起来,尽量少用。
  • 被到处 import 的工具模块、模型类,早点补注解,收益比边角脚本大。
  • 新代码要求带注解,改旧代码时顺手补——覆盖率会慢慢涨,不必周末通宵。
  • 真要加速迭代,文档还点到 mypy daemon。超大仓再考虑远程缓存,入门阶段大多用不上。

目标可以瞄着「某一天整仓也能过 mypy --strict」,但那是渐进结果,不是第一天的开关。existing codebase 页把 --strict 拆成多项配置,可按模块逐步打开,例如只对已补完的包启用 disallow_untyped_defs,防止回潮。

接到哪算够用

日常开发:主路径有注解,CI 固定跑同一命令,就比「只写注解不检查」强一截。原型阶段可以先动态,接口稳定后再补类型——Getting started 明确承认这种节奏。和 Ruff 并行时,各管各的:一个盯风格与一批 lint 规则,一个盯类型。别指望用 formatter 代替类型检查。

你那边是先钉 CI 再补注解,还是先给常改的模块补完再开流水线?