Pydantic v2.10 现已发布!您现在可以从 PyPI 安装它
pip install --upgrade pydantic
此版本包含了超过 30 位贡献者的工作成果!在这篇文章中,我们将介绍此版本的亮点。您可以在 GitHub 上查看完整的变更日志。
在此版本中,我们专注于添加各种新特性和错误修复。在下一个版本 v2.11 中,我们将转向专注于性能改进。
快速参考:
新特性
支持使用 experimental_allow_partial
进行部分验证
v2.10 中最令人兴奋的新特性也许是对部分验证的支持。这允许您验证不完整的 JSON 字符串,或表示不完整输入数据的 Python 对象。
当处理 LLM 的输出时,部分验证尤其有用,在 LLM 的输出中,模型流式传输结构化响应,您可能希望在仍在接收数据时开始验证流(例如,向用户显示部分数据)。
我们为此部分特性编写了详尽的文档。
目前,对此的支持仅适用于 TypeAdapter
实例,但很可能会很快为 BaseModel
和 Pydantic dataclass
es 添加支持。
from pydantic import TypeAdapter, BaseModel
from typing import Literal
class UserRecord(BaseModel):
id: int
name: str
role: Literal['admin', 'user']
ta = TypeAdapter(list[UserRecord])
# allow_partial if the input is a python object
d = ta.validate_python(
[
{'id': '1', 'name': 'Alice', 'role': 'user'},
{'id': '1', 'name': 'Ben', 'role': 'user'},
{'id': '1', 'name': 'Char'},
],
experimental_allow_partial=True,
)
print(d)
#> [UserRecord(id=1, name='Alice', role='user'), UserRecord(id=1, name='Ben', role='user')]
# allow_partial if the input is a json string
d = ta.validate_json(
'[{"id":"1","name":"Alice","role":"user"},{"id":"1","name":"Ben","role":"user"},{"id":"1","name":"Char"}]',
experimental_allow_partial=True,
)
print(d)
#> [UserRecord(id=1, name='Alice', role='user'), UserRecord(id=1, name='Ben', role='user')]
我们渴望获得您对此实验性特性的反馈,以便我们可以在迁移到一流支持之前最终确定 API。如果您有任何问题或反馈,请打开 GitHub 讨论,或者如果您遇到任何错误,请打开 GitHub 问题。
PR 参考:#10748
支持将已验证数据作为参数的默认工厂
Pydantic 现在支持将已验证数据作为参数的默认工厂,因此您可以定义依赖默认值。
例如
from pydantic import BaseModel
class Model(BaseModel):
a: int = 1
b: int = Field(default_factory=lambda data: data['a'] * 2)
model = Model()
assert model.b == 2
查看我们的依赖默认工厂以了解更多信息。
PR 参考:#10678
支持将 typing.Unpack
与 @validate_call
结合使用
Pydantic 支持在 typing.Unpack
中使用 @validate_call
装饰函数来指定可变关键字参数。
这是一个例子
from typing import Required, TypedDict, Unpack
from pydantic import ValidationError, validate_call, with_config
@with_config({'strict': True})
class TD(TypedDict, total=False):
a: int
b: Required[str]
@validate_call
def foo(**kwargs: Unpack[TD]):
pass
foo(a=1, b='test')
foo(b='test')
try:
foo(a='1')
except ValidationError as e:
print(e)
"""
2 validation errors for foo
a
Input should be a valid integer [type=int_type, input_value='1', input_type=str]
For further information visit https://errors.pydantic.dev/2.10/v/int_type
b
Field required [type=missing, input_value={'a': '1'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.10/v/missing
"""
foo
也在此处具有完整的类型检查支持,因此无效的 foo(a='1')
调用也会被您的类型检查器捕获。
实现:#10416
在 protected_namespaces
中支持编译模式
我们在 protected_namespaces
设置中实现了对编译模式(正则表达式)的支持。与以前基于前缀的方法相比,这允许在定义受保护的命名空间时具有更大的灵活性。
这是与放宽 protected_namespace
配置默认值结合添加的。
import re
import warnings
from pydantic import BaseModel, ConfigDict
with warnings.catch_warnings(record=True) as caught_warnings:
warnings.simplefilter('always') # Catch all warnings
class Model(BaseModel):
safe_field: str
also_protect_field: str
protect_this: str
model_config = ConfigDict(
protected_namespaces=(
'protect_me_',
'also_protect_',
re.compile('^protect_this$'),
)
)
for warning in caught_warnings:
print(f'{warning.message}')
'''
Field "also_protect_field" in Model has conflict with protected namespace "also_protect_".
You may be able to resolve this warning by setting `model_config['protected_namespaces'] = ('protect_me_', re.compile('^protect_this$'))`.
Field "protect_this" in Model has conflict with protected namespace "re.compile('^protect_this$')".
You may be able to resolve this warning by setting `model_config['protected_namespaces'] = ('protect_me_', 'also_protect_')`.
'''
更多文档:ConfigDict.protected_namespaces
实现细节:#10522
为 TypeAdapter
和 Pydantic dataclass
es 支持 defer_build
defer_build
是 Pydantic ConfigDict
设置,允许您延迟构建 Pydantic 核心模式、验证器和序列化器,直到第一次验证或手动触发构建为止。
对于具有许多模型的庞大应用程序,这可以为应用程序启动时间带来显著的性能优势。
在 v2.10 中,我们为 Pydantic dataclasses 和 TypeAdapter
实例引入了对此设置的支持。
以下是如何为 TypeAdapter
实例延迟模式构建的方法
from pydantic import ConfigDict, TypeAdapter
ta = TypeAdapter('MyInt', config=ConfigDict(defer_build=True))
# some time later, the forward reference is defined
MyInt = int
ta.rebuild() # (1)!
assert ta.validate_python(1) == 1
- 使用
rebuild
方法手动触发重建,类似于BaseModel
子类的model_rebuild
方法。
有关更多信息,请参阅下面的其他文档。
支持 fractions.Fraction
fractions.Fraction
现在在 Pydantic 中作为一等类型受到支持。您可以验证字符串、fraction.Fraction
实例以及 float
、int
和 decimal.Decimal
实例。fractions.Fraction
类型被序列化为字符串以确保往返安全性。
from pydantic import TypeAdapter
from fractions import Fraction
from decimal import Decimal
fraction_adapter = TypeAdapter(Fraction)
assert fraction_adapter.validate_python('3/2') == Fraction(3, 2)
assert fraction_adapter.validate_python(Fraction(3, 2)) == Fraction(3, 2)
assert fraction_adapter.validate_python(Decimal('1.5')) == Fraction(3, 2)
assert fraction_adapter.validate_python(1.5) == Fraction(3, 2)
assert fraction_adapter.dump_python(Fraction(3, 2)) == '3/2'
有关实现细节,请参阅 PR #10318
针对混合使用 v1 和 v2 模型的不兼容性警告
在 #10431 和 #10432 中,我们为错误地混合使用 v1 和 v2 模型的用户添加了更有帮助的警告。这应该使 v1 -> v2 迁移过程更加容易。
如果您尝试将 v1 模型与 v2 模型一起使用,您将看到如下警告
from pydantic import BaseModel as BaseModelV2
from pydantic.v1 import BaseModel as BaseModelV1
class V1Model(BaseModelV1):
...
class V2Model(BaseModelV2):
inner: V1Model
"""
UserWarning: Nesting V1 models inside V2 models is not supported. Please upgrade V1Model to V2.
"""
如果您尝试将 v2 模型与 v1 模型一起使用,您将看到如下警告
from pydantic import BaseModel as BaseModelV2
from pydantic.v1 import BaseModel as BaseModelV1
class V2Model(BaseModelV2):
...
class V1Model(BaseModelV1):
inner: V2Model
"""
UserWarning: Mixing V1 and V2 models is not supported. `V2Model` is a V2 model.
"""
为 JSON 模式生成公开公共 sort
方法
Pydantic 支持以各种方式自定义 JSON 模式生成,其中一种方式是对 GenerateJsonSchema
类进行子类化。
在此版本中,我们公开了 sort
方法,允许您以自定义方式排序 JSON 模式键。
默认情况下,我们对 JSON 模式键进行排序,不包括 properties
,以便保持模型中定义的字段顺序。如果您希望以不同的方式对键进行排序,您可以对 GenerateJsonSchema
进行子类化并覆盖 sort
方法。
下面,我们完全跳过对模式值的排序
import json
from typing import Optional
from pydantic import BaseModel, Field
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
class MyGenerateJsonSchema(GenerateJsonSchema):
def sort(
self, value: JsonSchemaValue, parent_key: Optional[str] = None
) -> JsonSchemaValue:
"""No-op, we don't want to sort schema values at all."""
return value
class Bar(BaseModel):
c: str
b: str
a: str = Field(json_schema_extra={'c': 'hi', 'b': 'hello', 'a': 'world'})
json_schema = Bar.model_json_schema(schema_generator=MyGenerateJsonSchema)
print(json.dumps(json_schema, indent=2))
"""
{
"type": "object",
"properties": {
"c": {
"type": "string",
"title": "C"
},
"b": {
"type": "string",
"title": "B"
},
"a": {
"type": "string",
"c": "hi",
"b": "hello",
"a": "world",
"title": "A"
}
},
"required": [
"c",
"b",
"a"
],
"title": "Bar"
}
"""
PR 参考:#10595
允许对 ValidationError
和 PydanticCustomError
进行子类化
您现在可以对 ValidationError
和 PydanticCustomError
进行子类化以创建自定义错误类。如果您想在自定义验证器中引发可能具有与默认 ValidationError
不同行为的自定义异常,这将非常有用。
实现细节:pydantic-core#1413
性能改进
虽然在此版本中,我们没有像过去那样强调性能改进,但我们确实进行了一些内部更改,这些更改在某些情况下应该可以提高性能。具体来说,我们做了以下更改
变更
放宽 protected_namespace
配置默认值
Pydantic 的 ConfigDict
具有 protected_namespaces
设置,允许您定义字符串和/或模式的命名空间,以防止模型具有与其冲突的名称的字段。
在 v2.10 之前,Pydantic 使用 ('model_',)
作为 protected_namespaces
配置设置的默认值,以防止模型属性与 BaseModel
自身的方法之间发生冲突。鉴于反馈表明此限制在 AI 和数据科学领域具有局限性(在这些领域中,拥有诸如 model_id
、model_input
、model_output
等名称的字段是很常见的),因此在 v2.10 中更改了此设置。
现在,默认值为 ('model_dump', 'model_validate',)
。我们认为这是在防止与核心 BaseModel
方法发生冲突与允许模型字段命名方面具有更大灵活性之间取得的良好平衡。
有关更多详细信息,请参阅这些文档。我们还在此设置中添加了对编译模式的支持,这增强了此特性的灵活性。
参考:PR #10441 和 Issue #10315
弃用 schema_generator
配置参数
在 v2.10 之前的版本中,我们在 Pydantic 的 ConfigDict
中公开了 schema_generator
参数。此参数充当自定义 GenerateJsonSchema
类的钩子。此参数被宣传为实验性的 + 在次要版本中可能会发生更改。
当我们希望进一步提高性能时,我们决定最好再次将核心模式生成器逻辑设为私有,以便我们能够灵活地进行重大更改以提高性能。
我们希望在 API 更加稳定且核心模式构建性能更高之后,再次公开此(或生成的)类的自定义。如果您由于弃用此配置设置而遇到问题,我们希望您打开一个 GitHub 问题,其中包含您的用例详细信息。谢谢!
参考:#10303
为 Base64Bytes
和 Base64Str
类型使用 b64decode
和 b64encode
在 v2.10 之前的 Pydantic 版本中,Base64Bytes
使用 base64.encodebytes
和 base64.decodebytes
函数。根据 base64 文档,这些方法被认为是遗留实现,因此,Pydantic v2.10+ 现在使用现代的 base64.b64encode
和 base64.b64decode
函数。
如果您想复制旧的行为,请参阅 这些文档 以获取说明。
PR:#10486
如果实例的 origin 是原始类的子类,则重新验证参数化泛型
这与其说是一个变更,不如说是一个错误修复,但值得在此处注意。与以前的版本中激活的行为相比,这应该会产生更直观的泛型验证行为。
简而言之,当使用嵌套泛型模型时,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))
有关更多详细信息,请参阅这些新文档的“实现细节”部分。
实现细节:#10666
迁移到子类化而不是注解方法以实现 Pydantic url 类型
我们通过将 Pydantic URL 类型构建为基本 URL 类型的具体子类,而不是使用带有 UrlConstraints
的 Annotated
来定义 URL 变体,从而改进了 Pydantic URL 类型的行为。
这样做更好,因为我们现在可以
- 根据给定 URL 类型的约束,为 URL 属性(如
host
)定义适当的类型 - 使用针对所述约束的验证正确初始化 URL 子类(使用
Annotated
类型无法安全地执行此操作) - 对 URL 子类执行正确的
isinstance
检查
例如
from pydantic import AnyHttpUrl, AnyUrl, TypeAdapter
any_http_url = AnyHttpUrl('https://127.0.0.1')
assert isinstance(any_http_url, AnyUrl)
assert isinstance(any_http_url, AnyHttpUrl)
url = TypeAdapter(AnyUrl).validate_python(any_http_url)
assert url is any_http_url
实现:#10662
整理 Literals
和 Enum
的 JSON 模式生成
以下是 v2.10 中 Literals
和 Enum
的 JSON 模式生成的样子
import json
from enum import Enum
from typing import Literal
from pydantic import BaseModel
class PrimaryColor(str, Enum):
red = 'red'
yellow = 'yellow'
blue = 'blue'
class Painting(BaseModel):
color: PrimaryColor
medium: Literal['canvas', 'paper']
name: Literal['my_painting']
print(json.dumps(Painting.model_json_schema(), indent=4))
"""
{
"$defs": {
"PrimaryColor": {
"enum": [
"red",
"yellow",
"blue"
],
"title": "PrimaryColor",
"type": "string"
}
},
"properties": {
"color": {
"$ref": "#/$defs/PrimaryColor"
},
"medium": {
"enum": [
"canvas",
"paper"
],
"title": "Medium",
"type": "string"
},
"name": {
"const": "my_painting",
"title": "Name",
"type": "string"
}
},
"required": [
"color",
"medium",
"name"
],
"title": "Painting",
"type": "object"
}
"""
相关 PR:#10692
支持远至公元前 1 年的日期
就这么简单!
from datetime import datetime
from pydantic import BaseModel
class Model(BaseModel):
dt: datetime
m = Model(dt='1000-01-01T00:00:00+00:00')
print(m.model_dump())
# > {'dt': datetime.datetime(1000, 1, 1, 0, 0, tzinfo=TzInfo(UTC))}
实现参考:pydantic/speedate#77
结论
我们很高兴分享 Pydantic v2.10.0 已经发布,并且它是迄今为止功能最丰富的 Pydantic 版本。如果您有任何问题或反馈,请打开 GitHub 讨论。如果您遇到任何错误,请打开 GitHub 问题。
感谢所有贡献者使此版本成为可能!我们要特别感谢以下个人为此版本做出的重大贡献
Pydantic Logfire
如果您喜欢 Pydantic,您可能会非常喜欢 Pydantic Logfire,这是 Pydantic 团队构建的全新可观测性工具。您现在可以免费试用 Logfire。如果您能加入 Pydantic Logfire Slack 并告诉我们您的想法,我们将不胜感激!