模型
API 文档
在 Pydantic 中定义模式的主要方式之一是通过模型。模型是继承自 BaseModel
并将字段定义为带注解属性的类。
您可以将模型看作类似于 C 等语言中的结构体,或者看作 API 中单个端点的需求。
模型与 Python 的 dataclasses 有许多相似之处,但在设计上存在一些微妙而重要的差异,这些差异简化了与验证、序列化和 JSON schema 生成相关的某些工作流程。您可以在文档的数据类部分找到更多相关讨论。
可以将不可信的数据传递给模型,经过解析和验证后,Pydantic 保证生成的模型实例的字段将符合模型上定义的字段类型。
验证 — 一个故意用词不当的术语
摘要
我们使用“验证”这个术语来指代实例化一个遵循指定类型和约束的模型(或其他类型)的过程。这项任务,Pydantic 因此而闻名,在口语中被最广泛地认知为“验证”,尽管在其他语境下,“验证”一词的含义可能更为狭窄。
长篇解释
围绕“验证”一词的潜在混淆源于这样一个事实:严格来说,Pydantic 的主要关注点与“验证”的字典定义并不完全一致。
验证
名词 检查或证明某事物的有效性或准确性的行为。
在 Pydantic 中,“验证”一词指的是实例化一个遵循指定类型和约束的模型(或其他类型)的过程。Pydantic 保证的是输出的类型和约束,而不是输入数据。当考虑到 Pydantic 的 ValidationError
是在数据无法成功解析为模型实例时引发的,这一区别就变得显而易见了。
虽然这个区别最初看起来可能很微妙,但它具有实际意义。在某些情况下,“验证”不仅仅是创建模型,还可能包括数据的复制和强制转换。这可能涉及到复制传递给构造函数的参数,以便在不改变原始输入数据的情况下将其强制转换为新类型。要更深入地了解这对您的使用的影响,请参阅下面的数据转换和属性复制部分。
本质上,Pydantic 的主要目标是确保经过处理(称为“验证”)后的结果结构完全符合应用的类型提示。鉴于“验证”作为这一过程的口语术语已被广泛采用,我们将在文档中持续使用它。
虽然“解析”和“验证”这两个术语以前可以互换使用,但从现在起,我们旨在专门使用“验证”,而“解析”将专门用于讨论JSON 解析。
模型基本用法¶
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
id: int
name: str = 'Jane Doe'
model_config = ConfigDict(str_max_length=10) # (1)!
在此示例中,User
是一个具有两个字段的模型:
关于类型的文档扩展了支持的类型。
可以使用 Field()
函数以多种方式自定义字段。有关更多信息,请参阅关于字段的文档。
然后可以实例化该模型:
user = User(id='123')
user
是 User
的一个实例。对象的初始化将执行所有解析和验证。如果没有引发 ValidationError
异常,您就知道生成的模型实例是有效的。
模型的字段可以像普通属性一样访问 user
对象:
assert user.name == 'Jane Doe' # (1)!
assert user.id == 123 # (2)!
assert isinstance(user.id, int)
name
在user
初始化时未设置,因此使用了默认值。可以检查model_fields_set
属性来查看在实例化期间显式设置的字段名称。- 请注意,字符串
'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
,从而导致验证错误。
模型方法和属性¶
上面的示例仅展示了模型功能的冰山一角。模型具有以下方法和属性:
model_validate()
:根据 Pydantic 模型验证给定的对象。请参见验证数据。model_validate_json()
:根据 Pydantic 模型验证给定的 JSON 数据。请参见验证数据。model_construct()
:在不运行验证的情况下创建模型。请参见创建不经验证的模型。model_dump()
:返回一个包含模型字段和值的字典。请参见序列化。model_dump_json()
:返回model_dump()
的 JSON 字符串表示。请参见序列化。model_copy()
:返回模型的副本(默认为浅拷贝)。请参见模型复制。model_json_schema()
:返回一个可 JSON 化的字典,表示模型的 JSON Schema。请参见JSON Schema。model_fields
:字段名称与其定义(FieldInfo
实例)之间的映射。model_computed_fields
:计算字段名称与其定义(ComputedFieldInfo
实例)之间的映射。model_extra
:验证期间设置的额外字段。model_fields_set
:模型初始化时明确提供的字段集合。model_parametrized_name()
:计算泛型类参数化的类名。model_post_init()
:在模型实例化并应用所有字段验证器后执行附加操作。model_rebuild()
:重建模型模式,也支持构建递归泛型模型。请参见重建模型模式。
注意
请参阅 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]
- 在这种情况下,您可能会想使用抽象的
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'}
- 如果
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',
}
"""
- 在创建
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_validate
和 model_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()
不会因存在与字段不对应的数据而引发错误。相反,所述输入数据将被简单地忽略。
模型复制¶
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]
"""
- 声明一个或多个类型变量以用于参数化您的模型。
- 声明一个 Pydantic 模型,它继承自
BaseModel
和typing.Generic
(按此特定顺序),并将您之前声明的类型变量列表作为参数添加到Generic
父类。 - 在您希望用其他类型替换它们的地方,使用类型变量作为注解。
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]
"""
- 声明一个 Pydantic 模型,并将类型变量列表作为类型参数添加。
- 在您希望用其他类型替换它们的地方,使用类型变量作为注解。
警告
当用具体类型参数化模型时,如果类型变量有上限,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]
"""
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 对待泛型模型的方式类似于它对待内置泛型类型如 list
和 dict
:
- 如果类型变量被限定上界或约束为特定类型,则将使用该类型。
- 如果类型变量有默认类型(根据 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)
- 当泛型未参数化时,输入数据将根据
ItemT
的上限进行验证。鉴于ItemBase
没有字段,item
字段的信息会丢失。 - 在这种情况下,类型变量被显式参数化,因此输入数据是根据
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 提供了 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]
"""
- 确保验证器名称不与任何字段名称冲突,因为在内部,Pydantic 会将所有成员收集到一个命名空间中,并使用
types
模块的工具来模仿类的正常创建过程。
注意
要对动态创建的模型进行 pickle 操作:
- 模型必须在全局范围内定义
- 必须提供
__module__
参数
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
字段顺序¶
字段顺序以下列方式影响模型:
- 字段顺序在模型的JSON Schema中保留
- 字段顺序在验证错误中保留
- 字段顺序在序列化数据时保留
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 视为字段,并且不包含在模型模式中。相反,这些属性被转换为“私有属性”,在调用 __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
准确的签名对于内省目的以及像 FastAPI
或 hypothesis
这样的库很有用。
生成的签名也会尊重自定义的 __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'
来覆盖此行为。