モデル
API 文档
在 Pydantic 中定义模式的主要方法之一是通过模型。模型只是从 pydantic.BaseModel
继承并将字段定义为注释属性的类。
你可以将模型视为类似于 C 语言中的结构体,或者视为 API 中的单个端点的要求。
模型与 Python 的数据类有很多相似之处,但经过了一些微妙但重要的设计差异,这些差异简化了与验证、序列化和 JSON 模式生成相关的某些工作流程。你可以在文档的“数据类”部分找到对此的更多讨论。
不可信的数据可以传递给模型,并且在解析和验证后,Pydantic 保证结果模型实例的字段将符合模型上定义的字段类型。
注意
“验证——故意的用词不当”### 简而言之
We use the term "validation" to refer to the process of instantiating a model (or other type) that adheres to specified types and constraints. This task, which Pydantic is well known for, is most widely recognized as "validation" in colloquial terms, even though in other contexts the term "validation" may be more restrictive.
The long version¶
The potential confusion around the term "validation" arises from the fact that, strictly speaking, Pydantic's primary focus doesn't align precisely with the dictionary definition of "validation":
validation¶
noun the action of checking or proving the validity or accuracy of something.
In Pydantic, the term "validation" refers to the process of instantiating a model (or other type) that adheres to specified
types and constraints. Pydantic guarantees the types and constraints of the output, not the input data.
This distinction becomes apparent when considering that Pydantic's ValidationError
is raised
when data cannot be successfully parsed into a model instance.
While this distinction may initially seem subtle, it holds practical significance. In some cases, "validation" goes beyond just model creation, and can include the copying and coercion of data. This can involve copying arguments passed to the constructor in order to perform coercion to a new type without mutating the original input data. For a more in-depth understanding of the implications for your usage, refer to the Data Conversion and Attribute Copies sections below.
In essence, Pydantic's primary goal is to assure that the resulting structure post-processing (termed "validation") precisely conforms to the applied type hints. Given the widespread adoption of "validation" as the colloquial term for this process, we will consistently use it in our documentation.
While the terms "parse" and "validation" were previously used interchangeably, moving forward, we aim to exclusively employ "validate", with "parse" reserved specifically for discussions related to JSON parsing.
基本模型用法¶
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str = 'Jane Doe'
在这个例子中, User
是一个有两个字段的模型:
-
id
,这是一个整数,是必需的 -
name
,这是一个字符串,不是必需的(它有一个默认值)。user = User(id='123')
在这个例子中, user
是 User
的一个实例。对象的初始化将执行所有的解析和验证。如果没有抛出 ValidationError
,你就知道生成的模型实例是有效的。
assert user.id == 123
assert isinstance(user.id, int)
# Note that '123' was coerced to an int and its value is 123
有关 pydantic 的强制逻辑的更多详细信息可以在数据转换中找到。模型的字段可以像 user
对象的普通属性一样访问。根据字段类型,字符串 '123'
已转换为整数。
assert user.name == 'Jane Doe'
当 user
被初始化时, name
没有被设置,所以它有默认值。
assert user.model_fields_set == {'id'}
用户初始化时提供的字段。
assert user.model_dump() == {'id': 123, 'name': 'Jane Doe'}
或者 .model_dump()
或 dict(user)
将提供一个字段字典,但 .model_dump()
可以接受许多其他参数。(请注意, dict(user)
不会递归地将嵌套模型转换为字典,但 .model_dump()
会。)
user.id = 321
assert user.id == 321
默认情况下,模型是可变的,可以通过属性赋值更改字段值。
模型方法和属性¶
上述示例仅展示了模型能够完成的工作的冰山一角。模型具有以下方法和属性:
-
模型计算字段:该模型实例的计算字段字典。
-
model_construct()
: 用于在不运行验证的情况下创建模型的类方法。请参阅不进行验证创建模型。 -
model_copy()
: 返回模型的副本(默认情况下,浅拷贝)。请参阅序列化。 -
model_dump()
: 返回模型字段和值的字典。请参阅序列化。 -
model_dump_json()
: 返回model_dump()
的 JSON 字符串表示形式。请参阅序列化。 -
model_extra
: 获取在验证期间设置的其他字段。 -
[ 模型字段集 ]:模型实例初始化时设置的字段集。
-
model_json_schema()
: 返回一个可 JSON 序列化的字典,表示模型为 JSON Schema。请参阅 JSON Schema。 -
model_parametrized_name()
:计算泛型类参数化的类名。 -
model_post_init()
: 在模型初始化后执行其他初始化操作。 -
model_rebuild()
: 重建模型模式,它也支持构建递归泛型模型。请参阅重建模型模式。 -
model_validate()
: 用于将任何对象加载到模型中的实用程序。请参阅辅助功能。 -
model_validate_json()
:用于根据 Pydantic 模型验证给定 JSON 数据的实用程序。请参阅辅助函数。
注意
请参阅 BaseModel
以获取类定义,包括所有方法和属性的完整列表。
提示
请参阅迁移指南中对 pydantic.BaseModel
的更改,以获取 Pydantic V1 以来的更改详细信息。
嵌套模型¶
可以使用模型本身作为注释中的类型来定义更复杂的层次数据结构。
from typing import List, 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'}],
}
"""
对于自引用模型,请参见推迟的注释。
注意
在定义模型时,要注意字段名与其类型、之前定义的模型或导入的库之间的命名冲突。
For example, the following would yield a validation error:
from typing import Optional
from pydantic import BaseModel
class Boo(BaseModel):
int: Optional[int] = None
m = Boo(int=123) # errors
int
is set to a default value of None
and has the exact same name as its type, so both are interpreted to be None
.
重建模型模式¶
可以使用 model_rebuild()
重建模型模式。这对于构建递归通用模型很有用。
from pydantic import BaseModel, PydanticUserError
class Foo(BaseModel):
x: 'Bar'
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',
}
"""
Pydantic 尝试自动确定何时需要这样做,如果没有这样做,它会报错,但在处理递归模型或泛型时,您可能希望主动调用 model_rebuild()
。
在 V2 中,[model_rebuild()
][
pydantic.main.BaseModel.model_rebuild
] 取代了 V1 中的 [update_forward_refs()
]。新行为有一些细微的差异。最大的变化是,在最外层模型上调用 [
model_rebuild()
][
pydantic.main.BaseModel.model_rebuild
] 时,它会构建一个用于验证整个模型(嵌套模型和所有模型)的核心模式,因此在调用 [model_rebuild()
][
pydantic.main.BaseModel.model_rebuild
] 之前,所有类型都需要准备好。
任意类实例¶
(原名“ORM 模式”/ from_orm
。)
Pydantic 模型也可以通过读取与模型字段名称相对应的实例属性来从任意类实例创建。此功能的一个常见应用是与对象关系映射(ORM)集成。
为此,请设置 config 属性 model_config['from_attributes'] = True
。有关更多信息,请参阅 模型配置 和 ConfigDict。
这里的示例使用了 SQLAlchemy,但同样的方法应该适用于任何 ORM。
from typing import List
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import declarative_base
from typing_extensions import Annotated
from pydantic import BaseModel, ConfigDict, StringConstraints
Base = declarative_base()
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False)
public_key = Column(String(20), index=True, nullable=False, unique=True)
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
class CompanyModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
public_key: Annotated[str, StringConstraints(max_length=20)]
name: Annotated[str, StringConstraints(max_length=63)]
domains: List[Annotated[str, StringConstraints(max_length=255)]]
co_orm = CompanyOrm(
id=123,
public_key='foobar',
name='Testing',
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' name='Testing' domains=['example.com', 'foobar.com']
"""
保留名称¶
你可能想根据保留的 SQLAlchemy 字段给 Column
命名。在这种情况下, Field
别名会很方便:
import typing
import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
from pydantic import BaseModel, ConfigDict, Field
class MyModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
metadata: typing.Dict[str, str] = Field(alias='metadata_')
Base = declarative_base()
class SQLModel(Base):
__tablename__ = 'my_table'
id = sa.Column('id', sa.Integer, primary_key=True)
# 'metadata' is reserved by SQLAlchemy, hence the '_'
metadata_ = sa.Column('metadata', sa.JSON)
sql_model = SQLModel(metadata_={'key': 'val'}, id=1)
pydantic_model = MyModel.model_validate(sql_model)
print(pydantic_model.model_dump())
#> {'metadata': {'key': 'val'}}
print(pydantic_model.model_dump(by_alias=True))
#> {'metadata_': {'key': 'val'}}
注意
上面的示例之所以有效,是因为别名在字段填充方面优先于字段名称。访问 SQLModel
的 metadata
属性将导致一个 ValidationError
。
嵌套属性¶
当使用属性来解析模型时,模型实例将根据需要从顶级属性和更深嵌套的属性中创建。
以下是一个示例,演示了该原理:
from typing import List
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
。
无论发现多少个错误,都会引发一个类型为 ValidationError
的单个异常,而 ValidationError
将包含有关所有错误及其发生方式的信息。
请参阅错误处理,以获取有关标准和自定义错误的详细信息。
作为一个示范:
from typing import List
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 在模型上提供了三个用于解析数据的 classmethod
帮助函数:
-
model_validate()
: 这与模型的__init__
方法非常相似,只是它接受一个字典或对象而不是关键字参数。如果传递的对象无法验证,或者它不是要验证的模型的字典或实例,则会引发ValidationError
。 -
model_validate_json()
: 此方法接受一个 str 或 bytes,并将其解析为 json,然后将结果传递给model_validate()
。 -
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]
"""
如果你想验证不是 JSON 格式的序列化数据,你应该自己将数据加载到字典中,然后将其传递给 model_validate
。
注意
根据涉及的类型和模型配置, model_validate
和 model_validate_json
可能具有不同的验证行为。如果您的数据来自非 JSON 源,但希望获得与 model_validate_json
相同的验证行为和错误,我们目前的建议是使用 model_validate_json(json.dumps(data))
,或者如果数据采用具有字符串键和值的(可能嵌套的)字典形式,则使用 model_validate_strings
。
注意
在文档的 JSON 部分了解更多关于 JSON 解析的信息。
注意
如果您正在将模型的实例传递给 model_validate
,则需要考虑在模型的配置中设置 revalidate_instances
。如果您不设置此值,则将跳过模型实例的验证。请参见下面的示例:
:x: revalidate_instances='never'
" ```py from pydantic import BaseModel
class Model(BaseModel):
a: int
m = Model(a=0)
# note: the `model_config` setting validate_assignment=True` 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)
```
" revalidate_instances='always'
" ```py from pydantic import BaseModel, ConfigDict, ValidationError
class Model(BaseModel):
a: int
model_config = ConfigDict(revalidate_instances='always')
m = Model(a=0)
# note: the `model_config` setting validate_assignment=True` 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()
方法,该方法允许在不进行验证的情况下创建模型。这在至少几种情况下可能很有用:
-
当处理已知有效的复杂数据(出于性能原因)时
-
当一个或多个验证器函数不是幂等的,或者
-
当一个或多个验证器函数有你不想触发的副作用时。
注意
在 Pydantic V2 中, BaseModel.__init__
和 BaseModel.model_construct
之间的性能差距已经大大缩小。对于简单的模型,调用 BaseModel.__init__
甚至可能更快。如果您出于性能原因正在使用 model_construct()
,在假定 model_construct()
更快之前,您可能需要对您的用例进行分析。
警告
model_construct()
不进行任何验证,这意味着它可以创建无效的模型。您应该只在已经验证过的数据或您绝对信任的数据上使用 model_construct()
方法。
from pydantic import BaseModel
class User(BaseModel):
id: int
age: int
name: str = 'John Doe'
original_user = User(id=123, age=32)
user_data = original_user.model_dump()
print(user_data)
#> {'id': 123, 'age': 32, 'name': 'John Doe'}
fields_set = original_user.model_fields_set
print(fields_set)
#> {'age', 'id'}
# ...
# pass user_data and fields_set to RPC or save to the database etc.
# ...
# you can then create a new instance of User without
# re-running validation which would be unnecessary at this point:
new_user = User.model_construct(_fields_set=fields_set, **user_data)
print(repr(new_user))
#> User(id=123, age=32, name='John Doe')
print(new_user.model_fields_set)
#> {'age', 'id'}
# construct can be dangerous, only use it with validated data!:
bad_user = User.model_construct(id='dog')
print(repr(bad_user))
#> User(id='dog', name='John Doe')
@pydantic.main.BaseModel.model_construct 的 _fields_set
关键字参数是可选的,但它允许你更精确地了解哪些字段最初被设置,哪些没有。如果省略了它,@pydantic.main.BaseModel.model_fields_set 就只是提供的数据的键。
例如,在上例中,如果没有提供 _fields_set
,则 new_user.model_fields_set
将是 {'id', 'age', 'name'}
。
请注意,对于 RootModel
的子类,可以将根值传递给 model_construct()
的位置,而不是使用关键字参数。
以下是关于 model_construct()
行为的一些其他注意事项:
-
当我们说“没有进行验证”时——这包括将字典转换为模型实例。因此,如果您有一个字段类型为
Model
,那么在将内部字典传递给model_construct()
之前,您需要自己将其转换为模型。-
特别是,
model_construct()
方法不支持从字典递归构建模型。
-
-
如果不为具有默认值的字段传递关键字参数,则仍将使用默认值。
-
对于具有私有属性的模型,将像调用
__init__
时一样初始化__pydantic_private__
字典。 -
当使用
model_construct()
构建实例时,不会调用模型或其任何父类中的__init__
方法,即使定义了自定义__init__
方法也是如此。
注意
“关于 extra
对 model_construct
的行为” * 对于具有 model_config['extra'] == 'allow'
的模型,与字段不对应的数据将正确存储在 __pydantic_extra__
字典中,并保存到模型的 __dict__
中。 * 对于具有 model_config['extra'] == 'ignore'
的模型,与字段不对应的数据将被忽略-即不会存储在 __pydantic_extra__
或 __dict__
实例上。 * 与对 __init__
的调用不同,对具有 model_config['extra'] == 'forbid'
的 model_construct
的调用在存在与字段不对应的数据时不会引发错误。相反,将简单地忽略所述输入数据。
通用模型¶
Pydantic 支持创建泛型模型,以更轻松地重用通用模型结构。
为了声明泛型模型,你执行以下步骤:
-
声明一个或多个
typing.TypeVar
实例来用于参数化模型。 -
声明一个从
pydantic.BaseModel
和typing.Generic
继承的 pydantic 模型,其中你将TypeVar
实例作为参数传递给typing.Generic
。 -
使用
TypeVar
实例作为注释,在需要用其他类型或 pydantic 模型替换它们的地方使用。
以下是一个使用通用的 BaseModel
子类创建易于重用的 HTTP 响应有效负载包装器的示例:
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel, ValidationError
DataT = TypeVar('DataT')
class DataModel(BaseModel):
numbers: List[int]
people: List[str]
class Response(BaseModel, Generic[DataT]):
data: Optional[DataT] = None
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(numbers=[1, 2, 3], people=[])
print(Response[DataModel](data=data).model_dump())
#> {'data': {'numbers': [1, 2, 3], 'people': []}}
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]
"""
如果你在泛型模型定义中设置了model_config
或使用了
@field_validator
或其他 Pydantic 装饰器,它们将以与从
BaseModel
子类继承时相同的方式应用于参数化子类。你在泛型类上定义的任何方法也将被继承。
Pydantic 的泛型也与类型检查器正确集成,因此如果为每个参数化声明一个不同的类型,您将获得您所期望的所有类型检查。
注意
在内部,当泛型模型被参数化时,Pydantic 在运行时创建 BaseModel
的子类。这些类被缓存,因此使用泛型模型引入的开销应该很小。
要从泛型模型继承并保留它是泛型的事实,子类也必须从 typing.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]):
# Inherit from Generic[TypeX]
pass
# Replace TypeX by int
print(ChildClass[int](X=1))
#> X=1
你还可以创建一个 BaseModel
的泛型子类,该子类部分或完全替换了父类中的类型参数:
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
# Replace TypeY by str
print(ChildClass[str, int](x='1', y='y', z='3'))
#> x=1 y='y' z=3
如果具体子类的名称很重要,也可以覆盖默认的名称生成:
from typing import Any, Generic, Tuple, Type, 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)))
"""
提示
当在另一个模型中使用参数化泛型模型作为类型(如 product: ResponseModel[Product]
)时,请确保在初始化模型实例时对该泛型模型进行参数化(如 response = ResponseModel[Product](content=product)
)。如果不这样做,将引发 ValidationError
,因为 Pydantic 不会根据传递给它的数据推断泛型模型的类型。
在嵌套模型中使用相同的 TypeVar
可以在模型的不同点强制实施类型关系:
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:
nested = InnerT[str](inner='a')
print(OuterT[int](outer='a', nested=nested))
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
Input should be a valid dictionary or instance of InnerT[int] [type=model_type, input_value=InnerT[str](inner='a'), input_type=InnerT[str]]
"""
当使用绑定类型参数时,以及当不指定类型参数时,Pydantic 对泛型模型的处理类似于对内置泛型类型如 List
和 Dict
的处理:
-
如果在实例化泛型模型之前没有指定参数,它们将被验证为
TypeVar
的绑定。 -
如果涉及的
TypeVar
没有界限,则将其视为Any
。
此外,与 List
和 Dict
一样,使用 TypeVar
指定的任何参数稍后都可以替换为具体类型:
from typing import Generic, TypeVar
from pydantic import BaseModel, ValidationError
AT = TypeVar('AT')
BT = TypeVar('BT')
class Model(BaseModel, Generic[AT, BT]):
a: AT
b: BT
print(Model(a='a', b='a'))
#> a='a' b='a'
IntT = TypeVar('IntT', bound=int)
typevar_model = Model[int, IntT]
print(typevar_model(a=1, b=1))
#> a=1 b=1
try:
typevar_model(a='a', b='a')
except ValidationError as exc:
print(exc)
"""
2 validation errors for Model[int, TypeVar]
a
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
b
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
"""
concrete_model = typevar_model[int]
print(concrete_model(a=1, b=1))
#> a=1 b=1
警告
虽然它可能不会引发错误,但我们强烈建议不要在 isinstance 检查中使用参数化泛型。
For example, you should not do isinstance(my_model, MyGenericModel[int])
. However, it is fine to do isinstance(my_model, MyGenericModel)
. (Note that, for standard generics, it would raise an error to do a subclass check with a parameterized generic.)
If you need to perform isinstance checks against parametrized generics, you can do this by subclassing the parametrized generic class. This looks like class MyIntModel(MyGenericModel[int]): ...
and isinstance(my_model, MyIntModel)
.
如果在 TypeVar
绑定中使用 Pydantic 模型,并且泛型类型从未参数化,那么 Pydantic 将使用该绑定进行验证,但在序列化方面将该值视为 Any
:
from typing import Generic, Optional, TypeVar
from pydantic import BaseModel
class ErrorDetails(BaseModel):
foo: str
ErrorDataT = TypeVar('ErrorDataT', bound=ErrorDetails)
class Error(BaseModel, Generic[ErrorDataT]):
message: str
details: Optional[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
from typing_extensions import 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}}
如果你使用 default=...
(在 Python >= 3.13 中可用或通过 typing-extensions
)或约束( TypeVar('T', str, int)
;请注意,你很少希望使用这种形式的 TypeVar
),那么如果类型变量没有参数化,那么默认值或约束将同时用于验证和序列化。你可以使用 pydantic.SerializeAsAny
覆盖此行为:
from typing import Generic, Optional
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: Optional[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',
},
}
class SerializeAsAnyError(BaseModel, Generic[ErrorDataT]):
message: str
details: Optional[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_extensions import TypeVar
from pydantic import BaseModel
TItem = TypeVar('TItem', bound='ItemBase')
class ItemBase(BaseModel): ...
class IntItem(ItemBase):
value: int
class ItemHolder(BaseModel, Generic[TItem]):
item: TItem
loaded_data = {'item': {'value': 1}}
print(ItemHolder(**loaded_data).model_dump()) # (1)!
#> {'item': {}}
print(ItemHolder[IntItem](**loaded_data).model_dump()) # (2)!
#> {'item': {'value': 1}}
-
当泛型没有参数化时,输入数据将根据泛型边界进行验证。由于
ItemBase
没有字段,因此会丢失item
字段信息。 -
在这种情况下,运行时类型信息是通过泛型参数化显式提供的,因此输入数据将根据
IntItem
类进行验证,并且序列化输出与预期的匹配。
Dynamic model creation¶
动态模型创建¶
API 文档
在某些情况下,希望使用运行时信息创建模型来指定字段。为此,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
这里的 StaticFoobarModel
和 DynamicFoobarModel
是相同的。
字段由以下元组形式之一定义:
(<type>, <default value>)
(<type>, Field(...))
typing.Annotated[<type>, Field(...)]
使用 Field(...)
调用作为元组中的第二个参数(默认值)可以进行更高级的字段配置。因此,以下是类似的:
from pydantic import BaseModel, Field, create_model
DynamicModel = create_model(
'DynamicModel',
foo=(str, Field(..., description='foo description', alias='FOO')),
)
class StaticModel(BaseModel):
foo: str = Field(..., description='foo description', alias='FOO')
可以使用特殊的关键字参数 __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 username_alphanumeric(cls, v):
assert v.isalnum(), 'must be alphanumeric'
return v
validators = {
'username_validator': field_validator('username')(username_alphanumeric)
}
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]
"""
注意
要腌制动态创建的模型:
- the model must be defined globally
- it must provide
__module__
RootModel
and custom root types¶
RootModel
和自定义根类型
Pydantic 模型可以通过继承 pydantic.RootModel
来定义一个“自定义根类型”。
根类型可以是 Pydantic 支持的任何类型,并通过泛型参数指定为 RootModel
。根值可以通过第一个也是唯一的参数传递给模型 __init__
或 model_validate
。
以下是一个示例,说明其工作原理:
from typing import Dict, List
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 typing import List
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 typing import List
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 的抽象基类 (ABCs) 一起使用。
import abc
from pydantic import BaseModel
class FooBarModel(BaseModel, abc.ABC):
a: str
b: int
@abc.abstractmethod
def my_abstract_method(self):
pass
字段顺序¶
字段顺序对模型的影响如下:
-
模型模式中保留了字段顺序
-
验证错误中保留了字段顺序
-
通过
.model_dump()
和.model_dump_json()
等保留字段顺序。
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',)]
必填字段¶
要声明一个字段为必填字段,您可以使用注解或注解与 Field
规范结合使用。您也可以使用 Ellipsis
/ ...
来强调一个字段是必填的,特别是在使用 Field
构造函数时。
Field
函数主要用于为属性配置设置,如 alias
或 description
。构造函数支持 Ellipsis
/ ...
作为唯一的位置参数。这用于表示该字段是必填的,尽管是类型提示强制执行此要求。
from pydantic import BaseModel, Field
class Model(BaseModel):
a: int
b: int = ...
c: int = Field(..., alias='C')
这里 a
、 b
和 c
都是必需的。然而,这种对 b: int = ...
的使用在 mypy 中不能正常工作,并且截至 v1.0 应在大多数情况下避免使用。
注意
在 Pydantic V1 中,用 Optional
或 Any
注释的字段即使没有显式指定默认值,也会被赋予隐式默认值 None
。这种行为在 Pydantic V2 中已经改变,不再有任何类型注释会导致字段具有隐式默认值。
See the migration guide for more details on changes to required and nullable fields.
具有不可哈希默认值的字段¶
Python 中常见的错误来源之一是将可变对象用作函数或方法参数的默认值,因为在每次调用中都会重用同一个实例。
在这种情况下, dataclasses
模块实际上会引发错误,提示你应该使用 default_factory
参数来调用 dataclasses.field
。
Pydantic 还支持使用 default_factory
作为不可哈希的默认值,但这不是必需的。如果默认值不可哈希,Pydantic 在创建模型的每个实例时将对默认值进行深拷贝:
from typing import Dict, List
from pydantic import BaseModel
class Model(BaseModel):
item_counts: List[Dict[str, int]] = [{}]
m1 = Model()
m1.item_counts[0]['a'] = 1
print(m1.item_counts)
#> [{'a': 1}]
m2 = Model()
print(m2.item_counts)
#> [{}]
具有动态默认值的字段¶
当声明一个带有默认值的字段时,您可能希望它是动态的(即对于每个模型都不同)。为此,您可能希望使用 default_factory
。
这是一个例子: 译文:
from datetime import datetime, timezone
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
def datetime_now() -> datetime:
return datetime.now(timezone.utc)
class Model(BaseModel):
uid: UUID = Field(default_factory=uuid4)
updated: datetime = Field(default_factory=datetime_now)
m1 = Model()
m2 = Model()
assert m1.uid != m2.uid
你可以在 Field
函数的文档中找到更多信息。
自动排除的属性¶
类变量¶
使用 typing.ClassVar
注释的属性将被 Pydantic 正确地视为类变量,并且不会成为模型实例上的字段:
from typing import ClassVar
from pydantic import BaseModel
class Model(BaseModel):
x: int = 2
y: ClassVar[int] = 1
m = Model()
print(m)
#> x=2
print(Model.y)
#> 1
Private model attributes¶
私有模型属性
API Documentation
API 文档
具有前导下划线的属性名称不会被 Pydantic 视为字段,也不会包含在模型模式中。相反,这些会被转换为“私有属性”,在调用 __init__
、 model_validate
等时不会进行验证,甚至不会设置。
注意
截至 Pydantic v2.1.0,如果尝试使用 Field
函数与私有属性一起使用,将收到 NameError。因为私有属性不作为字段处理,所以无法应用 Field() 函数。
以下是一个使用示例:
from datetime import datetime
from random import randint
from pydantic import BaseModel, PrivateAttr
class TimeAwareModel(BaseModel):
_processed_at: datetime = PrivateAttr(default_factory=datetime.now)
_secret_value: str
def __init__(self, **data):
super().__init__(**data)
# 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 可以对输入数据进行转换,以强制其符合模型字段类型,在某些情况下,这可能会导致信息丢失。例如:
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 模型都将根据其字段生成签名:
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')
注意
匹配大小写的语句似乎创建了一个新模型,但不要被愚弄;它只是获取属性并进行比较或声明和初始化的语法糖。
属性副本¶
在许多情况下,传递给构造函数的参数将被复制,以便进行验证和必要的强制转换。
在这个例子中,请注意列表的 ID 在类构造后发生了变化,因为它在验证期间被复制了:
from typing import List
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('id(c1.arr) == id(c2.arr):', id(c1.arr) == id(c2.arr))
#> id(c1.arr) == id(c2.arr): False
注意
在某些情况下,Pydantic 不会复制属性,例如在传递模型时——我们使用模型本身。你可以通过设置 model_config['revalidate_instances'] = 'always'
来覆盖此行为。
额外字段¶
默认情况下,当你为不被识别的字段提供数据时,Pydantic 模型不会报错,它们只会被忽略:
from pydantic import BaseModel
class Model(BaseModel):
x: int
m = Model(x=1, y='a')
assert m.model_dump() == {'x': 1}
如果你希望这引发一个错误,你可以通过 model_config
来实现这一点:
from pydantic import BaseModel, ConfigDict, ValidationError
class Model(BaseModel):
x: int
model_config = ConfigDict(extra='forbid')
try:
Model(x=1, y='a')
except ValidationError as exc:
print(exc)
"""
1 validation error for Model
y
Extra inputs are not permitted [type=extra_forbidden, input_value='a', input_type=str]
"""
要保留提供的任何其他数据,可以设置 extra='allow'
。然后,其他字段将存储在 BaseModel.__pydantic_extra__
中:
from pydantic import BaseModel, ConfigDict
class Model(BaseModel):
x: int
model_config = ConfigDict(extra='allow')
m = Model(x=1, y='a')
assert m.__pydantic_extra__ == {'y': 'a'}
默认情况下,不会对这些额外的项应用任何验证,但你可以通过覆盖 __pydantic_extra__
的类型注释来为值设置类型:
from typing import Dict
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Model(BaseModel):
__pydantic_extra__: Dict[str, int] = Field(init=False) # (1)!
x: int
model_config = ConfigDict(extra='allow')
try:
Model(x=1, y='a')
except ValidationError as exc:
print(exc)
"""
1 validation error for Model
y
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
"""
m = Model(x=1, y='2')
assert m.x == 1
assert m.y == 2
assert m.model_dump() == {'x': 1, 'y': 2}
assert m.__pydantic_extra__ == {'y': 2}
-
= Field(init=False)
在运行时没有任何效果,但可以防止__pydantic_extra__
字段被类型检查器视为模型的__init__
方法的参数。
相同的配置也适用于 TypedDict
和 dataclass
,但配置是通过将类的 __pydantic_config__
属性设置为有效的 ConfigDict
来控制的。
本文总阅读量次