/Release

Pydantic v2.12

Victorien Plot avatar
Victorien Plot
20 mins

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:

You can see the full changelog on GitHub.

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:

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.

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.

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:

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:

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:

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.

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:

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

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.

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.

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. alias or exclude) 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 FieldInfo class:

    The FieldInfo class (created by the Field() 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:

While not breaking, some JSON Schema changes in this release might break your tests if you make assertions on the generated JSON Schemas for your data. Here are the potential changes thay may affect you:

Model after validators are documented as being instance methods. However, an invalid signature used to accidentally be accepted, but could lead to undefined behavior. The following signature now raises an error:

class Model(BaseModel):
    @model_validator(mode='after')
    # This used to be converted into a classmethod, resulting
    # in this inconsistent signature still accepted:
    def validator(cls, model, info): ...

PR reference: #11957.

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.

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.