Pydantic v2.8 is now available! You can install it now via PyPi or your favorite package manager:
pip install --upgrade pydantic
This release features the work of over 50 contributors! In this post, we'll cover the highlights of the release. You can see the full changelog on github.
This release focused especially on typing related bug fixes and improvements, as well as some opt-in performance enhancing features.
New Features
Fail-Fast Validation
Pydantic v2.8 introduces a new feature called fail fast validation. This is currently available for a limited number of sequence types
including list
, tuple
, set
, and frozenset
. When you use FailFast
validation, Pydantic will stop validation as soon as it encounters an error.
This feature is useful when you care more about the validity of your data than the thoroughness of the validation errors. For many folks, this tradeoff in error specificity is well worth the performance gain.
You can either use FailFast()
as a type annotation or specify the fail_fast
parameter in the Field
constructor.
For example:
from typing import List
from typing_extensions import Annotated
from pydantic import BaseModel, FailFast, Field, ValidationError
class Model(BaseModel):
x: Annotated[List[int], FailFast()]
y: List[int] = Field(..., fail_fast=True)
# This will raise a single error for the first invalid value in each list
# At which point, validation for said field will stop
try:
obj = Model(x=[1, 2, 'a', 4, 5, 'b', 7, 8, 9, 'c'], y=[1, 2, 'a', 4, 5, 'b', 7, 8, 9, 'c'])
except ValidationError as e:
print(e)
"""
2 validation errors for Model
x.2
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/int_parsing
y.2
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='a', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/int_parsing
"""
We plan on extending this feature to other types in the future!
You can read more about FailFast
in the API reference
Model/field deprecation in JSON schema
In v2.7, we introduced the ability to deprecate models and fields. In v2.8, we've extended this feature to include deprecation information in the JSON schema.
from typing_extensions import deprecated
from pydantic import BaseModel, Field
@deprecated('DeprecatedModel is... sadly deprecated')
class DeprecatedModel(BaseModel):
deprecated_field: str = Field(..., deprecated=True)
json_schema = DeprecatedModel.schema()
assert json_schema['deprecated'] is True
assert json_schema['properties']['deprecated_field']['deprecated'] is True
Programmatic Title Generation
This new feature allows you to generate titles for your models and fields with a callable. This is quite helpful when you want to generate titles dynamically based on the model or field's attributes.
You can share callable title generators between models and fields, which helps keep your code DRY.
import json
from pydantic import BaseModel, ConfigDict, Field
class MyModel(BaseModel):
foo: str = Field(..., field_title_generator=lambda name, field_info: f'title-{name}-from-field')
bar: str
model_config = ConfigDict(
field_title_generator=lambda name, field_info: f'title-{name}-from-config',
model_title_generator=lambda cls: f'title-{cls.__name__}-from-config',
)
print(json.dumps(MyModel.model_json_schema(), indent=2))
"""
{
"properties": {
"foo": {
"title": "title-foo-from-field",
"type": "string"
},
"bar": {
"title": "title-bar-from-config",
"type": "string"
},
},
"required": [
"foo",
"bar",
],
"title": "title-MyModel-from-config",
"type": "object"
}
"""
Want to learn more? Check out the JSON schema customization docs.
Serialization Context for TypeAdapter
In v2.7 we added support for passing context to serializers for BaseModel
s.
In v2.8, we've extended that support to TypeAdapter
's serialization methods.
Here's a simple example, where we use a unit
provided in the context
to convert a distance
field:
from typing_extensions import Annotated
from pydantic import SerializationInfo, TypeAdapter, PlainSerializer
def serialize_distance(v: float, info: SerializationInfo) -> float:
"""We assume a distance is provided in meters, but we can convert it to other units if a context is provided."""
context = info.context
if context and 'unit' in context:
if context['unit'] == 'km':
v /= 1000 # convert to kilometers
elif context['unit'] == 'cm':
v *= 100 # convert to centimeters
return v
distance_adapter = TypeAdapter(Annotated[float, PlainSerializer(serialize_distance)])
print(distance_adapter.dump_python(500)) # no context, dumps in meters
# > 500.0
print(distance_adapter.dump_python(500, context={'unit': 'km'})) # with context, dumps in kilometers
# > 0.5
print(distance_adapter.dump_python(500, context={'unit': 'cm'})) # with context, dumps in centimeters
# > 50000
Experimental Features
In v2.8.0, we introduced a new pattern for introducing experimental features and settings. We've added a section to our version policy explaining how we'll handle experimental features moving forward.
You can find documentation for our new experimental features in the experimental features section.
Experimental features will either be:
- Located in the
experimental
module, so you can import them viafrom pydantic.experimental import ...
- Prefixed with
experimental
, so you can use them likesome_func(experimental_param=...)
orsome_model.experimental_method(...)
When you import an experimental feature from the experimental
module, you'll see a PydanticExperimentalWarning
. You can filter this via:
import warnings
from pydantic import PydanticExperimentalWarning
warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)
In our version policy, we also touch on the lifecycle of experimental features. It's very possible that experimental features will experience non-backward-compatible changes or be removed entirely in future versions, so please be aware of their volatility when opting to use them.
Pipeline API — Experimental
Pydantic v2.8.0 introduced an experimental "pipeline" API that allows composing of parsing (validation), constraints and transformations in a more type-safe manner than existing APIs.
Generally, the pipeline API is used to define a sequence of steps to apply to incoming data during validation. The below example illustrates how you can use the pipeline API to validate an int first as a string, strip extra whitespace, then parse it as an int, and finally ensure it's greater than or equal to 0.
import warnings
from typing_extensions import Annotated
from pydantic import BaseModel, PydanticExperimentalWarning, ValidationError
warnings.filterwarnings('ignore', category=PydanticExperimentalWarning)
from pydantic.experimental.pipeline import validate_as
class Model(BaseModel):
data: Annotated[str, validate_as(str).str_strip().validate_as(...).ge(0)]
print(repr(Model(data=' 123 ')))
#> Model(data=123)
try:
Model(data=' -123 ')
except ValidationError as e:
print(e)
"""
1 validation error for Model
data
Input should be greater than or equal to 0 [type=greater_than_equal, input_value=' -123 ', input_type=str]
For further information visit https://errors.pydantic.dev/2.8/v/greater_than_equal
"""
Note, validate_as(...)
is equivalent to validate_as(<type_in_annotation>)
. So for the above example,
validate_as(...)
is equivalent to validate_as(int)
.
We've added some additional examples to the pipeline docs, if you'd like to learn more.
defer_build
support for `TypeAdapter — Experimental
Pydantic BaseModel
s currently support a defer_build
setting in their configuration, allowing
for deferred schema building (until the first validation call). This can help reduce startup time for applications that might have
an abundance of complex models that bear a heavy schema-building cost.
In v2.8, we've added experimental support for defer_build
in TypeAdapter
's configuration.
Here's an example of how you can use this new experimental feature:
from pydantic import ConfigDict, TypeAdapter
from typing import TypeAlias
# in practice, this would be some complex type for which schema building
# takes a while, hence the value of deferring the build
SuperDuperComplexType: TypeAlias = int
ta = TypeAdapter(
SuperDuperComplexType,
config=ConfigDict(
defer_build=True,
experimental_defer_build_mode=('model', 'type_adapter'),
),
)
assert ta._core_schema is None
print(ta.validate_python(0))
# after a call is made that requires the core schema, it's built and cached
assert ta.core_schema == {'type': 'int'}
Performance Improvements
codeflash
Continuous Optimization
We've been working with the codeflash
team to make LLM driven optimizations to Pydantic's source code.
Thus far, we've made some minor optimizations in our internal logic, and we're looking forward to collaborating more significant improvements in the future.
We also hope to integrate codeflash
into our CI/CD pipeline to ensure that we're consistently checking for optimization opportunities.
Notable Improvements / Fixes
Support for Python 3.13
Pydantic V2 now supports Python 3.13!
A few of our test-oriented dependencies are not yet compatible with Python 3.13, but we plan to upgrade them soon. This means that not all tests can be run against Python 3.13 (there are just a few that aren't yet compatible), but this should only affect contributors.
For users, Pydantic should work as expected with Python 3.13. If you run into any issues, please let us know!
Smarter smart
Union
Validation
When validating data against a union of types, Pydantic offers a
smart
mode and a
left_to_right
mode.
We've made some improvements to the smart
mode to improve behavior when validating against a union of types
that contain many of the same fields. The following example showcases an example of the old behavior vs the
new behavior:
from pydantic import BaseModel, TypeAdapter
class ModelA(BaseModel):
a: int
class ModelB(ModelA):
b: str
ta = TypeAdapter(ModelA | ModelB)
print(repr(ta.validate_python({'a': 1, 'b': 'foo'})))
#> old behavior: ModelA(a=1)
#> new behavior: ModelB(a=1, b='foo')
There are many other more complex cases where the new behavior is more intuitive and correct. The general idea is that we now incorporate the number of valid fields into the scoring algorithm, in addition to the exactness (in terms of strict, lax, etc) of the match.
We have documented the match scoring algorithm in order to provide transparency and predictability. However, we do reserve the right to modify the smart union algorithm in the future to further improve its behavior. In general, we expect such changes will only impact edge cases, and we'll only make such changes when they near-universally improve the handling of such edge cases.
Better understanding this algorithm can also help you choose which union mode is right for your use case.
If you're looking for more performant union validation than smart mode provides, we recommend that you use tagged unions.
Respect Regex Flags in Constrained String Validation
Python's re
module supports flags that can be passed to regex patterns to change their behavior.
For example, the re.IGNORECASE
flag makes a pattern case-insensitive. In previous versions of Pydantic,
even when using the python-re
regex engine, these flags were ignored.
Now, we've improved Pydantic's constrained string validation by:
- Not recompiling Python regex patterns (which is more performant)
- Respecting the flags passed to a compiled pattern
!!! note
If you use a compiled regex pattern, the python-re engine will be used regardless of this setting.
This is so that flags such as re.IGNORECASE
are respected.
Here's an example of said flags in action:
import re
from typing_extensions import Annotated
from pydantic import BaseModel, ConfigDict, StringConstraints
class Model(BaseModel):
a: Annotated[str, StringConstraints(pattern=re.compile(r'[A-Z]+', re.IGNORECASE))]
model_config = ConfigDict(regex_engine='python-re')
# allows lowercase letters, even though the pattern is uppercase only due to the re.IGNORECASE flag
assert Model(a='abc').a == 'abc'
You can learn more about the regex_engine
setting in our model config docs
Conclusion
With these new features and performance improvements, Pydantic v2.8.0 is the best and 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:
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 during our open beta period. We'd love it if you'd join the Pydantic Logfire Slack and let us know what you think!