mcpField
P
pydantic

pydantic

github.com/pydantic开发1452.2k 次安装2 周前更新
代码仓库

关于 pydantic

Pydantic 是基于类型提示进行运行时验证与序列化的 Python 库,适用于处理外部或不可信数据。它提供模型字段、约束、类型转换和多种验证方式。

主要功能

  1. 01基于类型提示的验证
  2. 02数据序列化
  3. 03字段元数据与约束
  4. 04类型转换
  5. 05自定义及注解式验证器

适用场景

  1. 01验证 HTTP API 接收的数据
  2. 02将外部数据解析为类型化模型

安装

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

$npx skills add https://github.com/pydantic/skills --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