Skip to main content
/Pydantic Validation

Announcement: Pydantic v2.14 Release

15 mins
As Markdown

Pydantic v2.14 was released on October 8th. You can install it now from PyPI:

pip install --upgrade pydantic

This release features the work of 13 external contributors and provides various new features, performance improvements, and bug fixes. 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.

This release drops support for Python 3.9 and adds support for Python 3.15.

Highlights include:

You can see the full changelog on GitHub.

Pydantic 2.14 officially supports Python 3.15, including most of the new features it introduces. These are covered first.

The MISSING sentinel, added as an experimental feature in 2.12, is now stabilized and part of the main Pydantic API.

MISSING is a singleton indicating that 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 MISSING, BaseModel


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'}

# `is` can be used to discriminate between the sentinel and other values:
timeout = conf.timeout if conf.timeout is not MISSING else defaults['timeout']

PR reference: #13782.

Lazy imports were introduced by PEP 810 in Python 3.15.

Lazy imports let models in separate modules reference each other without running into cyclic imports:

a.py:

lazy from .b import B

from pydantic import BaseModel


class A(BaseModel):
    b: B | None = None

b.py:

lazy from .a import A

from pydantic import BaseModel


class B(BaseModel):
    a: A | None = None

See the documentation for more details.

PR reference: #13776.

typing.TypeForm was introduced by PEP 747 in Python 3.15, and is supported by major static type checkers.

Pydantic now uses TypeForm in a number of places, in particular with TypeAdapter:

from typing import reveal_type

from pydantic import TypeAdapter

ta = TypeAdapter(int | str)

validated = ta.validate_python(1)

reveal_type(validated)
# In <= 2.13: Unknown
# In >= 2.14: int | str

Type checkers now infer the correct type for any type form passed to Pydantic (x | y unions, Annotated[...] forms, etc.), not just classes.

PR reference: #13459.

The frozendict type was introduced by PEP 814 in Python 3.15. Pydantic supports it, and it validates and serializes like a regular dict. See the documentation for more details.

PR reference: #13634.

create_model() now accepts a __namespace__ argument, to add any attribute to the class namespace of the created model, such as validators, methods or computed fields:

from pydantic import computed_field, create_model


def full_name(self) -> str:
    return f'{self.first_name} {self.last_name}'


UserModel = create_model(
    'UserModel',
    first_name=str,
    last_name=str,
    __namespace__={'full_name': computed_field(property(full_name))},
)

UserModel(first_name='John', last_name='Doe').model_dump()
#> {'first_name': 'John', 'last_name': 'Doe', 'full_name': 'John Doe'}

The existing __validators__ argument is limited to validators. It is now recommended to use __namespace__ instead, as __validators__ will be deprecated in v3.

PR reference: #13895.

Several supported types now have their own core schema, meaning they are natively validated/serialized by the pydantic-core Rust component. Previously, Pydantic defined custom Python validators for them, which were slower and didn't support constraints.

The following types were migrated:

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

This release introduces a number of JSON Schema changes that may affect your generated model schemas.

Decimal pattern

In 2.12, #11987 added regex patterns in the JSON Schema for Decimal types. This caused a number of issues, as some users define validation contracts based on the generated schema.

For this reason, the pattern is no longer included by default. A custom GenerateJsonSchema subclass can be defined to enable the pattern again (see the documentation for more details).

PR reference: #13672.

TypeAdapter config

The configuration of the TypeAdapter is now used when generating the JSON Schema:

from pydantic import ConfigDict, TypeAdapter

ta = TypeAdapter(list[bytes], config=ConfigDict(ser_json_bytes='base64'))
ta.json_schema()
#> {'type': 'array', 'items': {'type': 'string', 'format': 'base64url'}}

PR reference: #13676.

Other JSON Schema changes

These changes are mostly bug fixes that make the JSON Schema more consistent with the validation/serialization behavior:

  • Don't apply serialization temporal formats to validation JSON Schemas in #13711.
  • Reflect str_min_length and str_max_length config in the JSON Schema in #13714.
  • Use the field name in validation JSON Schemas when validate_by_alias is False in #13717.
  • Fix config propagation of stdlib dataclasses and TypedDicts in JSON Schema in #13891.
  • Don't apply ser_json_timedelta to all datetime types in serialization inference in #13892.
  • Encode JSON Schema defaults with a consistent configuration in #13893.

Prior to 2.14, the model_config attribute could be mutated by Pydantic, e.g. to populate values from deprecated settings. This is no longer the case: model_config will always reflect what was set by the user (merged with the configuration of parent classes):

from pydantic import BaseModel


class Base(BaseModel):
    model_config = {'title': 'MyBase'}


class Model(Base):
    model_config = {'populate_by_name': True}


Model.model_config
#> In <= 2.13: {'title': 'MyBase', 'populate_by_name': True, 'validate_by_alias': True, 'validate_by_name': True}
#> In >= 2.14: {'title': 'MyBase', 'populate_by_name': True}

PR reference: #13825.

When validate_by_alias is set to False, the generated __signature__ of models and Pydantic dataclasses now uses the field name instead of the alias, matching what validation actually accepts:

import inspect

from pydantic import BaseModel, ConfigDict, Field


class Model(BaseModel):
    model_config = ConfigDict(validate_by_alias=False, validate_by_name=True)

    my_field: int = Field(alias='myAlias')


inspect.signature(Model)
#> In <= 2.13: (*, myAlias: int) -> None
#> In >= 2.14: (*, my_field: int) -> None

Contributed by @jaideeppyne. PR reference: #13730.

Using a non-positive value for multiple_of now raises an error when the model is defined. Previously, an unhandled runtime exception was raised during validation.

PR reference: #13862.

In the experimental pipeline API, numeric constraints (gt(), ge(), lt(), le() and multiple_of()) are now always applied natively by pydantic-core on numeric types. Previously, some of them were applied as Python validators, e.g. when chaining several constraints or when the constraint value didn't match the validated type. As a result, these constraints are now included in the JSON Schema, and validation errors are the same as for regular fields:

from typing import Annotated

from pydantic import TypeAdapter
from pydantic.experimental.pipeline import validate_as

ta = TypeAdapter(Annotated[int, validate_as(int).ge(1).le(100)])

ta.json_schema()
#> In <= 2.13: {'minimum': 1, 'type': 'integer'}
#> In >= 2.14: {'maximum': 100, 'minimum': 1, 'type': 'integer'}

ta.validate_python(200)
"""
In <= 2.13:
  Value error, Expected <= 100 [type=value_error, input_value=200, input_type=int]
In >= 2.14:
  Input should be less than or equal to 100 [type=less_than_equal, input_value=200, input_type=int]
"""

PR reference: #13516.

The handler of wrap serializers takes an optional second index_key argument, which must be an integer or a string. A previous docstring example wrongly passed the info argument instead, which went unnoticed unless include or exclude was used. Such invalid values now raise an explicit error:

from pydantic import BaseModel, model_serializer


class Model(BaseModel):
    a: int

    @model_serializer(mode='wrap')
    def ser_model(self, handler, info):
        return handler(self, info)  # should be `handler(self)`


Model(a=1).model_dump()
"""
In <= 2.13:
  {'a': 1}
In >= 2.14:
  PydanticSerializationError: Error calling function `ser_model`: TypeError: 'index_key' is expected to be an integer or a string, got 'SerializationInfo(...)'
"""

If you copied this pattern, remove the second argument when calling the handler.

PR reference: #13506.

2.14 includes several optimizations to schema generation. Most of them individually reduce model build time by 5–20% on our benchmarks, which adds up at import time for applications that define many models:

  • Optimize type lookup logic in core schema generation in #13573.
  • Refactor type references logic in #13643.
  • Move schema gathering logic to pydantic-core in #13725.
  • Improve performance of FieldInfo construction in #13726.
  • Improve performance of GenerateSchema.generate_schema() dispatching in #13614.
  • Avoid exponential core schema traversal in gather_schemas_for_cleaning() in #13523.
  • Introduce micro-optimizations for model class building in #13540.

Ready to try the new features? Upgrade now with pip install --upgrade pydantic or uv add --upgrade pydantic and check out the full release notes. Have questions or feedback? Join the conversation on our community Slack.

See more from Pydantic in Google Search

Add Pydantic as a preferred source so our latest articles are easier to find.

Choose Pydantic as a preferred source on Google (opens in a new tab)