/Release

Pydantic v2.11

Sydney Runkle avatar
Sydney Runkle
Victorien Plot avatar
Victorien Plot
15 mins

Pydantic v2.11 is here! You can install it now from PyPI:

pip install --upgrade pydantic

This release features the work of over 50 contributors (!) and brings major performance improvements and impactful new features.

Highlights include:

You can see the full changelog on GitHub.

We've worked hard in v2.11 to speed up schema build times and reduce memory usage for Pydantic models.

To showcase the improvements of this release, we will use one of our large benchmark files, the kubernetes model file benchmark (provided by a community member helping us to troubleshoot performance).

The following table shows results across different Pydantic versions (run on an M4 Mac):

Pydantic Version Startup Time (seconds) Total Memory Allocated Final Memory Usage
2.7.2 14.06 8.954 GB 3.691 GB
2.8.2 14.53 8.957 GB 3.970 GB
2.9.2 3.96 2.997 GB 634.4 MB
2.10.6 2.77 1.413 GB 589.0 MB
2.11.0 1.52 311.3 MB 230.8 MB

The significant drop in startup time starting in 2.9 is only reproducible in certain scenarios. However, what does matter are the final improvements shipping with 2.11. Other large benchmarks were also used during development and showed similar results.

With our recent changes, you can expect up to a 2x improvement in schema build times.

Unlike with the memory optimizations where we can point to one main source of improvement, the schema build time improvements are the result of a series of small to medium changes.

The following PRs contributed significantly to schema build time improvements:

Recent memory usage optimizations are most relevant for projects with lots of models, particularly those with nested/reused models. In these cases, you can expect a 2-5x reduction in memory usage.

The main change we implemented to optimize memory consumption was reusing pydantic-core SchemaValidator and SchemaSerializer structures for models with said structures prebuilt.

Some highlight stats:

  • Extreme reduction in total number of allocations, we tested up to almost 7x, but this could be even greater depending on model structure
  • Significant reduction in resident memory size — this is probably the most important metric for users — our experiments showed results between 2-4x
  • Reduction in total memory allocated throughout the entirety of an application's schema build process — 1.5-2x

For the kubernetes model file benchmark:

Metric Before After Change % Change Reduction Factor
Resident Memory Size 563MB 290MB -273MB -48.5% 1.94Ă—
Total Allocations 1,969,609 586,011 -1,383,598 -70.2% 3.36Ă—
Total Memory Allocated 787MB 519MB -268MB -34.1% 1.52Ă—

Using the aiotdlib library (containing a lot of auto-generated models):

Metric Before After Change % Change Reduction Factor
Resident Memory Size 884MB 212MB -672MB -76.0% 4.17Ă—
Total Allocations 5,069,626 746,466 -4,323,160 -85.3% 6.79Ă—
Total Memory Allocated 1.317GB 671MB -646MB -49.1% 1.96Ă—

The main set of changes was in pydantic-core, with the corresponding PR in pydantic showcasing 5-15% improvements in schema build times, as a nice side effect!

In Python 3.12, a new syntax was introduced to create generic classes. While it incidentally worked with Pydantic models, the new syntax wasn’t properly supported for other types (dataclasses, named tuples, etc). In this release, you can now safely make use of the syntax (assuming you are on Python 3.12 or greater):

from pydantic import BaseModel

class Model[T1, T2](BaseModel):
    t1: T1
    t2: 'list[T2]'  # Forward references also work

PEP 695 also introduced a new syntax to define type aliases. While support for such aliases was added in previous Pydantic versions, this release fixes many bugs and inconsistencies with them. Here is an example showcasing the use of the type statement:

from pydantic import TypeAdapter

type MyList[T] = list[T]
type MyDict = dict[str, MyList[int]]

TypeAdapter.validate_python({'test': ['1', 2]})
#> {'test': [1, 2]}

Note that PEP 695 type aliases are treated differently from “normal”/”old-style” type aliases. For more information, consult the dedicated documentation section.


Additionally, type variable defaults (introduced by PEP 696) are now properly supported.

PR references:

The API to provide fields to create_model() was simplified and supports more use cases. The new syntax is similar to model annotations:

from pydantic import create_model

Model = create_model(
    'Model',
    foo=str,         # Equivalent to `foo: str` on a model
    bar=(int, 123),  # Equivalent to `bar: int = 123` on a model
)

In previous Pydantic versions, a field without a default had to be specified as a two-tuple (e.g. (int, ...)). Having multiple Annotated metadata wasn’t supported as well.

The documentation can be found here.

PR reference: #11032.

Starting with the CPython 3.13 release, a new free-threaded build is made available to disable the global interpreter lock (GIL). In 2.11, Pydantic added experimental support for such builds.

However, do note that many unexpected issues can still occur, including during validation and/or serialization. The tests we run in CI are currently still flaky, and more investigation will be needed to identify whether this is due to CPython 3.13 itself or Pydantic.

Generated JSON Schemas for dict types now include an additionalProperties keyword with a value of true if the value type is Any or object.

from pydantic import TypeAdapter

print(TypeAdapter(dict[str, object]).json_schema())
#> {'additionalProperties': True, 'type': 'object'}

You asked, we listened! The API for configuring alias use in validation and serialization has been significantly improved.

The new API introduces increased configurability for alias usage via ConfigDict settings and runtime flags. For a full overview of the new API, check out the aliases documentation.

Here are the highlights:

New serialize_by_alias setting

This was the most requested change to the alias configuration API: to be more consistent with the validation by alias API, we've added a serialize_by_alias configuration setting.

serialize_by_alias is set to False by default for backwards compatibility. In V3, we anticipate changing the default to True to be consistent with the default behavior at validation time (validating by alias by default).

from pydantic import BaseModel, ConfigDict, Field


class Model(BaseModel):
    my_field: str = Field(serialization_alias='my_alias')

    model_config = ConfigDict(serialize_by_alias=True)


m = Model(my_field='foo')
m.model_dump()
#> {'my_alias': 'foo'}

Changes to alias validation

Up until now, Pydantic was using the alias to validate input data, unless populate_by_name was set to True (in which case both the alias and field name could be used).

Pydantic 2.11 introduces two new configuration settings that affect alias validation behavior: validate_by_alias and validate_by_name.

validate_by_name is equivalent to populate_by_name, and as such populate_by_name is pending deprecation in V3.

By default, validate_by_alias is True and validate_by_name is False (backwards compatible). Having two configuration settings allow for better customization of the validation behavior (for instance, it is now possible to only validate by name, by setting validate_by_alias to False).

from pydantic import BaseModel, ConfigDict, Field


class Model(BaseModel):
    my_field: str = Field(validation_alias='my_alias')

    model_config = ConfigDict(validate_by_alias=True, validate_by_name=True)


Model(my_alias='foo')
#> Model(my_field='foo')

Model(my_field='foo')
#> Model(my_field='foo')

Alias behavior on the validation methods

Two new parameters — by_alias and by_name — were added on the validation methods (such as model_validate()) to better control the alias validation behavior.

These settings were introduced to be more consistent with the serialization by alias API, which offers a runtime flag.

By default, by_alias is True and by_name is False. Runtime flags surpass model boundaries (unlike config settings), so they can also be used to override model configuration at validation time.

from pydantic import BaseModel, Field


class Model(BaseModel):
    my_field: str = Field(validation_alias='my_alias')


Model.model_validate(
    {'my_alias': 'foo'}, by_alias=True, by_name=True
)
#> Model(my_field='foo')

Model.model_validate(
    {'my_field': 'foo'}, by_alias=True, by_name=True
)
#> Model(my_field='foo')

PR references: pydantic changes and pydantic-core changes

As per Pydantic’s policy, support for Python 3.8 was dropped in this release. As a consequence, you can now make use of the new style generics for built-in types (introduced by PEP 585) on any Python version:

from collections.abc import Sequence

from pydantic import BaseModel

class Model(BaseModel):
    f1: list[int]
    f2: dict[str, bool]
    f3: Sequence[str]

The status of the supported Python versions can be found here.

PR reference: #11258

If a field is annotated as Final and doesn’t have a default value, it is considered as a frozen instance field by Pydantic. However, if the field has a default value, the field is currently treated as a class variable:

from typing import Final

from pydantic import BaseModel

class Model(BaseModel):
    a: Final[int]      # a is a "normal" field
    b: Final[int] = 2  # b is a class variable

This behavior is coming from the PEP 591 specification. It was recently superseded by an update in the typing specification, and as such Pydantic will align with the new expected behavior (i.e. such class variables will be “normal” fields) in V3.

Annotating a field as Final with a default value will now raise a deprecation warning. If you are relying on this behavior, consider using the ClassVar qualifier instead.

PR reference: #11168.

Accessing the model_fields and model_computed_fields attributes on model instances is deprecated:

from pydantic import BaseModel

class Model(BaseModel):
    a: int

my_model = Model(a=1)
my_model.model_fields  # ❌ will not work in V3
Model.model_fields     # âś… will continue to work

This is done in an effort to clean up the BaseModel class namespace (so that model_fields can actually be used as a field name).

PR reference: #11169.

In v2.10 and earlier, types such as Mapping, Path (and other path-like types) and deque supported some constraints that weren't fully appropriate for the underlying types/validators. In v2.11, we've moved to a more standard schema building approach for these types, which resulted in a few changes to the constraints that are supported.

For Path types, support for constraints like min_length, max_length, and strip_whitespace/other string constraints has been removed, and should be implemented via custom validators instead.

For deque types, max_length constraints are still enforced, but we no longer monkeypatch the max_length constraint with the maxlen attribute from the validated deque instance. If you desire this edge case-y behavior, you should implement it via a custom validator.

For Mapping like types, we don't anticipate changes to the allowed constraints, but we always recommend custom validator usage for anything that isn't a relatively standard constraint.

PR references:

Pydantic v2.11 is launching backed by a new third party test suite, which is a collection of tests from other popular libraries run nightly against the main branch. We also run this test suite on PRs with changes that have the potential to affect Pydantic users.

These tests help to catch regressions in Pydantic and ensure that upgrades to Pydantic are smooth for library maintainers and users alike.

Thus far, we've added tests against:

This project is still ongoing! If you'd like to add your library to the test suite, follow this guide to learn more.

We're excited to share Pydantic v2.11, the most performant 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 @MarkusSintonen for his significant contributions to improving schema build times as a part of 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!