跳转至

Mypy

Pydantic 与 mypy 配合得很好,开箱即用。

然而,Pydantic 还附带了一个 mypy 插件,该插件为 mypy 添加了许多重要的特定于 Pydantic 的功能,从而提高了它对代码进行类型检查的能力。

例如,考虑以下脚本:

from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel


class Model(BaseModel):
    age: int
    first_name = 'John'
    last_name: Optional[str] = None
    signup_ts: Optional[datetime] = None
    list_of_ints: List[int]


m = Model(age=42, list_of_ints=[1, '2', b'3'])
print(m.middle_name)  # not a model field!
Model()  # will raise a validation error for age and list_of_ints

在没有任何特殊配置的情况下,mypy 不会捕获缺失的模型字段注释,并警告关于 Pydantic 正确解析的 list_of_ints 参数:

test.py:15: error: List item 1 has incompatible type "str"; expected "int"  [list-item]
test.py:15: error: List item 2 has incompatible type "bytes"; expected "int"  [list-item]
test.py:16: error: "Model" has no attribute "middle_name"  [attr-defined]
test.py:17: error: Missing named argument "age" for "Model"  [call-arg]
test.py:17: error: Missing named argument "list_of_ints" for "Model"  [call-arg]

但是启用了插件,它会给出正确的错误:

9: error: Untyped fields disallowed  [pydantic-field]
16: error: "Model" has no attribute "middle_name"  [attr-defined]
17: error: Missing named argument "age" for "Model"  [call-arg]
17: error: Missing named argument "list_of_ints" for "Model"  [call-arg]

使用 pydantic 的 mypy 插件,您可以放心地重构模型,因为如果您的字段名称或类型发生更改,mypy 将捕获任何错误。

还有其他好处!详情请见下文。

不使用插件使用 mypy

你可以使用:

mypy \
  --ignore-missing-imports \
  --follow-imports=skip \
  --strict-optional \
  pydantic_mypy_test.py

严格可选

为了使你的代码通过 --strict-optional ,你需要使用 Optional[]Optional[] 的别名来代替所有默认值为 None 的字段。(这是 mypy 的标准做法。)

其他 Pydantic 接口

Pydantic 数据类和 validate_call 装饰器也应该与 mypy 配合良好。

Mypy 插件功能

Model.__init__ 生成一个签名

  • 任何没有动态确定别名的必填字段都将作为必填的关键字参数包含在内。

  • 如果 Config.populate_by_name=True ,则生成的签名将使用字段名称,而不是别名。

  • 如果 Config.extra='forbid' 和你不使用动态确定的别名,生成的签名将不允许意外输入。

  • 可选:如果 init_forbid_extra 插件设置为 True ,则对 __init__ 的意外输入即使 Config.extra 不是 'forbid' 也会引发错误。

  • 可选:如果 init_typed 插件设置设置为 True ,则生成的签名将使用模型字段的类型(否则将被注释为 Any 以允许解析)。

Model.model_construct 生成带类型的签名

  • 当输入数据已知有效且不应被解析时, model_construct 方法是 __init__ 的一种替代方法。由于此方法在运行时不执行任何验证,因此静态检查对于检测错误很重要。

保留 Config.frozen

  • 如果 Config.frozenTrue ,那么当你尝试更改模型字段的值时,会得到一个 mypy 错误;可参看假不变性。

dataclasses 生成一个签名

  • @pydantic.dataclasses.dataclass 修饰的类与标准 Python 数据类的类型检查方式相同

  • @pydantic.dataclasses.dataclass 修饰符接受一个 config 关键字参数,其含义与 Config 子类相同。

尊重 Fielddefaultdefault_factory 的类型

  • 字段同时具有 defaultdefault_factory 将在静态检查期间导致错误。

  • defaultdefault_factory 值的类型必须与字段的类型兼容。

警告使用未指定类型的字段

  • 你在模型上分配公共属性而不注释其类型时,将得到一个 mypy 错误

  • 如果你的目标是设置一个 ClassVar,你应该使用 typing.ClassVar 显式地注释该字段。

可选功能:

防止使用必需的动态别名

  • 如果将 warn_required_dynamic_aliases 插件设置设置为 True ,则在使用具有 Config.populate_by_name=False 的模型上的动态确定别名或别名生成器时,将得到 mypy 错误。

  • 这很重要,因为如果存在这样的别名,mypy 就不能正确地对 __init__ 的调用进行类型检查。在这种情况下,它将默认将所有参数都视为可选的。

启用插件

要启用该插件,只需将 pydantic.mypy 添加到您的 mypy 配置文件中的插件列表中(这可能是 mypy.inipyproject.tomlsetup.cfg )。

要开始使用,您只需要创建一个带有以下内容的 mypy.ini 文件:

[mypy]
plugins = pydantic.mypy

注意

如果您使用的是 pydantic.v1 模型,您需要将 pydantic.v1.mypy 添加到您的插件列表中。

该插件与 mypy 版本 >=0.930 兼容。

请参考插件配置文档以获取更多详细信息。

配置插件

要更改插件设置的值,请在您的 mypy 配置文件中创建一个名为 [pydantic-mypy] 的部分,并添加任何要覆盖的设置的键值对。

启用了所有插件严格标志(以及其他一些 mypy 严格标志)的 mypy.ini 文件可能看起来像:

[mypy]
plugins = pydantic.mypy

follow_imports = silent
warn_redundant_casts = True
warn_unused_ignores = True
disallow_any_generics = True
check_untyped_defs = True
no_implicit_reexport = True

# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = True

[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True

截至 mypy>=0.900 ,mypy 配置也可能包含在 pyproject.toml 文件中,而不是 mypy.ini 中。与上述相同的配置为:

[tool.mypy]
plugins = [
  "pydantic.mypy"
]

follow_imports = "silent"
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true

# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = true

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true

关于 --disallow-any-explicit 的说明

如果你正在使用 --disallow-any-explicitmypy 配置设置(或其他禁止 Any 的设置),那么在扩展 BaseModel 时可能会遇到 no-any-explicit 错误。这是因为默认情况下,Pydanticmypy 插件会添加一个带有 init 方法的签名,类似于 def init(self, field_1: Any, field_2: Any, **kwargs: Any):

注释

“为什么要有额外的签名?”Pydantic mypy 插件添加了一个带有 __init__ 签名的 def __init__(self, field_1: Any, field_2: Any, **kwargs: Any): 方法,以避免在使用与字段注释不匹配的类型初始化模型时出现类型错误。例如,如果没有这个 Any 签名, Model(date='2024-01-01') 会引发类型错误,但 Pydantic 有能力将字符串 '2024-01-01' 解析为 datetime.date 类型。

要解决此问题,您需要为 Pydantic mypy 插件启用严格模式设置。具体来说,在您的 [pydantic-mypy] 部分添加以下选项:

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true

使用 init_forbid_extra = True ,从生成的 __init__ 签名中删除 **kwargs 。使用 init_typed = True ,将字段的 Any 类型替换为其实际的类型提示。

这种配置允许你在 Pydantic 模型中使用 --disallow-any-explicit 而不会出现错误。但是,请注意,这种更严格的检查可能会将一些有效的 Pydantic 使用案例(例如将字符串传递给 datetime 字段)标记为类型错误。


本文总阅读量