mcpField
P
pydantic

pydantic

github.com/pydantic开发29.3k57 次安装今天更新
代码仓库

关于 pydantic

一项介绍 Pydantic 的技能,说明如何依据 Python 类型提示进行运行时验证与序列化,并讲解模型、约束和验证器,适用于处理不可信输入。

主要功能

  1. 01运行时验证与序列化
  2. 02字段元数据与约束
  3. 03自定义及注解式验证器
  4. 04类型强制转换与集合处理

适用场景

  1. 01验证 HTTP API 接收的数据
  2. 02定义带约束的模型字段
  3. 03选择验证器写法
  4. 04排查注解与子类问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pydantic/pydantic --skill pydantic

安装后在 Claude Code 中输入/pydantic确认已加载。

详细介绍

Pydantic 校验

简而言之,Pydantic 是带有运行时校验功能的 dataclasses。它利用类型提示来确定应如何进行校验(以及序列化)。它主要适用于处理外部不可信数据的场景,例如定义 HTTP API 时。

一般不建议使用 Pydantic 来定义在用户代码中实例化的类。这样做会失去灵活性(例如,无法使用 Pydantic 不支持的类型,且更难执行初始化后的修改)。在这种情况下,通常更好的做法是使用普通类(或标准库 dataclasses),因为静态类型检查器已经能够发现类型不匹配的问题。

基本用法

以下是使用 Pydantic 模型的一个简单示例:

from datetime import date

from pydantic import BaseModel, Field


class Person(BaseModel):
    name: str
    age: int = Field(description='The age of the person')
    birthdate: date | None = None


p = Person(name='John', age=20, birthdate='1970-01-01')

Pydantic 会对兼容的输入进行强制转换:ISO 日期字符串 '1970-01-01' 会被解析为 date。

约束与字段元数据

Field() 函数用于提供元数据和约束。 需要区分两类元数据:

  • 字段专属元数据:例如 deprecated 和 alias 等,只有附加到字段上时才有意义。
  • 类型专属元数据:包括 gt、max_length 等约束,以及影响 JSON Schema 的元数据(例如 description、title)。

使用赋值形式通过 Field() 声明模型字段:

from pydantic import BaseModel, Field


class User(BaseModel):
    first_name: str = Field(alias='name')

或者使用 Annotated 模式:

from typing import Annotated

from pydantic import BaseModel, Field


class Model(BaseModel):
    value: Annotated[int, Field(deprecated=True)] = 1

Annotated 模式有以下优势:

  • 使用 f: <type> = Field() 形式(不带默认值)可能会造成混淆,让用户误以为 f 有默认值,而实际上该字段仍然是必填的。
  • 可以为字段提供任意数量的元数据元素。如上例所示,Field() 函数只支持有限的约束/元数据,在某些情况下,你可能需要使用其他 Pydantic 工具,例如 WithJsonSchema。

但需要注意:

  • 对于对静态类型检查器有意义的元数据,应使用赋值形式。这包括:alias、default 和 default_factory。

  • 字段专属元数据只能用于“顶层”类型。一个常见的陷阱是这样写:

    from typing import Annotated
    
    from pydantic import BaseModel, Field
    
    
    class Model(BaseModel):
        field_bad: Annotated[int, Field(deprecated=True)] | None = None
        field_ok: Annotated[int | None, Field(deprecated=True)] = None
    

    在这个示例中,字段专属元数据应作用于整个联合类型。

约束

尽可能使用“内置”的校验约束,而不是自定义校验器:

from typing import Annotated

from annotated_types import Gt  # annotated_types is an alternative to the `Field()` function.
from pydantic import BaseModel, field_validator


class Model(BaseModel):
    constrained_int_ok: Annotated[int, Gt(1)]  # This is good

    constrained_int_bad: int

    @field_validator('constrained_int_bad')  # This is bad
    @classmethod
    def validate(cls, v: int) -> int:
        if not v > 1:
            raise ValueError('Value is not greater than 1')
        return v

有些约束无法通过 Field() 函数表达。例如,strip_whitespace、to_upper、to_lower 和 ascii_only 等字符串约束只能通过 pydantic.StringConstraints 指定:

from typing import Annotated

from pydantic import BaseModel, StringConstraints


class Model(BaseModel):
    # Do this instead of a validator calling s.strip():
    a: Annotated[str, StringConstraints(strip_whitespace=True)]

https://pydantic.dev/docs/validation/latest/api/pydantic/standard_library_types/ 是所有受支持的标准库类型及其约束的权威文档。

校验器

在某些情况下,你可能必须使用自定义校验器。应尽可能使用 after 校验器。由于它们在 Pydantic 校验之后运行,此时值已经是字段的类型。如果使用 before 校验器,输入数据可以是任意内容,因此更容易出错(尤其是对于模型校验器,输入不一定是字典,也可能是任意对象)。

如果可能,优先为校验器使用 Annotated 模式:

from typing import Annotated

from pydantic import AfterValidator, BaseModel, field_validator


def is_even(value: int) -> int:
    if value % 2 == 1:
        raise ValueError(f'{value} is not an even number')
    return value


class Model(BaseModel):
    # Prefer this form: the validator is right next to the field, making it easy to understand
    even: Annotated[int, AfterValidator(is_even)]
    odd: int

    # If you define a validator as decorator, make sure to define it as classmethod.
    @field_validator('odd', mode='after')
    @classmethod
    def is_odd(cls, value: int) -> int:
        if value % 2 == 0:
            raise ValueError(f'{value} is not an odd number')
        return value

使用装饰器模式可能导致行为不清晰,尤其是在校验器的执行顺序方面(特别是在子类中)。

类型强制转换、集合与联合类型

除非你使用了严格模式,Pydantic 在大多数情况下都会进行类型强制转换。例如,对于类型为 int 的字段,像 '123' 这样的字符串也会被接受。这一点同样适用于集合类型:list[str] 也接受元组、集合等。

因此,你应当避免以下做法:

  • 如果你的目标是通过验证器把 str 强制转换为 int,就不要使用 int | str 这样的联合类型。
  • 如果你的目标是同时接受列表和元组,就不要使用 collections.abc.Sequence 这类抽象集合。使用这些抽象集合效率较低。

一般来说,应尽量避免使用联合类型,因为该字段的每次使用都需要先检查每种类型,然后才能进行任何操作。

前向注解

Python 允许使用字符串形式编写注解,即前向引用。这可能会给 Pydantic 的求值带来麻烦,因此如果可能,最好避免使用。

如果你在模块中定义 Pydantic 模型,请尽量避免使用 from __future__ import annotations(它会默认将所有注解字符串化)。只为尚未定义的类型注解添加显式引号,例如:

from pydantic import BaseModel


class Model(BaseModel):
    self_ref: 'Model'

另外需要注意,在 Python >= 3.14 中,注解求值会被延迟,因此你根本不应使用字符串注解。

递归类型别名

你可能会想这样定义别名:

from typing import TypeAlias

JsonValue: TypeAlias = 'list[JsonValue] | dict[str, JsonValue] | str | bool | int | float | None'

由于该别名是递归的,因此必须加引号。Pydantic 通常无法对加引号的 TypeAlias 求值。 此时应改用显式类型别名(Python 3.12+ 上使用 type,或使用 TypeAliasType),Pydantic 可以正确解析:

type JsonValue = list[JsonValue] | dict[str, JsonValue] | str | bool | int | float | None
# Or, if not on Python >= 3.12:
from typing_extensions import TypeAliasType

JsonValue = TypeAliasType('JsonValue', 'list[JsonValue] | dict[str, JsonValue] | str | bool | int | float | None')
模型子类与可辨识联合

子类化是一种非常常见的 Python 模式,但在 Pydantic 中可能会埋下隐患。你可能会想这样写:

from pydantic import BaseModel


class Base(BaseModel):
    base_field: int

    def common_method(self) -> None: ...


class Sub1(Base):
    sub1_field: str


class Sub2(Base):
    sub2_field: bool


class Main(BaseModel):
    model: Base


m: Main = Main(model=Sub1(base_field=1, sub1_field='test'))

这个示例可以运行,但在序列化 m 时不会符合预期:

m.model_dump()
#> {'model': {'base_field': 1}} -> sub1_field missing

原因在于 Pydantic 是按照声明的类型(Base)而非运行时的子类进行序列化的。 验证也遵循同样的规则:Main(model={'base_field': 1, 'sub1_field': 'test'}) 会按照 Base 进行验证,因此 sub1_field 会被忽略,而不会生成 Sub1 实例。

因此,建议尽量使用可辨识联合(前提是你能设置一个 type 字段来区分各个模型):

from typing import Annotated, Literal, TypeAlias

from pydantic import BaseModel, Field


class Sub1(Base):
    type: Literal['sub1']
    sub1_field: str


class Sub2(Base):
    type: Literal['sub2']
    sub2_field: bool


Subs: TypeAlias = Annotated[Sub1 | Sub2, Field(discriminator='type')]


class Main(BaseModel):
    model: Subs

或者使用泛型:

from pydantic import BaseModel


class Main[BaseT: Base](BaseModel):
    model: BaseT


m: Main[Sub1] = Main[Sub1](model={'base_field': 1, 'sub1_field': 'test'})  # Will work

如果可辨识联合和泛型都不适用,则可以将多态序列化(Pydantic >=2.13)或serialize as any(Pydantic <2.13)作为最后的手段。

评论

支持 Markdown