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.
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.
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
- You just need lightweight typed classes with no external-data parsing; dataclasses or attrs do that with less magic and no compiled dependency
- You are on a hot pure-serialization path where every microsecond counts; msgspec benchmarks faster for plain encode/decode work
- Your codebase is full of Pydantic v1 idioms; v2 renamed most of the API (.dict() to model_dump(), @validator to @field_validator) and migrating a large app is real work, not a find-and-replace
- You expect validation on mutation: models do not revalidate on attribute assignment unless you enable validate_assignment, so a validated object can silently drift invalid
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 intsMutable 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 selfmode='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 envBaseSettings 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 coercingDefault lax mode coerces '123' to 123; strict mode rejects it, and you can also set strict per field via Field(strict=True).
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| msgspec | PyPI | Raw JSON encode/decode speed matters more than validator features |
| attrs | PyPI | You want plain typed classes without runtime coercion or a compiled core |
| marshmallow | PyPI | You prefer schema classes kept separate from your domain objects |