Python 类型检查策略:优先进行公共 API 验证

优先进行公共 API 验证,而非源代码严格性

库维护者应优先在测试套件上运行尽可能多的类型检查器,而不是仅仅专注于其内部源代码。虽然在源代码上运行类型检查器可以验证内部逻辑,但在测试上运行多个检查器可以确保公共 API 对尽可能广泛的用户保持兼容且可用,无论他们使用哪种工具。

这种方法可以防止源代码被大量的 # type: ignore 注释“污染”。因为不同的类型检查器——例如 Mypy, Pyrefly, Pyright, ty, 和 Zuban——通常对 Python typing spec 有不同的解释或不同程度的严格性,试图在实现中满足所有检查器可能会导致冗余且令人分心的注解。

案例研究:Polars 与 __eq__ 的实现

在 Polars 库中实现像 DataType.__eq__ 这样的方法,说明了多检查器源代码验证所带来的摩擦。在 Python 中,__eq__ 通常被期望返回一个 bool。然而,在 Polars 中,该函数可以根据输入返回不同的类型,因此需要使用 overloads。

为了在源代码中同时满足 Mypy, Pyrefly, 和 ty,开发者可能被迫编写如下函数签名:

@overload  # type: ignore[override]
def __eq__(  # pyrefly: ignore[bad-override]
    self, other: pl.DataTypeExpr
) -> pl.Expr: ...

@overload
def __eq__(self, other: PolarsDataType) -> bool: ...

def __eq__(self, other: pl.DataTypeExpr | PolarsDataType) -> pl.Expr | bool:  # ty: ignore[invalid-method-override]  # pyright: ignore[reportIncompatibleMethodOverride]

这导致七行代码中出现了四个不同的 type-ignore 注释。相反,当相同的功能通过测试套件进行验证时,Mypy, Pyrefly, Pyright, ty, 和 Zuban 都能对使用情况进行类型检查而不会报告错误。这表明,虽然检查器在功能应该如何实现上可能存在分歧,但它们通常在公共 API 应该如何表现上达成一致。

理解 Python 类型检查器格局

目前有五种主流的 Python 类型检查器:Mypy, Pyrefly, Pyright, ty, 和 Zuban。它们之间的差异源于 Python typing spec 中的歧义,特别是关于如何处理定义不足的类型信息。

类型检查器通常分为两类:

  • 严格检查器 (Strict Checkers): 这些检查器优先考虑防御潜在的错误,并可能为了确保最大安全性而发出误报。Pyrefly 被定位为一种严格、快速且符合规范的选项。
  • 宽松检查器 (Lenient Checkers): 这些检查器允许更渐进地采用类型注解,从而减少在遗留代码库中添加类型的初始摩擦。

社区观点与挑战

开发者之间的技术讨论突出了当前 Python typing 现状的一些系统性挫败感:

碎片化与工具开销

许多开发者发现检查器的激增是 typing 系统“拼凑而成”的迹象。一些用户报告同时运行 Pyright (在 CI 中) 和 Mypy (在本地) 因为它们能捕获不同类型的错误,例如 Pyright 在 overloads 上更严格,而 Mypy 在捕获 None 问题上更有效。

“错误墙”与推断

从 Mypy 转向更严格替代方案的开发者经常会遇到“错误墙”,特别是在像 Django 这样的复杂框架中。人们强烈希望有一种基于约束和推断的系统——即类型可以从使用情况中推断出来(例如 + 运算符),而不是需要详尽的手动注解。

静态 vs. 动态的权衡

一些批评者认为,与从一开始就使用静态类型语言相比,在 Python 中进行广泛的类型检查是低效的的,因为静态类型语言能为 AI 编码代理提供确定性,并带来运行时性能提升。

新兴工具

新的工具正在涌现以弥补这一差距,例如 RightTyper,它通过以大约 25% 的运行时开销监控程序执行来自动生成类型注解,从而允许开发者在不进行手动编写的情况下将类型集成到现有测试中。

Sources