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:
- Significantly faster schema build times (startup times)
- Reduced memory usage for pydantic model schemas
- Improved alias configuration API
- Python 3.8 support dropped
- Official support for PEP 695 and PEP 696
You can see the full changelog on GitHub.
Quick Reference:
Performance Improvements
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.
Speeding Up Schema Build Times
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:
- Only evaluate
FieldInfo
annotations if required during schema building: 5-10% improvement - Improve annotation application performance: 3-7% boost (with limited context)
- Optimize calls to
get_type_ref()
: up to 30% speedup - Refactor and optimize schema cleaning logic: 15-30% improvement
- Disable
pydantic-core
schema validation: 5-10% boost - Reuse
SchemaValidator
andSchemaSerializer
instances: 5-15% speedup - Reuse cached core schemas for parametrized generics: 67% improvement for our FastAPI startup benchmark (only relevant for heavy use of generics)
Reducing Memory Usage
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!
New Features
Official Support for PEP 695 and PEP 696
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:
New API to Define Fields with create_model()
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.
Experimental support for free threading
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.
Changes
JSON Schema
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'}
Improved Alias Configuration API
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
Python 3.8 Support Dropped
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
Deprecation: Annotated Final
Fields with Defaults
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.
Deprecation: Accessing model_fields
on an instance
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.
Constraint support changes for various types
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:
Bonus: We've added a Third Party Test Suite!
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:
- FastAPI
- SQLModel
- openapi-python-client
- Langchain
- BeanieODM
- Polar
- Pandera
- ODMantic
- BentoML
- semantic-kernel
This project is still ongoing! If you'd like to add your library to the test suite, follow this guide to learn more.
Conclusion
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.
Pydantic Logfire
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!