/发布

Pydantic v2.10

Sydney Runkle avatar
Sydney Runkle
12 分钟

Pydantic v2.10 现已发布!您现在可以从 PyPI 安装它

pip install --upgrade pydantic

此版本包含了超过 30 位贡献者的工作成果!在这篇文章中,我们将介绍此版本的亮点。您可以在 GitHub 上查看完整的变更日志。

在此版本中,我们专注于添加各种新特性和错误修复。在下一个版本 v2.11 中,我们将转向专注于性能改进。

v2.10 中最令人兴奋的新特性也许是对部分验证的支持。这允许您验证不完整的 JSON 字符串,或表示不完整输入数据的 Python 对象。

当处理 LLM 的输出时,部分验证尤其有用,在 LLM 的输出中,模型流式传输结构化响应,您可能希望在仍在接收数据时开始验证流(例如,向用户显示部分数据)。

我们为此部分特性编写了详尽的文档

目前,对此的支持仅适用于 TypeAdapter 实例,但很可能会很快为 BaseModel 和 Pydantic dataclasses 添加支持。

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

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_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

defer_build 是 Pydantic ConfigDict 设置,允许您延迟构建 Pydantic 核心模式、验证器和序列化器,直到第一次验证或手动触发构建为止。

对于具有许多模型的庞大应用程序,这可以为应用程序启动时间带来显著的性能优势

在 v2.10 中,我们为 Pydantic dataclassesTypeAdapter 实例引入了对此设置的支持。

以下是如何为 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
  1. 使用 rebuild 方法手动触发重建,类似于 BaseModel 子类的 model_rebuild 方法。

有关更多信息,请参阅下面的其他文档。

PR 参考:#10313#10329#10537

fractions.Fraction 现在在 Pydantic 中作为一等类型受到支持。您可以验证字符串、fraction.Fraction 实例以及 floatintdecimal.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

#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.
"""

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

您现在可以对 ValidationErrorPydanticCustomError 进行子类化以创建自定义错误类。如果您想在自定义验证器中引发可能具有与默认 ValidationError 不同行为的自定义异常,这将非常有用。

实现细节:pydantic-core#1413

虽然在此版本中,我们没有像过去那样强调性能改进,但我们确实进行了一些内部更改,这些更改在某些情况下应该可以提高性能。具体来说,我们做了以下更改

  • 优化命名空间管理并减少 #10530 中的不必要副本
  • 跳过 #10286 中模式清理期间的不必要副本
  • 将 JSON 模式相关计算延迟到需要时再进行,参见 #10675

Pydantic 的 ConfigDict 具有 protected_namespaces 设置,允许您定义字符串和/或模式的命名空间,以防止模型具有与其冲突的名称的字段。

在 v2.10 之前,Pydantic 使用 ('model_',) 作为 protected_namespaces 配置设置的默认值,以防止模型属性与 BaseModel 自身的方法之间发生冲突。鉴于反馈表明此限制在 AI 和数据科学领域具有局限性(在这些领域中,拥有诸如 model_idmodel_inputmodel_output 等名称的字段是很常见的),因此在 v2.10 中更改了此设置。

现在,默认值为 ('model_dump', 'model_validate',)。我们认为这是在防止与核心 BaseModel 方法发生冲突与允许模型字段命名方面具有更大灵活性之间取得的良好平衡。

有关更多详细信息,请参阅这些文档。我们还在此设置中添加了对编译模式的支持,这增强了此特性的灵活性。

参考:PR #10441Issue #10315

在 v2.10 之前的版本中,我们在 Pydantic 的 ConfigDict 中公开了 schema_generator 参数。此参数充当自定义 GenerateJsonSchema 类的钩子。此参数被宣传为实验性的 + 在次要版本中可能会发生更改。

当我们希望进一步提高性能时,我们决定最好再次将核心模式生成器逻辑设为私有,以便我们能够灵活地进行重大更改以提高性能。

我们希望在 API 更加稳定且核心模式构建性能更高之后,再次公开此(或生成的)类的自定义。如果您由于弃用此配置设置而遇到问题,我们希望您打开一个 GitHub 问题,其中包含您的用例详细信息。谢谢!

参考:#10303

在 v2.10 之前的 Pydantic 版本中,Base64Bytes 使用 base64.encodebytesbase64.decodebytes 函数。根据 base64 文档,这些方法被认为是遗留实现,因此,Pydantic v2.10+ 现在使用现代的 base64.b64encodebase64.b64decode 函数。

如果您想复制旧的行为,请参阅 这些文档 以获取说明。

PR:#10486

这与其说是一个变更,不如说是一个错误修复,但值得在此处注意。与以前的版本中激活的行为相比,这应该会产生更直观的泛型验证行为。

简而言之,当使用嵌套泛型模型时,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 类型构建为基本 URL 类型的具体子类,而不是使用带有 UrlConstraintsAnnotated 来定义 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

以下是 v2.10 中 LiteralsEnum 的 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

就这么简单!

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,您可能会非常喜欢 Pydantic Logfire,这是 Pydantic 团队构建的全新可观测性工具。您现在可以免费试用 Logfire。如果您能加入 Pydantic Logfire Slack 并告诉我们您的想法,我们将不胜感激!