/Release

Pydantic v2.10

Sydney Runkle avatar
Sydney Runkle
12 mins

Pydantic v2.10 is now available! You can install it now from PyPI:

pip install --upgrade pydantic

This release features the work of over 30 contributors! In this post, we'll cover the highlights of the release. You can see the full changelog on GitHub.

For this release, we focused on adding a variety of new features and bug fixes. We'll be pivoting to a focus on performance improvements in the next release, v2.11.

Perhaps the most exciting new feature in v2.10 is support for partial validation. This allows you to validate an incomplete JSON string, or a Python object representing incomplete input data.

Partial validation is particularly helpful when processing the output of an LLM, where the model streams structured responses, and you may wish to begin validating the stream while you're still receiving data (e.g. to show partial data to users).

We've written extensive documentation for this partial feature.

Right now, support for this is only available for TypeAdapter instances, but support will likely be added for BaseModels and Pydantic dataclasses soon.

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')]

We're eager to get your feedback on this experimental feature so that we can finalize the API before migrating to first class support. If you have any questions or feedback, please open a GitHub discussion, or if you encounter any bugs, please open a GitHub issue.

PR reference: #10748

Pydantic now supports default factories that take already validated data as an argument, so you can define dependent defaults.

For example:

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

Check out our dependent default factories to learn more.

PR reference: #10678

Pydantic supports the use of typing.Unpack to specify variadic keyword arguments in @validate_call decorated functions.

Here's an example:

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 also has full type checking support here, so the invalid call to foo(a='1') will be caught by your type checker as well.

Implementation: #10416

We've implemented support for compiled patterns (regexes) in the protected_namespaces setting. This allows for more flexibility in defining protected namespaces, compared to the previous prefix-based approach.

This was added in conjunction with Relaxing the protected_namespace config default.

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_')`.
    '''

More docs: ConfigDict.protected_namespaces

Implementation details: #10522

defer_build is a Pydantic ConfigDict setting that allows you to defer the building of Pydantic core schemas, validators, and serializers until the first validation, or until manual building is triggered.

This can bring significant performance benefits for application startup time, especially for large applications with many models.

In v2.10, we've introduced support for this setting to Pydantic dataclasses and TypeAdapter instances.

Here's how you might defer schema building for a TypeAdapter instance:

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. Manual rebuild triggered with the rebuild method, similar to the model_rebuild method for BaseModel subclasses.

For more information, see the additional documentation below.

PR reference: #10313, #10329, and #10537

fractions.Fraction is now supported as a first class type in Pydantic. You can validate strings, fraction.Fraction instances, and float, int, and decimal.Decimal instances. fractions.Fraction types are serialized as strings to ensure round-trip safety.

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'

For implementation details, see PR #10318

In #10431 and #10432, we've added more helpful warnings for users who mistakenly mix v1 and v2 models. This should make the v1 -> v2 migration process easier.

If you try to use a v1 model with a v2 model, you'll see a warning like this:

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

If you try to use a v2 model with a v1 model, you'll see a warning like this:

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 supports customizing JSON schema generation in a variety of ways, one of which being subclassing of the GenerateJsonSchema class.

In this release, we've made the sort method public, allowing you to sort JSON schema keys in a custom way.

By default, we sort the JSON schema keys, excluding properties, in order to maintain field order as defined in a model. If you'd prefer to sort the keys in a different way, you can subclass GenerateJsonSchema and override the sort method.

Below, we skip sorting the schema values at all:

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 reference: #10595

You can now subclass ValidationError and PydanticCustomError to create custom error classes. This can be helpful if you want to raise custom exceptions in your custom validators that might have different behavior than the default ValidationError.

Implementation details: pydantic-core#1413

While we didn't emphasize performance improvements in this release as much as we have in the past, we did make some internal changes that should improve performance in some cases. Specifically, we made the following changes:

  • Optimize namespace management and reduce unnecessary copies in #10530
  • Skip unnecessary copies during schema cleaning in #10286
  • Defer JSON schema related computations until needed in #10675

Pydantic's ConfigDict has a protected_namespaces setting that allows you to define a namespace of strings and/or patterns that prevent models from having fields with names that conflict with them.

Before v2.10, Pydantic used ('model_',) as the default value for the protected_namespaces config setting to prevent collisions between model attributes and BaseModel's own methods. This was changed in v2.10 given feedback that this restriction was limiting in AI and data science contexts, where it is common to have fields with names like model_id, model_input, model_output, etc.

Now, the default value is ('model_dump', 'model_validate',). We believe this is a good balance between preventing collisions with core BaseModel methods and allowing for more flexibility in model field naming.

For more details, see these docs. We've also added support for compiled patterns in this setting which enhances the flexibility of this feature.

Reference: PR #10441 and Issue #10315

In versions prior to v2.10, we exposed a schema_generator argument in Pydantic's ConfigDict. This argument served as a hook into customizing the GenerateJsonSchema class. This argument was advertised as experimental + subject to change in minor releases.

As we look to further improve performance, we've decided it's best to once again make the core schema generator logic private so that we have flexibility to make significant changes in the name of performance.

We hope to once again make customization of this (or the resultant) class public, once the API is more stable and core schema building is more performant. If you're running into issues due to the deprecation of this config setting, we'd you to open a GitHub issue with your use case details. Thanks!

Reference: #10303

In versions of Pydantic prior to v2.10, Base64Bytes used base64.encodebytes and base64.decodebytes functions. According to the base64 documentation, these methods are considered legacy implementation, and thus, Pydantic v2.10+ now uses the modern base64.b64encode and base64.b64decode functions.

If you'd like to replicate the old behavior, see these docs for instructions.

PR: #10486

This is more of a bug fix than a change, but it's worth noting here. This should result in more intuitive generic validation behavior than was active in previous versions.

Simply put, when using nested generic models, Pydantic sometimes performs revalidation in an attempt to produce the most intuitive validation result. Specifically, if you have a field of type GenericModel[SomeType] and you validate data like GenericModel[SomeCompatibleType] against this field, we will inspect the data, recognize that the input data is sort of a "loose" subclass of GenericModel, and revalidate the contained SomeCompatibleType data.

This adds some validation overhead, but makes things more intuitive for cases like that shown below:

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

See the "implementation details" section of these new docs for more details.

Implementation details: #10666

We've improved the behavior of Pydantic URL types by building them as concrete subclasses of a base URL type as opposed to using Annotated with UrlConstraints to define URL variations.

This is better, as we can now:

  • Define appropriate types for URL attributes (like host) based on the constraints for a given URL type
  • Properly initialize URL subclasses with validation against said constraints (can't safely do this with Annotated types)
  • Do proper isinstance checks on URL subclasses

For example:

from pydantic import AnyHttpUrl, AnyUrl, TypeAdapter

any_http_url = AnyHttpUrl('https://localhost')
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

Implementation: #10662

Here's what JSON schema generation for Literals and Enums looked like in v2.10:

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"
}
"""

Associated PR: #10692

Simple as that!

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

Implementation reference: pydantic/speedate#77

We're excited to share that Pydantic v2.10.0 is here, and it's the most feature-rich version of Pydantic yet. If you have any questions or feedback, please open a GitHub discussion. If you encounter any bugs, please open a GitHub issue.

Thank you to all of our contributors for making this release possible! We would especially like to acknowledge the following individuals for their significant contributions to this release:

If you're enjoying Pydantic, you might really like Pydantic Logfire, a new observability tool built by the team behind Pydantic. You can now try Logfire for free. We'd love it if you'd join the Pydantic Logfire Slack and let us know what you think!