/Pydantic Validation

Announcement: Pydantic v2.13 Release

15 mins

Pydantic v2.13 was released on April 13th. You can install it now from PyPI:

pip install --upgrade pydantic

v2.13 also comes with new docs! A unified home for Pydantic Validation, Pydantic AI and Logfire.

This release features the work of 21 external contributors and provides various new features and bug fixes. Several minor changes (considered non-breaking changes according to our versioning policy) are also included in this release. Make sure to look into them before upgrading.

This release contains the updated pydantic.v1 namespace, matching version 1.10.26 which includes support for Python 3.14.

Highlights include:

You can see the full changelog on GitHub.

In v2.12, the behavior of serialize_as_any was unified with the SerializeAsAny annotation. This introduced unfortunate breakage for users relying on serialize_as_any to serialize model subclasses.

In this release, we introduced a new polymorphic serialization option, that applies specifically to model subclasses:

from pydantic import BaseModel


class User(BaseModel):
    name: str


class UserLogin(User):
    password: str


class OuterModel(BaseModel):
    user: User


outer_model = OuterModel(
    user=UserLogin(name='pydantic', password='password'),
)


print(outer_model.model_dump())
#> {'user': {'name': 'pydantic'}}
print(outer_model.model_dump(polymorphic_serialization=True))
#> {'user': {'name': 'pydantic', 'password': 'password'}}

In some way, polymorphic serialization can be seen as a subset of serialize as any, that only applies to Pydantic models and dataclasses (serialize as any applies to all kind of fields).

PR reference: #12518.

In the previous v2.12 version, a new exclude_if option was added for regular fields. This option is now available for computed fields:

from pydantic import BaseModel, computed_field


class Model(BaseModel):
    cash_payments: int
    card_payments: int

    @computed_field(exclude_if=lambda v: v == 0)
    def total_payments(self) -> int:
        return self.cash_payments + self.card_payments


m = Model(cash_payments=0, card_payments=0)
m.model_dump()
#> {'cash_payments': 0, 'card_payments': 0}

Contributed by @andresliszt. PR reference: #12748.

In v2.10, default factories of regular fields gained the ability to take the validated data as an optional argument. This ability was extended to private attributes in this release:

from pydantic import BaseModel, PrivateAttr


class Model(BaseModel):
    foo: int
    _priv1: int = PrivateAttr(default=2)
    _priv2: int = PrivateAttr(default_factory=lambda data: data['foo'] + data['_priv1'])


m = Model(foo=1)
m._priv2
#> 3

Contributed by @andresliszt. PR reference: #11685.

A new ascii_only flag was added to the StringConstraints metadata:

from typing import Annotated

from pydantic import StringConstraints, TypeAdapter

ta = TypeAdapter(Annotated[str, StringConstraints(ascii_only=True)])
ta.validate_python('é')
"""
pydantic_core._pydantic_core.ValidationError: 1 validation error for constrained-str
  String should contain only ASCII characters [type=string_not_ascii, input_value='é', input_type=str]
    For further information visit https://errors.pydantic.dev/2.13/v/string_not_ascii
"""

Contributed by @ai-man-codes. PR reference: #12907.

This release contains some minor changes that may affect existing code. Make sure to go through them before upgrading.

Some fixes and optimizations were applied to the serialization of unions (including discriminated unions). This should result in more correct behavior and better performance, but could introduce unexpected regressions.

PR references: #12604, #12825.

While extra fields set during validation were already tracked in model_fields_set, this was not the case if set after model instantiation (unlike regular fields):

from pydantic import BaseModel


class Model(BaseModel, extra='allow'):
    a: int
    b: int = 1


m = Model(a=1, extra_data='test')
m.model_fields_set
#> {'a', 'extra_data'}
m.b = 2
m.model_fields_set
#> {'a', 'b', 'extra_data'}
m.other_extra = 'test'
m.model_fields_set
#> In <= 2.12: {'a', 'b', 'extra_data'}
#> In >= 2.13: {'a', 'b', 'extra_data', 'other_extra'}

PR reference: #12817.

The behavior of shallow model_copy() on RootModel has been changed to make a shallow copy of the root type.

The previous behavior was to only make a shallow copy of the root model instance, which wasn't really the behavior users would expect:

from pydantic import RootModel


class MyList(RootModel[list[int]]):
    root: list[int]


m = MyList([1, 2])
copied = m.model_copy(deep=False)  # shallow copy (the default)
copied.root is m.root
#> In <= 2.12: True
#> In >= 2.13: False

Contributed by @YassinNouh21. PR reference: #12679.

### Validation of @field_serializer field names

Up until now, the field names arguments to the @field_serializer decorator were not validated, unlike the @field_validator.

If you don't provide any field name to the decorator, or arguments that are not strings, a PydanticUserError will now be raised.

Prior to v2.13, complex Python values were validated using the following rules:

  • complex instances are validated as-is.
  • Strings are validated using the complex() constructor.
  • Numbers (integers and floats) are used as the real part.

Pydantic now defers validation to the complex() constructor, meaning it will support the __complex__(), __float__() or __index__() methods on the input being validated.

Similarly, Decimal values can now be validated as a three-tuple:

from decimal import Decimal

from pydantic import TypeAdapter

ta = TypeAdapter(Decimal)
ta.validate_python((0, (1, 4, 1, 4), -3))
#> Decimal('1.414')
ta.validate_json("[0, [1, 4, 1, 4], -3]")
#> Decimal('1.414')

Contributed by @tanmaymunjal. PR references: #12498, #12500.

When using a subclass of an existing NamedTuple class, Pydantic was treating all annotations of this subclass as fields. In v2.13, Pydantic aligns with the Python behavior (which doesn't support fields defined on subclasses), by checking the _fields attribute:

from typing import NamedTuple

from pydantic import TypeAdapter

class Foo(NamedTuple):
    test: str
    test2: str

class Bar(Foo):
    test3: str


ta = TypeAdapter(Bar)

# Would work on <= 2.12, fails in >= 2.13:
ta.validate_python({'test': 'a', 'test2': 'b', 'test3': 'c'})

Defining such subclasses isn't a supported Python pattern for now, see python/typing#427.

PR reference: #12951.


Ready to try the new features? Upgrade now with pip install --upgrade pydantic or uv add --upgrade pydantic and check out the https😕/github.com/pydantic/pydantic/releases/tag/v2.13.0. Have questions or feedback? Join the conversation on our community Slack.