The long awaited Pydantic v2.12 is here! You can install it now from PyPI:
pip install --upgrade pydantic
This release features the work of over 20 external contributors and provides useful new features, along with initial Python 3.14 support. 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.
Highlights include:
- Support for Python 3.14
- A new
MISSINGsentinel - PEP 728 support – TypedDict with Typed Extra Items
- Preserve empty URL paths
You can see the full changelog on GitHub.
Quick Reference:
New Features
Support for Python 3.14
Python 3.14 ships with new type annotations semantics, introduced by PEP 649
and PEP 749. Type annotations are now lazily evaluated, dropping the need
to use string annotations (or
enabling this behavior per module using from __future__ import annotations).
Here is an example demonstrating the new behavior:
class Model(BaseModel):
f: ForwardType # No quotes required.
type ForwardType = int
m = Model(f=1)
PR references:
MISSING sentinel
A highly requested Pydantic feature is having the ability to differentiate between a default value and value provided during
the model creation, especially if None has a specific meaning in the context it is used. Until now, Pydantic provided the
model_fields_set property, but
its usage was proven difficult.
Pydantic 2.12 introduces an experimental MISSING sentinel:
a singleton indicating a field value was not provided during validation. During serialization, any field with MISSING as a value is
excluded from the output.
from pydantic import BaseModel
from pydantic.experimental.missing_sentinel import MISSING
class Configuration(BaseModel):
timeout: int | None | MISSING = MISSING
# configuration defaults, stored somewhere else:
defaults = {'timeout': 200}
conf = Configuration()
# `timeout` is excluded from the serialization output:
conf.model_dump()
# {}
# The `MISSING` value doesn't appear in the JSON Schema:
Configuration.model_json_schema()['properties']['timeout']
#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}
This feature is marked as experimental because it relies on the draft PEP 661, introducing sentinels in the standard library.
See the documentation for more details.
PR reference: #11883.
PEP 728 support – TypedDict with Typed Extra Items
Back in August, PEP 728 was accepted as part of the upcoming Python 3.15 release.
This PEP adds two class parameters, closed and extra_items to type the extra items on a TypedDict,
addressing the need to define closed TypedDict types or to type a subset of keys that might appear in a dict while permitting additional items of a specified type.
from typing_extensions import TypedDict
from pydantic import TypeAdapter
class TD(TypedDict, extra_items=int):
a: str
ta = TypeAdapter(TD)
print(ta.validate_python({'a': 'test', 'extra': 1}))
#> {'a': 'test', 'extra': 1}
Such behavior was partially achievable using the extra
configuration value. It is now recommended to use PEP 728 capabilities.
Note that PEP 728 is currently only implemented in typing_extensions. Make sure to
import TypedDict from there if you want to make use of this feature.
The additionalProperties JSON Schema keyword
will be populated depending on the closed/extra_items specification (False if closed, True if extra_items is Any or object,
matching the schema of the extra_items type otherwise).
PR reference: #12179.
Preserve empty URL paths
The URL types had a behavior change in V2, where a trailing slash would be added if the path was empty. In many cases, this is unwanted.
Pydantic 2.12 adds a new url_preserve_empty_path
configuration value to opt-out of this behavior:
from pydantic import AnyUrl, BaseModel, ConfigDict
class Model(BaseModel):
u: AnyUrl
model_config = ConfigDict(url_preserve_empty_path=True)
print(Model(u='https://example.com').u)
#> Before: 'https://example/com/'
#> After: 'https://example/com'
This is also configurable per-field using the UrlConstraints
metadata class:
from typing import Annotated
from pydantic import TypeAdapter, UrlConstraints
ta = TypeAdapter(Annotated[AnyUrl, UrlConstraints(preserve_empty_path=True)])
print(ta.validate_python('https://example.com'))
#> https://example.com
This configuration value may default to True in V3.
PR references:
Control validation behavior of timestamps
Pydantic uses to guess if a timestamp was provided in seconds or milliseconds for temporal types (such as datetime
or date). The new val_temporal_unit
configuration value can now be used to force validation as seconds, milliseconds, or by inferring as before.
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class Model(BaseModel):
d: datetime
model_config = ConfigDict(val_temporal_unit='milliseconds')
print(Model(d=datetime(1970, 4, 11, 19, 13).timestamp() * 1000))
#> d=datetime.datetime(1970, 4, 11, 18, 13)), would be somewhere around year 2245 in 'infer' mode.
A new ser_json_temporal configuration
value is also introduced, generalizing the existing ser_json_timedelta
one.
Contributed by @ollz272. PR references:
exclude_if field option
A new exclude_if option was added, which can be used at the field level.
from pydantic import BaseModel, Field
class Transaction(BaseModel):
id: int
value: int = Field(ge=0, exclude_if=lambda v: v == 0)
print(Transaction(id=1, value=0).model_dump())
#> {'id': 1}
Contributed by @andresliszt. PR references:
ensure_ascii JSON serialization option
A new ensure_ascii option was added to the JSON serialization methods
(such as model_dump_json()), to ensure non-ASCII characters will be Unicode-encoded. For backwards compatibility, this option defaults to False.
from pydantic import TypeAdapter
ta = TypeAdapter(str)
ta.dump_json('🔥', ensure_ascii=True)
#> b'"\\ud83d\\udd25"'
PR reference: pydantic-core#1689.
extra configuration per validation
It is now possible to control the extra behavior per validation call:
from pydantic import BaseModel
class Model(BaseModel):
x: int
model_config = ConfigDict(extra='allow')
# Validates fine:
m = Model(x=1, y='a')
# Raises a validation error:
m = Model.model_validate({'x': 1, 'y': 'a'}, extra='forbid')
Contributed by @anvilpete. PR references:
Changes
This release contains some minor changes that may affect existing code. Make sure to go over through them before upgrading.
Error when using incompatible pydantic-core versions
Pydantic is meant to work with one and only one pydantic-core version. However, many users reported errors caused by an incompatible
pydantic-core version being used. Starting in 2.12, Pydantic raises an explicit error at startup if this is the case. Note that most
dependency bots (such as GitHub's Dependabot) do not understand the pydantic-core exact constraint,
which might be the source of such issues.
PR reference: #12196.
Remove warning for experimental features
Pydantic has a set of experimental features, exposed under the pydantic.experimental
module. Until now, a PydanticExperimentalWarning was emitted whenever the module was imported. We decided to remove this warning, as the module
name already conveys that it is experimental. As such, it is no longer required to filter the warning
on import.
PR reference: #12265.
Field changes
Small changes and bug fixes affect Pydantic fields and the Field() function.
-
Warning emitted when field-specific metadata is used in invalid contexts:
Using field-specific metadata (e.g.
aliasorexclude) in invalid contexts will now raise a warning. Previously, it was silently ignored and was a common source of confusion. In particular, these two examples don't behave as expected:from typing import Annotated, Optional from pydantic import BaseModel, Field # ❌ `alias` can't be used on type aliases: type AliasedInt = Annotated[int, Field(alias='b')] class Model(BaseModel): a: AliasedInt # ❌ Instead use: Annotated[Optional[int], Field(exclude=True)] c: Optional[Annotated[int, Field(exclude=True)]](for type aliases, see more details in the documentation).
-
Refactor of the
FieldInfoclass:The
FieldInfoclass (created by theField()function and used to store information about each field) underwent a complete refactor to fix many related bugs. While this shouldn't affect anything in theory, there is always a small change that it can break some edge cases. Do not hesitate to report any issues that may be occur from this refactor.As a result, the undocumented
FieldInfo.merge_field_infos()is deprecated. If you made use of this function previously, please open an issue to discuss potential alternatives. -
Dataclasses fields inconsistencies:
Two small inconsistencies were found when mixing dataclasses and the Pydantic [
Field()] function. These two bugs were fixed in 2.12, but may result in a behavior change in your code. The two issues can be found in issue #12045.
PR references:
Unify serialize_as_any/SerializeAsAny behavior
Pydantic provides a way to serialize values as if they were typed as Any. In this case, Pydantic does not make use of the type annotation
to infer how to serialize the value, but instead inspects the actual type of the value to do so.
This serialize_as_any behavior is useful when you want "duck typing" behavior with model subclasses
(as per the documentation):
from pydantic import BaseModel, SerializeAsAny
class User(BaseModel):
name: str
class UserLogin(User):
password: str
class OuterModel(BaseModel):
user: User
user = UserLogin(name='pydantic', password='password')
print(OuterModel(user=user).model_dump(serialize_as_any=True))
"""
{
'user': {'name': 'pydantic', 'password': 'password'}
}
"""
If serialize_as_any wasn't set, the password field wouldn't have been included in the output, because it isn't present in the User annotation.
Such behavior can also be enabled at the field level, by using annotating user as SerializeAsAny[User] instead.
Before 2.12, the serialize_as_any parameter was behaving quite differently from the
SerializeAsAny annotation,
and such behavior has been unified in this release. This may result in serialization errors when using the serialize_as_any flag,
which would have happened already if using the SerializeAsAny annotation. To mitigate the issue, you can apply the SerializeAsAny
annotation only on the relevant fields (as serialize_as_any will apply the behavior to every value, which in most cases isn't wanted).
If you still require serialize_as_any to be set, please refer to this issue.
JSON Schema changes
While not breaking, some JSON Schema changes in this release might affect your tests if you make assertions on the generated JSON Schemas for your data. Here are the potential changes thay may affect you:
- Add regex patterns to JSON schema for
Decimaltype (contributed by @Dima-Bulavenko in #11987). - Respect custom title in functions JSON Schema (in #11892).
- When manually creating TypedDict schemas, the
extra_behaviorkey is now used to populate theadditionalPropertieskeyword.
After model validators
Model after validators are documented as being instance methods. However, class methods used to be accepted:
class Model(BaseModel):
@model_validator(mode='after')
@classmethod
def validator(cls, model, info): ...
Starting in 2.12, using this signature will now raise a deprecation warning. Instead, make sure to define the validator as an instance method:
class Model(BaseModel):
@model_validator(mode='after')
def validator(self, info): ...
PR references:
Mypy version support
Until now, Pydantic supported the mypy versions released less than 6 months ago to work with the mypy plugin. This incurred extra maintenance cost on our side, and as such we now only explicitly support the latest mypy released version.
PR reference: #11832.
Disable virtual subclassing capabilities on Pydantic models
The CPython implementation has a long-standing performance issue when defining a large
number of subclasses of an abstract base class (to be able to define abstract Pydantic models, the BaseModel class uses
abc.ABCMeta as a metaclass and as such is affected).
To work around this issue, Pydantic implemented partial optimization in isinstance()/issubclass() model checks. However, this ended up
breaking virtual subclasses (see the register() method). The partial
optimization still resulted in performance and memory issues, and was not working correctly with some logic from the
unittest.mock module.
As such, we restored the isinstance()/issubclass() behavior to how it is normally implemented, and using the
register() method now raises a warning. Doing
so also improves validation performance slightly.
PR reference: #11669.