mrkeyoor.com_
Wed 05 Aug 05:05 UTC
PyPIUtilsupdated 05 Aug 2026

pydantic

Pydantic validates data using standard Python type hints: declare a class with annotated fields and it parses incoming dicts or JSON into typed objects, coercing '123' to 123, raising structured errors with exact field paths, and serializing back out. The heavy lifting happens in pydantic-core, a compiled Rust extension, which is why v2 is fast. It sits underneath a large slice of the Python ecosystem: FastAPI request models, LLM structured output, settings management, and config parsing all speak Pydantic.

Verdict

The de facto standard for data validation in Python and a safe dependency: fast, thoroughly documented, and everywhere. Learn the v2 idioms first and ignore tutorials written before 2023.

API stability4/5The v2 API has been stable since 2023 and v1 compatibility ships inside the package (from pydantic import v1), but the v1 to v2 break was large and outdated examples still circulate widely.
Docs5/5docs.pydantic.dev has a full concepts guide, API reference, migration guide, and an llms.txt; validation errors link to documented error codes.
Maintenance5/5Developed by a funded company (Pydantic Inc.) with a full-time team, daily pushes, and frequent patch releases; 2.13.4 current at review time.
Ecosystem5/5FastAPI, the OpenAI SDK, LangChain, and thousands of other packages build on it; pydantic-settings and pydantic-extra-types extend the core, and roughly 274M weekly downloads speak for adoption.

Use it if

  • You accept untrusted JSON or dict input (APIs, webhooks, LLM output) and want typed objects with precise, machine-readable validation errors
  • You use FastAPI or another framework that already builds on Pydantic models
  • You need JSON Schema output: model_json_schema() feeds OpenAPI docs and LLM tool definitions directly
  • You want typed environment and settings parsing via the companion pydantic-settings package
Skip it if

Setup reality

pip install pydantic pulls prebuilt pydantic-core wheels for every common platform, so installation is painless; only unusual targets ever compile Rust. The real setup cost is conceptual: learning the v2 spelling (model_validate, model_dump, Field, ConfigDict) when a decade of StackOverflow answers teach v1, remembering that BaseSettings moved to the separate pydantic-settings package, and that email validation needs the pydantic[email] extra. Python 3.9+ is required.

Patterns

Define and instantiate a modeldefine-model

from datetime import datetime
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str = 'John Doe'
    signup_ts: datetime | None = None
    friends: list[int] = []

user = User(id='123', friends=[1, '2'])  # coerced to ints

Mutable defaults like [] are safe here; Pydantic copies them per instance, unlike plain dataclasses.

Validate a dict or raw JSONvalidate-external-data

user = User.model_validate({'id': 123, 'name': 'Ana'})
user = User.model_validate_json('{"id": 123, "name": "Ana"}')

model_validate_json parses JSON inside the Rust core; it beats json.loads followed by model_validate.

Dump a model to dict or JSONserialize

user.model_dump()                      # dict
user.model_dump_json()                 # JSON string
user.model_dump(exclude_none=True, by_alias=True)

The v1 names .dict() and .json() are gone in v2; model_dump_json returns str, not bytes.

Constrain fields with Fieldfield-constraints

from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0)
    sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')

Constraints run at validation time only; later attribute assignment skips them unless validate_assignment is enabled.

Add a custom field validatorcustom-field-validator

from pydantic import BaseModel, field_validator

class User(BaseModel):
    username: str

    @field_validator('username')
    @classmethod
    def no_spaces(cls, v: str) -> str:
        if ' ' in v:
            raise ValueError('no spaces allowed')
        return v.lower()

Return the (possibly transformed) value; a raised ValueError becomes a ValidationError with the field's location.

Validate across fieldsmodel-validator

from pydantic import BaseModel, model_validator

class Range(BaseModel):
    low: int
    high: int

    @model_validator(mode='after')
    def check_order(self):
        if self.low > self.high:
            raise ValueError('low must be <= high')
        return self

mode='after' receives the constructed model; mode='before' receives the raw input dict instead.

Nest modelsnested-models

class Address(BaseModel):
    city: str
    zip_code: str

class User(BaseModel):
    name: str
    addresses: list[Address]

u = User.model_validate({
    'name': 'A',
    'addresses': [{'city': 'Pune', 'zip_code': '411001'}],
})

Nested dicts validate recursively, and errors carry the full path, like addresses.0.city.

Load settings from environment variablessettings-from-env

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    debug: bool = False

settings = Settings()  # reads DATABASE_URL and DEBUG from the env

BaseSettings lives in the separate pydantic-settings package since v2; importing it from pydantic raises.

Discriminated unionsdiscriminated-union

from typing import Literal, Union
from pydantic import BaseModel, Field

class Cat(BaseModel):
    pet_type: Literal['cat']
    meows: int

class Dog(BaseModel):
    pet_type: Literal['dog']
    barks: int

class Owner(BaseModel):
    pet: Union[Cat, Dog] = Field(discriminator='pet_type')

The discriminator keeps union validation fast and error messages readable; without it every branch gets tried.

Validate non-model types with TypeAdaptervalidate-arbitrary-types

from pydantic import TypeAdapter

adapter = TypeAdapter(list[dict[str, int]])
data = adapter.validate_python([{'a': '1'}])
schema = adapter.json_schema()

Build the adapter once at module level, not per call; construction is the expensive part.

Emit JSON Schemajson-schema

schema = User.model_json_schema()

This is the same output FastAPI uses for OpenAPI and what you hand to LLM tool-calling APIs.

Turn off type coercionstrict-mode

from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    id: int  # '123' now fails instead of coercing

Default lax mode coerces '123' to 123; strict mode rejects it, and you can also set strict per field via Field(strict=True).

Alternatives

PackageRegistryPick it when
msgspecPyPIRaw JSON encode/decode speed matters more than validator features
attrsPyPIYou want plain typed classes without runtime coercion or a compiled core
marshmallowPyPIYou prefer schema classes kept separate from your domain objects