跳转到内容

模型

API 文档

pydantic.main.BaseModel

在 Pydantic 中定义模式的主要方式之一是通过模型。模型是继承自 BaseModel 并将字段定义为带注解属性的类。

您可以将模型看作类似于 C 等语言中的结构体,或者看作 API 中单个端点的需求。

模型与 Python 的 dataclasses 有许多相似之处,但在设计上存在一些微妙而重要的差异,这些差异简化了与验证、序列化和 JSON schema 生成相关的某些工作流程。您可以在文档的数据类部分找到更多相关讨论。

可以将不可信的数据传递给模型,经过解析和验证后,Pydantic 保证生成的模型实例的字段将符合模型上定义的字段类型。

验证 — 一个故意用词不当的术语

摘要

我们使用“验证”这个术语来指代实例化一个遵循指定类型和约束的模型(或其他类型)的过程。这项任务,Pydantic 因此而闻名,在口语中被最广泛地认知为“验证”,尽管在其他语境下,“验证”一词的含义可能更为狭窄。


长篇解释

围绕“验证”一词的潜在混淆源于这样一个事实:严格来说,Pydantic 的主要关注点与“验证”的字典定义并不完全一致。

验证

名词 检查或证明某事物的有效性或准确性的行为。

在 Pydantic 中,“验证”一词指的是实例化一个遵循指定类型和约束的模型(或其他类型)的过程。Pydantic 保证的是输出的类型和约束,而不是输入数据。当考虑到 Pydantic 的 ValidationError 是在数据无法成功解析为模型实例时引发的,这一区别就变得显而易见了。

虽然这个区别最初看起来可能很微妙,但它具有实际意义。在某些情况下,“验证”不仅仅是创建模型,还可能包括数据的复制和强制转换。这可能涉及到复制传递给构造函数的参数,以便在不改变原始输入数据的情况下将其强制转换为新类型。要更深入地了解这对您的使用的影响,请参阅下面的数据转换属性复制部分。

本质上,Pydantic 的主要目标是确保经过处理(称为“验证”)后的结果结构完全符合应用的类型提示。鉴于“验证”作为这一过程的口语术语已被广泛采用,我们将在文档中持续使用它。

虽然“解析”和“验证”这两个术语以前可以互换使用,但从现在起,我们旨在专门使用“验证”,而“解析”将专门用于讨论JSON 解析

模型基本用法

注意

Pydantic 严重依赖现有的 Python 类型构造来定义模型。如果您不熟悉这些,以下资源可能会有所帮助:

from pydantic import BaseModel, ConfigDict


class User(BaseModel):
    id: int
    name: str = 'Jane Doe'

    model_config = ConfigDict(str_max_length=10)  # (1)!
  1. Pydantic 模型支持多种配置值(请参阅此处了解可用的配置值)。

在此示例中,User 是一个具有两个字段的模型:

  • id,它是一个整数(使用 int 类型定义),并且是必需的。
  • name,它是一个字符串(使用 str 类型定义),并且不是必需的(它有默认值)。

关于类型的文档扩展了支持的类型。

可以使用 Field() 函数以多种方式自定义字段。有关更多信息,请参阅关于字段的文档

然后可以实例化该模型:

user = User(id='123')

userUser 的一个实例。对象的初始化将执行所有解析和验证。如果没有引发 ValidationError 异常,您就知道生成的模型实例是有效的。

模型的字段可以像普通属性一样访问 user 对象:

assert user.name == 'Jane Doe'  # (1)!
assert user.id == 123  # (2)!
assert isinstance(user.id, int)
  1. nameuser 初始化时未设置,因此使用了默认值。可以检查 model_fields_set 属性来查看在实例化期间显式设置的字段名称。
  2. 请注意,字符串 '123' 被强制转换为整数,其值为 123。有关 Pydantic 强制转换逻辑的更多详细信息,请参见数据转换部分。

可以使用 model_dump() 方法序列化模型实例:

assert user.model_dump() == {'id': 123, 'name': 'Jane Doe'}

在实例上调用 dict 也会提供一个字典,但嵌套字段不会被递归地转换为字典。model_dump() 还提供了许多参数来自定义序列化结果。

默认情况下,模型是可变的,可以通过属性赋值来更改字段值:

user.id = 321
assert user.id == 321

警告

在定义模型时,请注意字段名称与其类型注解之间的命名冲突。

例如,以下代码的行为将不如预期,并会产生验证错误:

from typing import Optional

from pydantic import BaseModel


class Boo(BaseModel):
    int: Optional[int] = None


m = Boo(int=123)  # Will fail to validate.

由于 Python 评估带注解的赋值语句的方式,该语句等同于 int: None = None,从而导致验证错误。

模型方法和属性

上面的示例仅展示了模型功能的冰山一角。模型具有以下方法和属性:

注意

请参阅 BaseModel 的 API 文档以获取类定义,包括完整的方法和属性列表。

提示

请参阅迁移指南中的pydantic.BaseModel 的变更,了解与 Pydantic V1 的差异详情。

数据转换

Pydantic 可能会转换输入数据以强制其符合模型字段类型,在某些情况下,这可能导致信息丢失。例如:

from pydantic import BaseModel


class Model(BaseModel):
    a: int
    b: float
    c: str


print(Model(a=3.000, b='2.72', c=b'binary data').model_dump())
#> {'a': 3, 'b': 2.72, 'c': 'binary data'}

这是 Pydantic 的一个刻意决定,并且通常是最有用的方法。有关此主题的更长讨论,请参见此问题

尽管如此,Pydantic 提供了一个严格模式,在这种模式下不执行数据转换。值必须与声明的字段类型相同。

集合也是如此。在大多数情况下,您不应使用抽象容器类,而应直接使用具体类型,例如 list

from pydantic import BaseModel


class Model(BaseModel):
    items: list[int]  # (1)!


print(Model(items=(1, 2, 3)))
#> items=[1, 2, 3]
  1. 在这种情况下,您可能会想使用抽象的 Sequence 类型来同时允许列表和元组。但 Pydantic 会负责将元组输入转换为列表,因此在大多数情况下这并非必要。

此外,使用这些抽象类型也可能导致验证性能不佳,总的来说,使用具体的容器类型可以避免不必要的检查。

额外数据

默认情况下,当您提供额外数据时,Pydantic 模型不会报错,这些值将被简单地忽略:

from pydantic import BaseModel


class Model(BaseModel):
    x: int


m = Model(x=1, y='a')
assert m.model_dump() == {'x': 1}

extra 配置值可用于控制此行为:

from pydantic import BaseModel, ConfigDict


class Model(BaseModel):
    x: int

    model_config = ConfigDict(extra='allow')


m = Model(x=1, y='a')  # (1)!
assert m.model_dump() == {'x': 1, 'y': 'a'}
assert m.__pydantic_extra__ == {'y': 'a'}
  1. 如果 extra 设置为 'forbid',这将失败。

该配置可以接受三个值:

  • 'ignore':提供额外数据将被忽略(默认值)。
  • 'forbid':不允许提供额外数据。
  • 'allow':允许提供额外数据,并将其存储在 __pydantic_extra__ 字典属性中。可以显式地对 __pydantic_extra__ 进行注解,以为额外字段提供验证。

验证方法(例如 model_validate())有一个可选的 extra 参数,它将覆盖该验证调用的模型 extra 配置值。

有关更多详细信息,请参阅 extra API 文档。

Pydantic 数据类也支持额外数据(请参见数据类配置部分)。

嵌套模型

更复杂的层次化数据结构可以通过使用模型本身作为注解中的类型来定义。

from typing import Optional

from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: Optional[float] = None


class Bar(BaseModel):
    apple: str = 'x'
    banana: str = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: list[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
"""
foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'), Bar(apple='x2', banana='y')]
"""
print(m.model_dump())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [{'apple': 'x1', 'banana': 'y'}, {'apple': 'x2', 'banana': 'y'}],
}
"""
from pydantic import BaseModel


class Foo(BaseModel):
    count: int
    size: float | None = None


class Bar(BaseModel):
    apple: str = 'x'
    banana: str = 'y'


class Spam(BaseModel):
    foo: Foo
    bars: list[Bar]


m = Spam(foo={'count': 4}, bars=[{'apple': 'x1'}, {'apple': 'x2'}])
print(m)
"""
foo=Foo(count=4, size=None) bars=[Bar(apple='x1', banana='y'), Bar(apple='x2', banana='y')]
"""
print(m.model_dump())
"""
{
    'foo': {'count': 4, 'size': None},
    'bars': [{'apple': 'x1', 'banana': 'y'}, {'apple': 'x2', 'banana': 'y'}],
}
"""

支持自引用模型。有关更多详细信息,请参阅与前向引用注解相关的文档。

重建模型模式

当您在代码中定义一个模型类时,Pydantic 将分析该类的主体以收集执行验证和序列化所需的各种信息,这些信息被收集在一个核心模式中。值得注意的是,模型的类型注解会被评估,以理解每个字段的有效类型(更多信息可以在架构文档中找到)。然而,有时注解可能会引用在创建模型类时尚未定义的符号。为了规避这个问题,可以使用 model_rebuild() 方法:

from pydantic import BaseModel, PydanticUserError


class Foo(BaseModel):
    x: 'Bar'  # (1)!


try:
    Foo.model_json_schema()
except PydanticUserError as e:
    print(e)
    """
    `Foo` is not fully defined; you should define `Bar`, then call `Foo.model_rebuild()`.

    For further information visit https://errors.pydantic.dev/2/u/class-not-fully-defined
    """


class Bar(BaseModel):
    pass


Foo.model_rebuild()
print(Foo.model_json_schema())
"""
{
    '$defs': {'Bar': {'properties': {}, 'title': 'Bar', 'type': 'object'}},
    'properties': {'x': {'$ref': '#/$defs/Bar'}},
    'required': ['x'],
    'title': 'Foo',
    'type': 'object',
}
"""
  1. 在创建 Foo 类时,Bar 尚未定义。因此,使用了前向引用注解

Pydantic 会尝试自动判断何时需要这样做,并在未完成时报错,但您可能希望在处理递归模型或泛型时主动调用 model_rebuild()

在 V2 中,model_rebuild() 替换了 V1 中的 update_forward_refs()。新行为有一些细微的差异。最大的变化是,当在最外层模型上调用 model_rebuild() 时,它会构建一个用于验证整个模型(包括嵌套模型等)的核心模式,因此在调用 model_rebuild() 之前,所有级别的所有类型都需要准备好。

任意类的实例

(以前称为“ORM 模式”/from_orm)。

Pydantic 模型也可以通过读取与模型字段名称相对应的实例属性来从任意类的实例创建。此功能的一个常见应用是与对象关系映射(ORM)集成。

要做到这一点,请将 from_attributes 配置值设置为 True(有关更多详细信息,请参阅配置文档)。

这里的示例使用了 SQLAlchemy,但同样的方法应该适用于任何 ORM。

from typing import Annotated

from sqlalchemy import ARRAY, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

from pydantic import BaseModel, ConfigDict, StringConstraints


class Base(DeclarativeBase):
    pass


class CompanyOrm(Base):
    __tablename__ = 'companies'

    id: Mapped[int] = mapped_column(primary_key=True, nullable=False)
    public_key: Mapped[str] = mapped_column(
        String(20), index=True, nullable=False, unique=True
    )
    domains: Mapped[list[str]] = mapped_column(ARRAY(String(255)))


class CompanyModel(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    id: int
    public_key: Annotated[str, StringConstraints(max_length=20)]
    domains: list[Annotated[str, StringConstraints(max_length=255)]]


co_orm = CompanyOrm(
    id=123,
    public_key='foobar',
    domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <__main__.CompanyOrm object at 0x0123456789ab>
co_model = CompanyModel.model_validate(co_orm)
print(co_model)
#> id=123 public_key='foobar' domains=['example.com', 'foobar.com']

嵌套属性

当使用属性来解析模型时,模型实例将根据顶层属性和更深层嵌套的属性适当地创建。

下面是一个演示该原理的示例:

from pydantic import BaseModel, ConfigDict


class PetCls:
    def __init__(self, *, name: str, species: str):
        self.name = name
        self.species = species


class PersonCls:
    def __init__(self, *, name: str, age: float = None, pets: list[PetCls]):
        self.name = name
        self.age = age
        self.pets = pets


class Pet(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    name: str
    species: str


class Person(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    name: str
    age: float = None
    pets: list[Pet]


bones = PetCls(name='Bones', species='dog')
orion = PetCls(name='Orion', species='cat')
anna = PersonCls(name='Anna', age=20, pets=[bones, orion])
anna_model = Person.model_validate(anna)
print(anna_model)
"""
name='Anna' age=20.0 pets=[Pet(name='Bones', species='dog'), Pet(name='Orion', species='cat')]
"""

错误处理

Pydantic 在其验证的数据中发现错误时,会引发一个 ValidationError 异常。

无论发现多少个错误,都只会引发一个异常,并且该验证错误将包含有关所有错误及其发生方式的信息。

有关标准和自定义错误的详细信息,请参见错误处理

作为演示:

from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    list_of_ints: list[int]
    a_float: float


data = dict(
    list_of_ints=['1', 2, 'bad'],
    a_float='not a float',
)

try:
    Model(**data)
except ValidationError as e:
    print(e)
    """
    2 validation errors for Model
    list_of_ints.2
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='bad', input_type=str]
    a_float
      Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='not a float', input_type=str]
    """

验证数据

Pydantic 在模型类上提供了三种用于解析数据的方法:

  • model_validate():这与模型的 __init__ 方法非常相似,但它接受一个字典或对象,而不是关键字参数。如果传递的对象无法被验证,或者它不是字典或相关模型的实例,将引发 ValidationError
  • model_validate_json():这会将提供的数据作为 JSON 字符串或 bytes 对象进行验证。如果您的传入数据是 JSON 负载,这通常被认为更快(而不是手动将数据解析为字典)。在文档的JSON部分了解更多关于 JSON 解析的信息。
  • model_validate_strings():这接受一个带有字符串键和值的字典(可以是嵌套的),并以 JSON 模式验证数据,以便可以将所述字符串强制转换为正确的类型。
from datetime import datetime
from typing import Optional

from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: Optional[datetime] = None


m = User.model_validate({'id': 123, 'name': 'James'})
print(m)
#> id=123 name='James' signup_ts=None

try:
    User.model_validate(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    """
    1 validation error for User
      Input should be a valid dictionary or instance of User [type=model_type, input_value=['not', 'a', 'dict'], input_type=list]
    """

m = User.model_validate_json('{"id": 123, "name": "James"}')
print(m)
#> id=123 name='James' signup_ts=None

try:
    m = User.model_validate_json('{"id": 123, "name": 123}')
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    name
      Input should be a valid string [type=string_type, input_value=123, input_type=int]
    """

try:
    m = User.model_validate_json('invalid JSON')
except ValidationError as e:
    print(e)
    """
    1 validation error for User
      Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='invalid JSON', input_type=str]
    """

m = User.model_validate_strings({'id': '123', 'name': 'James'})
print(m)
#> id=123 name='James' signup_ts=None

m = User.model_validate_strings(
    {'id': '123', 'name': 'James', 'signup_ts': '2024-04-01T12:00:00'}
)
print(m)
#> id=123 name='James' signup_ts=datetime.datetime(2024, 4, 1, 12, 0)

try:
    m = User.model_validate_strings(
        {'id': '123', 'name': 'James', 'signup_ts': '2024-04-01'}, strict=True
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    signup_ts
      Input should be a valid datetime, invalid datetime separator, expected `T`, `t`, `_` or space [type=datetime_parsing, input_value='2024-04-01', input_type=str]
    """
from datetime import datetime

from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None = None


m = User.model_validate({'id': 123, 'name': 'James'})
print(m)
#> id=123 name='James' signup_ts=None

try:
    User.model_validate(['not', 'a', 'dict'])
except ValidationError as e:
    print(e)
    """
    1 validation error for User
      Input should be a valid dictionary or instance of User [type=model_type, input_value=['not', 'a', 'dict'], input_type=list]
    """

m = User.model_validate_json('{"id": 123, "name": "James"}')
print(m)
#> id=123 name='James' signup_ts=None

try:
    m = User.model_validate_json('{"id": 123, "name": 123}')
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    name
      Input should be a valid string [type=string_type, input_value=123, input_type=int]
    """

try:
    m = User.model_validate_json('invalid JSON')
except ValidationError as e:
    print(e)
    """
    1 validation error for User
      Invalid JSON: expected value at line 1 column 1 [type=json_invalid, input_value='invalid JSON', input_type=str]
    """

m = User.model_validate_strings({'id': '123', 'name': 'James'})
print(m)
#> id=123 name='James' signup_ts=None

m = User.model_validate_strings(
    {'id': '123', 'name': 'James', 'signup_ts': '2024-04-01T12:00:00'}
)
print(m)
#> id=123 name='James' signup_ts=datetime.datetime(2024, 4, 1, 12, 0)

try:
    m = User.model_validate_strings(
        {'id': '123', 'name': 'James', 'signup_ts': '2024-04-01'}, strict=True
    )
except ValidationError as e:
    print(e)
    """
    1 validation error for User
    signup_ts
      Input should be a valid datetime, invalid datetime separator, expected `T`, `t`, `_` or space [type=datetime_parsing, input_value='2024-04-01', input_type=str]
    """

如果您想验证除 JSON 以外格式的序列化数据,您应该自己将数据加载到字典中,然后将其传递给 model_validate

注意

根据所涉及的类型和模型配置,model_validatemodel_validate_json 可能具有不同的验证行为。如果您有来自非 JSON 源的数据,但希望获得与 model_validate_json 相同的验证行为和错误,我们目前的建议是使用 model_validate_json(json.dumps(data)),或者如果数据是(可能是嵌套的)带有字符串键和值的字典形式,则使用 model_validate_strings

注意

如果您将模型的实例传递给 model_validate,您需要考虑在模型的配置中设置 revalidate_instances。如果您不设置此值,那么模型实例上的验证将被跳过。请参见以下示例:

from pydantic import BaseModel


class Model(BaseModel):
    a: int


m = Model(a=0)
# note: setting `validate_assignment` to `True` in the config can prevent this kind of misbehavior.
m.a = 'not an int'

# doesn't raise a validation error even though m is invalid
m2 = Model.model_validate(m)
from pydantic import BaseModel, ConfigDict, ValidationError


class Model(BaseModel):
    a: int

    model_config = ConfigDict(revalidate_instances='always')


m = Model(a=0)
# note: setting `validate_assignment` to `True` in the config can prevent this kind of misbehavior.
m.a = 'not an int'

try:
    m2 = Model.model_validate(m)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    a
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not an int', input_type=str]
    """

创建不经验证的模型

Pydantic 还提供了 model_construct() 方法,该方法允许不经验证地创建模型。这在至少几种情况下可能很有用:

  • 当处理已知有效的复杂数据时(出于性能原因)
  • 当一个或多个验证器函数不是幂等的
  • 当一个或多个验证器函数具有您不希望被触发的副作用时。

警告

model_construct() 不执行任何验证,这意味着它可以创建无效的模型。您只应在数据已经过验证或您绝对信任的情况下使用 model_construct() 方法。

注意

在 Pydantic V2 中,验证(通过直接实例化或 model_validate* 方法)与 model_construct() 之间的性能差距已大大缩小。对于简单的模型,进行验证甚至可能更快。如果您出于性能原因使用 model_construct(),您可能需要对您的用例进行性能分析,而不是假定它实际上更快。

请注意,对于根模型,根值可以按位置传递给 model_construct(),而不是使用关键字参数。

以下是关于 model_construct() 行为的一些附加说明:

  • 当我们说“不执行任何验证”时——这包括将字典转换为模型实例。因此,如果您有一个引用模型类型的字段,您需要自己将内部字典转换为模型。
  • 如果您没有为具有默认值的字段传递关键字参数,仍将使用默认值。
  • 对于具有私有属性的模型,__pydantic_private__ 字典的填充方式将与使用验证创建模型时相同。
  • 模型或其任何父类的 __init__ 方法都不会被调用,即使定义了自定义的 __init__ 方法。

关于额外数据行为与 model_construct()

  • 对于 extra 设置为 'allow' 的模型,与字段不对应的数据将正确存储在 __pydantic_extra__ 字典中,并保存到模型的 __dict__ 属性中。
  • 对于 extra 设置为 'ignore' 的模型,与字段不对应的数据将被忽略——即,不会存储在实例的 __pydantic_extra____dict__ 中。
  • 与使用验证实例化模型不同,当 extra 设置为 'forbid' 时,调用 model_construct() 不会因存在与字段不对应的数据而引发错误。相反,所述输入数据将被简单地忽略。

模型复制

API 文档

pydantic.main.BaseModel.model_copy

model_copy() 方法允许复制模型(可选择更新),这在处理冻结模型时特别有用。

from pydantic import BaseModel


class BarModel(BaseModel):
    whatever: int


class FooBarModel(BaseModel):
    banana: float
    foo: str
    bar: BarModel


m = FooBarModel(banana=3.14, foo='hello', bar={'whatever': 123})

print(m.model_copy(update={'banana': 0}))
#> banana=0 foo='hello' bar=BarModel(whatever=123)

# normal copy gives the same object reference for bar:
print(id(m.bar) == id(m.model_copy().bar))
#> True
# deep copy gives a new object reference for `bar`:
print(id(m.bar) == id(m.model_copy(deep=True).bar))
#> False

泛型模型

Pydantic 支持创建泛型模型,以方便重用通用的模型结构。新的类型参数语法(由 Python 3.12 的 PEP 695 引入)和旧语法都支持(更多详情请参阅Python 文档)。

这是一个使用泛型 Pydantic 模型来创建易于重用的 HTTP 响应负载包装器的示例:

from typing import Generic, TypeVar

from pydantic import BaseModel, ValidationError

DataT = TypeVar('DataT')  # (1)!


class DataModel(BaseModel):
    number: int


class Response(BaseModel, Generic[DataT]):  # (2)!
    data: DataT  # (3)!


print(Response[int](data=1))
#> data=1
print(Response[str](data='value'))
#> data='value'
print(Response[str](data='value').model_dump())
#> {'data': 'value'}

data = DataModel(number=1)
print(Response[DataModel](data=data).model_dump())
#> {'data': {'number': 1}}
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    1 validation error for Response[int]
    data
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='value', input_type=str]
    """
  1. 声明一个或多个类型变量以用于参数化您的模型。
  2. 声明一个 Pydantic 模型,它继承自 BaseModeltyping.Generic(按此特定顺序),并将您之前声明的类型变量列表作为参数添加到 Generic 父类。
  3. 在您希望用其他类型替换它们的地方,使用类型变量作为注解。
from pydantic import BaseModel, ValidationError


class DataModel(BaseModel):
    number: int


class Response[DataT](BaseModel):  # (1)!
    data: DataT  # (2)!


print(Response[int](data=1))
#> data=1
print(Response[str](data='value'))
#> data='value'
print(Response[str](data='value').model_dump())
#> {'data': 'value'}

data = DataModel(number=1)
print(Response[DataModel](data=data).model_dump())
#> {'data': {'number': 1}}
try:
    Response[int](data='value')
except ValidationError as e:
    print(e)
    """
    1 validation error for Response[int]
    data
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='value', input_type=str]
    """
  1. 声明一个 Pydantic 模型,并将类型变量列表作为类型参数添加。
  2. 在您希望用其他类型替换它们的地方,使用类型变量作为注解。

警告

当用具体类型参数化模型时,如果类型变量有上限,Pydantic 不会验证所提供的类型是否可分配给该类型变量

泛型模型上设置的任何配置验证序列化逻辑也将应用于参数化的类,就像从模型类继承一样。任何自定义方法或属性也将被继承。

泛型模型也能与类型检查器正确集成,因此您可以获得所有您期望的类型检查,就像为每个参数化声明一个不同的类型一样。

注意

在内部,当泛型模型类被参数化时,Pydantic 会在运行时创建泛型模型的子类。这些类会被缓存,因此使用泛型模型引入的开销应该很小。

要从一个泛型模型继承并保留其泛型特性,子类也必须继承自 Generic

from typing import Generic, TypeVar

from pydantic import BaseModel

TypeX = TypeVar('TypeX')


class BaseClass(BaseModel, Generic[TypeX]):
    X: TypeX


class ChildClass(BaseClass[TypeX], Generic[TypeX]):
    pass


# Parametrize `TypeX` with `int`:
print(ChildClass[int](X=1))
#> X=1

您还可以创建一个模型的泛型子类,该子类部分或全部替换超类中的类型变量:

from typing import Generic, TypeVar

from pydantic import BaseModel

TypeX = TypeVar('TypeX')
TypeY = TypeVar('TypeY')
TypeZ = TypeVar('TypeZ')


class BaseClass(BaseModel, Generic[TypeX, TypeY]):
    x: TypeX
    y: TypeY


class ChildClass(BaseClass[int, TypeY], Generic[TypeY, TypeZ]):
    z: TypeZ


# Parametrize `TypeY` with `str`:
print(ChildClass[str, int](x='1', y='y', z='3'))
#> x=1 y='y' z=3

如果具体子类的名称很重要,您还可以通过覆盖 model_parametrized_name() 方法来覆盖默认的名称生成:

from typing import Any, Generic, TypeVar

from pydantic import BaseModel

DataT = TypeVar('DataT')


class Response(BaseModel, Generic[DataT]):
    data: DataT

    @classmethod
    def model_parametrized_name(cls, params: tuple[type[Any], ...]) -> str:
        return f'{params[0].__name__.title()}Response'


print(repr(Response[int](data=1)))
#> IntResponse(data=1)
print(repr(Response[str](data='a')))
#> StrResponse(data='a')

您可以将参数化的泛型模型用作其他模型中的类型:

from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar('T')


class ResponseModel(BaseModel, Generic[T]):
    content: T


class Product(BaseModel):
    name: str
    price: float


class Order(BaseModel):
    id: int
    product: ResponseModel[Product]


product = Product(name='Apple', price=0.5)
response = ResponseModel[Product](content=product)
order = Order(id=1, product=response)
print(repr(order))
"""
Order(id=1, product=ResponseModel[Product](content=Product(name='Apple', price=0.5)))
"""

在嵌套模型中使用相同的类型变量可以让你在模型的不同点强制执行类型关系:

from typing import Generic, TypeVar

from pydantic import BaseModel, ValidationError

T = TypeVar('T')


class InnerT(BaseModel, Generic[T]):
    inner: T


class OuterT(BaseModel, Generic[T]):
    outer: T
    nested: InnerT[T]


nested = InnerT[int](inner=1)
print(OuterT[int](outer=1, nested=nested))
#> outer=1 nested=InnerT[int](inner=1)
try:
    print(OuterT[int](outer='a', nested=InnerT(inner='a')))  # (1)!
except ValidationError as e:
    print(e)
    """
    2 validation errors for OuterT[int]
    outer
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
    nested.inner
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
    """
  1. OuterT 模型用 int 参数化,但在验证期间与 T 注解相关联的数据类型是 str,导致验证错误。

警告

虽然可能不会引发错误,但我们强烈建议不要在 isinstance() 检查中使用参数化的泛型。

例如,您不应该执行 isinstance(my_model, MyGenericModel[int])。但是,执行 isinstance(my_model, MyGenericModel) 是可以的(请注意,对于标准泛型,使用参数化泛型类进行子类检查会引发错误)。

如果您需要对参数化的泛型执行 isinstance() 检查,可以通过子类化参数化的泛型类来实现:

class MyIntModel(MyGenericModel[int]): ...

isinstance(my_model, MyIntModel)
实现细节

当使用嵌套的泛型模型时,Pydantic 有时会执行重新验证,以尝试产生最直观的验证结果。具体来说,如果您有一个类型为 GenericModel[SomeType] 的字段,并且您针对此字段验证像 GenericModel[SomeCompatibleType] 这样的数据,我们将检查数据,识别出输入数据是 GenericModel 的一种“松散”子类,并重新验证包含的 SomeCompatibleType 数据。

这增加了一些验证开销,但对于如下所示的情况,它使得事情更加直观。

from typing import Any, Generic, TypeVar

from pydantic import BaseModel

T = TypeVar('T')


class GenericModel(BaseModel, Generic[T]):
    a: T


class Model(BaseModel):
    inner: GenericModel[Any]


print(repr(Model.model_validate(Model(inner=GenericModel[int](a=1)))))
#> Model(inner=GenericModel[Any](a=1))

注意,如果您例如正在对 GenericModel[int] 进行验证,并传入一个实例 GenericModel[str](a='not an int'),验证仍然会失败。

还值得注意的是,这种模式也会重新触发任何自定义验证,比如额外的模型验证器等。验证器将在第一遍中被调用一次,直接对 GenericModel[Any] 进行验证。该验证失败,因为 GenericModel[int] 不是 GenericModel[Any] 的子类。这与上面关于在 isinstance()issubclass() 检查中使用参数化泛型的复杂性的警告有关。然后,验证器将在第二遍中再次被调用,在更宽松的强制重新验证阶段,这次会成功。为了更好地理解这一后果,请看下面:

from typing import Any, Generic, Self, TypeVar

from pydantic import BaseModel, model_validator

T = TypeVar('T')


class GenericModel(BaseModel, Generic[T]):
    a: T

    @model_validator(mode='after')
    def validate_after(self: Self) -> Self:
        print('after validator running custom validation...')
        return self


class Model(BaseModel):
    inner: GenericModel[Any]


m = Model.model_validate(Model(inner=GenericModel[int](a=1)))
#> after validator running custom validation...
#> after validator running custom validation...
print(repr(m))
#> Model(inner=GenericModel[Any](a=1))

未参数化类型变量的验证

当类型变量未被参数化时,Pydantic 对待泛型模型的方式类似于它对待内置泛型类型如 listdict

  • 如果类型变量被限定上界约束为特定类型,则将使用该类型。
  • 如果类型变量有默认类型(根据 PEP 696 指定),则将使用该类型。
  • 对于未限定上界或未约束的类型变量,Pydantic 将回退到 Any
from typing import Generic

from typing_extensions import TypeVar

from pydantic import BaseModel, ValidationError

T = TypeVar('T')
U = TypeVar('U', bound=int)
V = TypeVar('V', default=str)


class Model(BaseModel, Generic[T, U, V]):
    t: T
    u: U
    v: V


print(Model(t='t', u=1, v='v'))
#> t='t' u=1 v='v'

try:
    Model(t='t', u='u', v=1)
except ValidationError as exc:
    print(exc)
    """
    2 validation errors for Model
    u
      Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='u', input_type=str]
    v
      Input should be a valid string [type=string_type, input_value=1, input_type=int]
    """

警告

在某些情况下,对未参数化的泛型模型进行验证可能导致数据丢失。具体来说,如果正在使用类型变量上限、约束或默认值的子类型,并且模型未明确参数化,那么结果类型将不会是所提供的类型:

from typing import Generic, TypeVar

from pydantic import BaseModel

ItemT = TypeVar('ItemT', bound='ItemBase')


class ItemBase(BaseModel): ...


class IntItem(ItemBase):
    value: int


class ItemHolder(BaseModel, Generic[ItemT]):
    item: ItemT


loaded_data = {'item': {'value': 1}}


print(ItemHolder(**loaded_data))  # (1)!
#> item=ItemBase()

print(ItemHolder[IntItem](**loaded_data))  # (2)!
#> item=IntItem(value=1)
  1. 当泛型未参数化时,输入数据将根据 ItemT 的上限进行验证。鉴于 ItemBase 没有字段,item 字段的信息会丢失。
  2. 在这种情况下,类型变量被显式参数化,因此输入数据是根据 IntItem 类进行验证的。

未参数化类型变量的序列化

当使用具有上界约束或默认值的类型变量时,序列化的行为会有所不同:

如果在类型变量上界中使用了 Pydantic 模型,并且该类型变量从未被参数化,那么 Pydantic 将使用上界进行验证,但在序列化方面将该值视为 Any

from typing import Generic, TypeVar

from pydantic import BaseModel


class ErrorDetails(BaseModel):
    foo: str


ErrorDataT = TypeVar('ErrorDataT', bound=ErrorDetails)


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: ErrorDataT


class MyErrorDetails(ErrorDetails):
    bar: str


# serialized as Any
error = Error(
    message='We just had an error',
    details=MyErrorDetails(foo='var', bar='var2'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
        'bar': 'var2',
    },
}

# serialized using the concrete parametrization
# note that `'bar': 'var2'` is missing
error = Error[ErrorDetails](
    message='We just had an error',
    details=ErrorDetails(foo='var'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
    },
}

这里是上述行为的另一个例子,列举了关于上界规范和泛型类型参数化的所有排列组合:

from typing import Generic, TypeVar

from pydantic import BaseModel

TBound = TypeVar('TBound', bound=BaseModel)
TNoBound = TypeVar('TNoBound')


class IntValue(BaseModel):
    value: int


class ItemBound(BaseModel, Generic[TBound]):
    item: TBound


class ItemNoBound(BaseModel, Generic[TNoBound]):
    item: TNoBound


item_bound_inferred = ItemBound(item=IntValue(value=3))
item_bound_explicit = ItemBound[IntValue](item=IntValue(value=3))
item_no_bound_inferred = ItemNoBound(item=IntValue(value=3))
item_no_bound_explicit = ItemNoBound[IntValue](item=IntValue(value=3))

# calling `print(x.model_dump())` on any of the above instances results in the following:
#> {'item': {'value': 3}}

然而,如果使用了约束或默认值(根据 PEP 696),那么如果类型变量未被参数化,默认类型或约束将同时用于验证和序列化。您可以使用 SerializeAsAny 覆盖此行为:

from typing import Generic

from typing_extensions import TypeVar

from pydantic import BaseModel, SerializeAsAny


class ErrorDetails(BaseModel):
    foo: str


ErrorDataT = TypeVar('ErrorDataT', default=ErrorDetails)


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: ErrorDataT


class MyErrorDetails(ErrorDetails):
    bar: str


# serialized using the default's serializer
error = Error(
    message='We just had an error',
    details=MyErrorDetails(foo='var', bar='var2'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
    },
}
# If `ErrorDataT` was using an upper bound, `bar` would be present in `details`.


class SerializeAsAnyError(BaseModel, Generic[ErrorDataT]):
    message: str
    details: SerializeAsAny[ErrorDataT]


# serialized as Any
error = SerializeAsAnyError(
    message='We just had an error',
    details=MyErrorDetails(foo='var', bar='baz'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
        'bar': 'baz',
    },
}
from typing import Generic

from typing import TypeVar

from pydantic import BaseModel, SerializeAsAny


class ErrorDetails(BaseModel):
    foo: str


ErrorDataT = TypeVar('ErrorDataT', default=ErrorDetails)


class Error(BaseModel, Generic[ErrorDataT]):
    message: str
    details: ErrorDataT


class MyErrorDetails(ErrorDetails):
    bar: str


# serialized using the default's serializer
error = Error(
    message='We just had an error',
    details=MyErrorDetails(foo='var', bar='var2'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
    },
}
# If `ErrorDataT` was using an upper bound, `bar` would be present in `details`.


class SerializeAsAnyError(BaseModel, Generic[ErrorDataT]):
    message: str
    details: SerializeAsAny[ErrorDataT]


# serialized as Any
error = SerializeAsAnyError(
    message='We just had an error',
    details=MyErrorDetails(foo='var', bar='baz'),
)
assert error.model_dump() == {
    'message': 'We just had an error',
    'details': {
        'foo': 'var',
        'bar': 'baz',
    },
}

动态创建模型

API 文档

pydantic.main.create_model

在某些情况下,希望使用运行时信息来指定字段来创建模型。Pydantic 提供了 create_model() 函数以允许动态创建模型:

from pydantic import BaseModel, create_model

DynamicFoobarModel = create_model('DynamicFoobarModel', foo=str, bar=(int, 123))

# Equivalent to:


class StaticFoobarModel(BaseModel):
    foo: str
    bar: int = 123

字段定义通过关键字参数指定,并且应该是:

  • 单个元素,表示字段的类型注解。
  • 一个二元组,第一个元素是类型,第二个元素是赋的值(可以是默认值或 Field() 函数)。

这是一个更高级的例子:

from typing import Annotated

from pydantic import BaseModel, Field, PrivateAttr, create_model

DynamicModel = create_model(
    'DynamicModel',
    foo=(str, Field(alias='FOO')),
    bar=Annotated[str, Field(description='Bar field')],
    _private=(int, PrivateAttr(default=1)),
)


class StaticModel(BaseModel):
    foo: str = Field(alias='FOO')
    bar: Annotated[str, Field(description='Bar field')]
    _private: int = PrivateAttr(default=1)

特殊关键字参数 __config____base__ 可用于自定义新模型。这包括使用额外字段扩展基础模型。

from pydantic import BaseModel, create_model


class FooModel(BaseModel):
    foo: str
    bar: int = 123


BarModel = create_model(
    'BarModel',
    apple=(str, 'russet'),
    banana=(str, 'yellow'),
    __base__=FooModel,
)
print(BarModel)
#> <class '__main__.BarModel'>
print(BarModel.model_fields.keys())
#> dict_keys(['foo', 'bar', 'apple', 'banana'])

您还可以通过向 __validators__ 参数传递一个字典来添加验证器。

from pydantic import ValidationError, create_model, field_validator


def alphanum(cls, v):
    assert v.isalnum(), 'must be alphanumeric'
    return v


validators = {
    'username_validator': field_validator('username')(alphanum)  # (1)!
}

UserModel = create_model(
    'UserModel', username=(str, ...), __validators__=validators
)

user = UserModel(username='scolvin')
print(user)
#> username='scolvin'

try:
    UserModel(username='scolvi%n')
except ValidationError as e:
    print(e)
    """
    1 validation error for UserModel
    username
      Assertion failed, must be alphanumeric [type=assertion_error, input_value='scolvi%n', input_type=str]
    """
  1. 确保验证器名称不与任何字段名称冲突,因为在内部,Pydantic 会将所有成员收集到一个命名空间中,并使用 types 模块的工具来模仿类的正常创建过程。

注意

要对动态创建的模型进行 pickle 操作:

  • 模型必须在全局范围内定义
  • 必须提供 __module__ 参数

RootModel 和自定义根类型

API 文档

pydantic.root_model.RootModel

Pydantic 模型可以通过子类化 pydantic.RootModel 来定义具有“自定义根类型”。

根类型可以是 Pydantic 支持的任何类型,并由 RootModel 的泛型参数指定。根值可以通过第一个也是唯一的参数传递给模型的 __init__model_validate

以下是一个说明其工作原理的示例:

from pydantic import RootModel

Pets = RootModel[list[str]]
PetsByName = RootModel[dict[str, str]]


print(Pets(['dog', 'cat']))
#> root=['dog', 'cat']
print(Pets(['dog', 'cat']).model_dump_json())
#> ["dog","cat"]
print(Pets.model_validate(['dog', 'cat']))
#> root=['dog', 'cat']
print(Pets.model_json_schema())
"""
{'items': {'type': 'string'}, 'title': 'RootModel[list[str]]', 'type': 'array'}
"""

print(PetsByName({'Otis': 'dog', 'Milo': 'cat'}))
#> root={'Otis': 'dog', 'Milo': 'cat'}
print(PetsByName({'Otis': 'dog', 'Milo': 'cat'}).model_dump_json())
#> {"Otis":"dog","Milo":"cat"}
print(PetsByName.model_validate({'Otis': 'dog', 'Milo': 'cat'}))
#> root={'Otis': 'dog', 'Milo': 'cat'}

如果你想直接访问 root 字段中的项或遍历这些项,你可以实现自定义的 __iter____getitem__ 函数,如下例所示。

from pydantic import RootModel


class Pets(RootModel):
    root: list[str]

    def __iter__(self):
        return iter(self.root)

    def __getitem__(self, item):
        return self.root[item]


pets = Pets.model_validate(['dog', 'cat'])
print(pets[0])
#> dog
print([pet for pet in pets])
#> ['dog', 'cat']

您也可以直接创建参数化根模型的子类:

from pydantic import RootModel


class Pets(RootModel[list[str]]):
    def describe(self) -> str:
        return f'Pets: {", ".join(self.root)}'


my_pets = Pets.model_validate(['dog', 'cat'])

print(my_pets.describe())
#> Pets: dog, cat

伪不可变性

模型可以通过 model_config['frozen'] = True 配置为不可变的。设置此项后,尝试更改实例属性的值将引发错误。更多详情请参阅 API 参考

注意

此行为在 Pydantic V1 中通过配置设置 allow_mutation = False 实现。此配置标志在 Pydantic V2 中已弃用,并被 frozen 取代。

警告

在 Python 中,不可变性并非强制执行。开发者如果选择,有能力修改通常被认为是“不可变”的对象。

from pydantic import BaseModel, ConfigDict, ValidationError


class FooBarModel(BaseModel):
    model_config = ConfigDict(frozen=True)

    a: str
    b: dict


foobar = FooBarModel(a='hello', b={'apple': 'pear'})

try:
    foobar.a = 'different'
except ValidationError as e:
    print(e)
    """
    1 validation error for FooBarModel
    a
      Instance is frozen [type=frozen_instance, input_value='different', input_type=str]
    """

print(foobar.a)
#> hello
print(foobar.b)
#> {'apple': 'pear'}
foobar.b['apple'] = 'grape'
print(foobar.b)
#> {'apple': 'grape'}

尝试更改 a 导致了错误,并且 a 保持不变。然而,字典 b 是可变的,foobar 的不可变性并不能阻止 b 被更改。

抽象基类

Pydantic 模型可以与 Python 的抽象基类(ABC)一起使用。

import abc

from pydantic import BaseModel


class FooBarModel(BaseModel, abc.ABC):
    a: str
    b: int

    @abc.abstractmethod
    def my_abstract_method(self):
        pass

字段顺序

字段顺序以下列方式影响模型:

from pydantic import BaseModel, ValidationError


class Model(BaseModel):
    a: int
    b: int = 2
    c: int = 1
    d: int = 0
    e: float


print(Model.model_fields.keys())
#> dict_keys(['a', 'b', 'c', 'd', 'e'])
m = Model(e=2, a=1)
print(m.model_dump())
#> {'a': 1, 'b': 2, 'c': 1, 'd': 0, 'e': 2.0}
try:
    Model(a='x', b='x', c='x', d='x', e='x')
except ValidationError as err:
    error_locations = [e['loc'] for e in err.errors()]

print(error_locations)
#> [('a',), ('b',), ('c',), ('d',), ('e',)]

自动排除的属性

类变量

使用 ClassVar 注解的属性被 Pydantic 正确地视为类变量,并且不会成为模型实例上的字段:

from typing import ClassVar

from pydantic import BaseModel


class Model(BaseModel):
    x: ClassVar[int] = 1

    y: int = 2


m = Model()
print(m)
#> y=2
print(Model.x)
#> 1

私有模型属性

API 文档

pydantic.fields.PrivateAttr

名称带有前导下划线的属性不被 Pydantic 视为字段,并且不包含在模型模式中。相反,这些属性被转换为“私有属性”,在调用 __init__model_validate 等期间不会被验证甚至设置。

以下是一个使用示例:

from datetime import datetime
from random import randint
from typing import Any

from pydantic import BaseModel, PrivateAttr


class TimeAwareModel(BaseModel):
    _processed_at: datetime = PrivateAttr(default_factory=datetime.now)
    _secret_value: str

    def model_post_init(self, context: Any) -> None:
        # this could also be done with `default_factory`:
        self._secret_value = randint(1, 5)


m = TimeAwareModel()
print(m._processed_at)
#> 2032-01-02 03:04:05.000006
print(m._secret_value)
#> 3

私有属性名称必须以下划线开头,以防止与模型字段冲突。然而,双下划线名称(如 __attr__)不受支持,并将从模型定义中完全忽略。

模型签名

所有 Pydantic 模型都将根据其字段生成签名:

import inspect

from pydantic import BaseModel, Field


class FooModel(BaseModel):
    id: int
    name: str = None
    description: str = 'Foo'
    apple: int = Field(alias='pear')


print(inspect.signature(FooModel))
#> (*, id: int, name: str = None, description: str = 'Foo', pear: int) -> None

准确的签名对于内省目的以及像 FastAPIhypothesis 这样的库很有用。

生成的签名也会尊重自定义的 __init__ 函数:

import inspect

from pydantic import BaseModel


class MyModel(BaseModel):
    id: int
    info: str = 'Foo'

    def __init__(self, id: int = 1, *, bar: str, **data) -> None:
        """My custom init!"""
        super().__init__(id=id, bar=bar, **data)


print(inspect.signature(MyModel))
#> (id: int = 1, *, bar: str, info: str = 'Foo') -> None

要包含在签名中,字段的别名或名称必须是有效的 Python 标识符。在生成签名时,Pydantic 会优先使用字段的别名而非其名称,但如果别名不是有效的 Python 标识符,则可能使用字段名称。

如果一个字段的别名和名称不是有效的标识符(这可能通过奇特地使用 create_model 实现),则会添加一个 **data 参数。此外,如果 model_config['extra'] == 'allow'**data 参数将始终存在于签名中。

结构化模式匹配

Pydantic 支持模型的结构化模式匹配,这是由 Python 3.10 中的 PEP 636 引入的。

from pydantic import BaseModel


class Pet(BaseModel):
    name: str
    species: str


a = Pet(name='Bones', species='dog')

match a:
    # match `species` to 'dog', declare and initialize `dog_name`
    case Pet(species='dog', name=dog_name):
        print(f'{dog_name} is a dog')
#> Bones is a dog
    # default case
    case _:
        print('No dog matched')

注意

match-case 语句可能看起来像是在创建一个新模型,但不要被迷惑;它只是获取属性并进行比较或声明和初始化的语法糖。

属性复制

在许多情况下,传递给构造函数的参数将被复制,以便执行验证和在必要时进行强制转换。

在此示例中,请注意列表的 ID 在类构造后发生了变化,因为它在验证期间被复制了:

from pydantic import BaseModel


class C1:
    arr = []

    def __init__(self, in_arr):
        self.arr = in_arr


class C2(BaseModel):
    arr: list[int]


arr_orig = [1, 9, 10, 3]


c1 = C1(arr_orig)
c2 = C2(arr=arr_orig)
print(f'{id(c1.arr) == id(c2.arr)=}')
#> id(c1.arr) == id(c2.arr)=False

注意

在某些情况下,Pydantic 不会复制属性,例如在传递模型时——我们按原样使用模型。您可以通过设置 model_config['revalidate_instances'] = 'always' 来覆盖此行为。