Skip to content

模型

API 文档

pydantic.main.BaseModel

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

你可以将模型视为类似于C语言中的结构体,或者API中单个端点的需求。

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

不受信任的数据可以传递给模型,在解析和验证后,Pydantic保证结果模型实例的字段将符合模型上定义的字段类型。

验证——一个故意的误称

TL;DR

我们使用术语“验证”来指代实例化符合指定类型和约束的模型(或其他类型)的过程。这项任务,Pydantic以其闻名,在口语中最广泛地被称为“验证”,尽管在其他上下文中“验证”一词可能更具限制性。


详细说明

关于“验证”一词的潜在混淆源于严格来说,Pydantic的主要关注点并不完全符合“验证”的字典定义:

validation

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

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

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

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

虽然“解析”和“验证”这两个术语以前可以互换使用,但今后我们旨在专门使用“验证”,而“解析”专门保留用于与JSON解析相关的讨论。

基本模型用法

Note

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,是一个整数且是必需的
  • name,是一个字符串且不是必需的(它有默认值)。

可以使用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. 初始化user时没有设置name,所以使用了默认值。 可以检查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

Warning

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

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

from typing import Optional

from pydantic import BaseModel


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


m = Boo(int=123)  # 验证将失败。

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

模型方法和属性

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

Note

参见BaseModel的API文档,包括方法和属性的完整列表。

Tip

有关从Pydantic V1更改的详细信息,请参阅迁移指南中的pydantic.BaseModel的更改

数据转换

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__以提供额外字段的验证。

更多详情,请参阅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`未完全定义;你应该先定义`Bar`,然后调用`Foo.model_rebuild()`。

    更多信息请访问 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. Bar在创建Foo类时尚未定义。因此,使用了前向注解

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
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='bad', input_type=str]
    a_float
      输入应为有效数字,无法将字符串解析为数字 [type=float_parsing, input_value='not a float', input_type=str]
    """

验证数据

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

  • model_validate(): 这与模型的__init__方法非常相似,只是它接受字典或对象而不是关键字参数。如果传递的对象无法验证,或者它不是相关模型的字典或实例,将引发ValidationError
  • model_validate_json(): 这将提供的字符串或bytes对象作为JSON数据进行验证。如果你的传入数据是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
      输入应为有效字典或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
      输入应为有效字符串 [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
      无效JSON:第1行第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
      输入应为有效日期时间,无效的日期时间分隔符,期望`T`、`t`、`_`或空格 [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
      输入应为有效字典或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
      输入应为有效字符串 [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
      无效JSON:第1行第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
      输入应为有效日期时间,无效的日期时间分隔符,期望`T`、`t`、`_`或空格 [type=datetime_parsing, input_value='2024-04-01', input_type=str]
    """

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

Note

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

Note

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

from pydantic import BaseModel


class Model(BaseModel):
    a: int


m = Model(a=0)
# 注意:在配置中将`validate_assignment`设置为`True`可以防止这种不当行为。
m.a = 'not an int'

# 即使m无效,也不会引发验证错误
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)
# 注意:在配置中将`validate_assignment`设置为`True`可以防止这种不当行为。
m.a = 'not an int'

try:
    m2 = Model.model_validate(m)
except ValidationError as e:
    print(e)
    """
    1 validation error for Model
    a
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='not an int', input_type=str]
    """

无验证创建模型

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

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

Warning

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

Note

在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)

# 普通复制为bar提供相同的对象引用:
print(id(m.bar) == id(m.model_copy().bar))
#> True
# 深度复制为`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
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='value', input_type=str]
    """
  1. 声明一个或多个类型变量以用于参数化你的模型。
  2. 声明一个继承自BaseModeltyping.Generic(按此特定顺序)的Pydantic模型,并将你之前声明的类型变量列表作为参数添加到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
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='value', input_type=str]
    """
  1. 声明一个Pydantic模型,并将类型变量列表作为类型参数添加。
  2. 使用类型变量作为注解,你希望用其他类型替换它们的地方。

Warning

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

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

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

Note

在内部,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


# 用`int`参数化`TypeX`:
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


# 用`str`参数化`TypeY`:
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
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='a', input_type=str]
    nested.inner
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='a', input_type=str]
    """
  1. OuterT模型用int参数化,但验证期间与T注解关联的数据是str类型,导致验证错误。

Warning

虽然它可能不会引发错误,但我们强烈建议不要在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
      输入应为有效整数,无法将字符串解析为整数 [type=int_parsing, input_value='u', input_type=str]
    v
      输入应为有效字符串 [type=string_type, input_value=1, input_type=int]
    """

Warning

在某些情况下,针对未参数化泛型模型的验证可能导致数据丢失。具体来说,如果正在使用类型变量的上界、约束或默认的子类型,并且模型未被显式参数化,结果类型将不是提供的类型:

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


# 序列化为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',
    },
}

# 使用具体参数化序列化
# 注意`'bar': 'var2'`缺失
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))

# 在任何上述实例上调用`print(x.model_dump())`的结果如下:
#> {'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


# 使用默认的序列化器序列化
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',
    },
}
# 如果`ErrorDataT`使用上界,`bar`将出现在`details`中。


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


# 序列化为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


# 使用默认的序列化器序列化
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',
    },
}
# 如果`ErrorDataT`使用上界,`bar`将出现在`details`中。


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


# 序列化为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))

# 等同于:


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(), '必须为字母数字'
    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
      断言失败,必须为字母数字 [type=assertion_error, input_value='scolvi%n', input_type=str]
    """
  1. 确保验证器名称不与任何字段名称冲突,因为在内部,Pydantic将所有成员收集到一个命名空间中,并使用types模块工具模拟类的正常创建。

Note

要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参考

Note

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

Warning

在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
      实例被冻结 [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的抽象基类(ABCs)一起使用。

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:
        # 这也可以用`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:
        """我的自定义初始化!"""
        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:
    # 将`species`匹配到'dog',声明并初始化`dog_name`
    case Pet(species='dog', name=dog_name):
        print(f'{dog_name} is a dog')
#> Bones is a dog
    # 默认情况
    case _:
        print('No dog matched')

Note

匹配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

Note

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