关于 pydantic
Pydantic 是基于类型提示进行运行时验证与序列化的 Python 库,适用于处理外部或不可信数据。它提供模型字段、约束、类型转换和多种验证方式。
主要功能
- 01基于类型提示的验证
- 02数据序列化
- 03字段元数据与约束
- 04类型转换
- 05自定义及注解式验证器
适用场景
- 01验证 HTTP API 接收的数据
- 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)作为最后的手段。