cheat sheet
pydantic
Package-level reference for Pydantic on PyPI — install variants, v1 vs v2 split, optional extras, and alternatives.
pydantic
What it is
pydantic is a runtime data-validation library created by Samuel Colvin in 2017 and now developed by Pydantic Inc., which provides commercial support and adjacent products (Logfire, pydantic-ai). Pydantic v2, released in 2023, rewrote the validation core in Rust (via pydantic-core), delivering roughly an order-of-magnitude speedup over v1.
Reach for Pydantic whenever you need to enforce a typed schema at runtime — request/response models in FastAPI/Litestar, config-from-env via pydantic-settings, or arbitrary JSON parsing. Reach for msgspec when raw throughput matters more than ecosystem reach, or attrs when you specifically want plain-Python class semantics without validation.
Install
pip install pydantic
Output: (none — exits 0 on success)
uv add pydantic
Output: dependency resolved + added to pyproject.toml
poetry add pydantic
Output: updated lockfile + virtualenv install
pip install "pydantic[email]" # adds email-validator for EmailStr
pip install "pydantic[timezone]" # adds tzdata for cross-platform tz names
Output: Pydantic plus the named optional validator dependency
pip install "pydantic-settings" # separate package for .env / settings management
Output: Pydantic-Settings installed alongside Pydantic
Versioning & Python support
- Current line is the
2.xseries (as of mid-2026). Pydantic2.0shipped in mid-2023 and was a near-complete rewrite —1.xAPI is preserved underpydantic.v1for migration, but new projects should use the2.xAPI directly. 1.xis in long-tail maintenance mode — security fixes only, no new features.- Supports Python 3.9+ on recent
2.xreleases. The Rust core means a binary wheel is shipped for all common platforms; pure-Python fallback exists but is much slower. - Strict semver since
2.0— minor releases (2.10,2.11, …) are additive.
Package metadata
- Maintainer: Pydantic Inc. (commercial entity) + community contributors
- Project home: github.com/pydantic/pydantic
- Docs: docs.pydantic.dev
- PyPI: pypi.org/project/pydantic
- License: MIT
- Governance: company-backed (Pydantic Inc., funded 2023), open source under MIT
- First released: 2017
- Downloads: ~200M+/month — one of the most-downloaded packages on PyPI; pulled in by FastAPI, Litestar, LangChain, OpenAI SDK, and most modern Python frameworks
Optional dependencies & extras
pydantic[email]— installsemail-validatorfor theEmailStrandNameEmailtypes.pydantic[timezone]— installstzdata(Windows-only by default; required there for IANA timezone names).
Core dependencies pulled in automatically:
pydantic-core— the Rust-backed validation engine (shipped as a precompiled wheel)annotated-types—typing.Annotatedconstraint primitivestyping-extensions— backports for newer typing features
Sibling packages (not extras — separate installs):
pydantic-settings—BaseSettingsfor.env/ env-var config. Was part ofpydanticin1.x; split into its own package in2.0.pydantic-extra-types— domain types:Color,PhoneNumber,PaymentCardNumber,Coordinate, currency codes, country codes, etc.pydantic-ai— agent framework built on Pydantic (by the same org)logfire— observability platform by Pydantic Inc.
Alternatives
| Package | Trade-off |
|---|---|
msgspec | Faster than Pydantic v2 for hot paths; smaller surface; less ecosystem integration. Use for high-throughput serialization. |
attrs | Plain-Python classes with __init__/__repr__/comparison; no validation by default. Use when you want classes, not schemas. |
dataclasses (stdlib) | Zero-dependency, no validation. Use for internal data carriers without external boundaries. |
marshmallow | Schema-first (define schema separately from class); the pre-Pydantic standard. Use in legacy Flask codebases. |
cerberus | Dict-based validation. Use when you want schema-validate raw dicts without classes. |
voluptuous | Lightweight dict validation. Use for small CLI / config validators. |
Common gotchas
v1vsv2is a major API change.BaseModel.parse_obj→model_validate,.dict()→model_dump(),Configclass →model_config = ConfigDict(...),@validator→@field_validator. Tutorials older than 2023 are likely v1. Pin a major (pydantic>=2,<3).- FastAPI version mismatch.
fastapi<0.100requirespydantic<2;fastapi>=0.100requirespydantic>=2. Same constraint for Litestar and pydantic-settings — pin them together. pydantic-settingsis a separate package now. In1.xit wasfrom pydantic import BaseSettings; in2.xit'sfrom pydantic_settings import BaseSettings. A missingpip install pydantic-settingsproduces anImportError.- Strict vs lax coercion. By default
2.xis lax —int("5")succeeds,int("5.5")does not, etc. Enablemodel_config = ConfigDict(strict=True)for per-model strict mode or useAnnotated[int, Strict()]per-field. Optional[X]no longer implies a default ofNone. In1.x,field: Optional[int]defaulted toNone. In2.xyou must writefield: Optional[int] = Noneexplicitly.- Validators run during construction. Avoid heavy I/O in
@field_validator/@model_validator— every instantiation re-runs them. For one-time normalization, use a@classmethodconstructor. - JSON Schema generation differs by version.
2.xemits OpenAPI 3.1 / JSON Schema Draft 2020-12 by default; some legacy consumers expect Draft 7. Usemode="serialization"and the appropriateby_aliasflag to match what FastAPI emits. - Rust wheel availability.
pydantic-corewheels cover all common platforms, but exotic ones (musllinux on uncommon arches, some BSDs) fall back to a slow pure-Python build. Checkpip installoutput if startup feels unusually slow.
Version migration guide
The v1 → v2 rewrite in 2023 is one of the largest API breaks in any popular Python package. Almost every API surface moved or renamed. The compatibility shim pydantic.v1 lets you import the old API by changing the import line, but new code should target v2 directly.
Top-level API renames:
| v1 | v2 |
|---|---|
class Config: orm_mode = True | model_config = ConfigDict(from_attributes=True) |
class Config: allow_population_by_field_name = True | model_config = ConfigDict(populate_by_name=True) |
Model.parse_obj(data) | Model.model_validate(data) |
Model.parse_raw(json_str) | Model.model_validate_json(json_str) |
instance.dict() | instance.model_dump() |
instance.json() | instance.model_dump_json() |
Model.schema() | Model.model_json_schema() |
Model.update_forward_refs() | Model.model_rebuild() |
@validator("field") | @field_validator("field") + @classmethod |
@root_validator | @model_validator(mode="before" | "after") |
Field(..., env="MY_VAR") | moved to pydantic-settings |
from pydantic import BaseSettings | from pydantic_settings import BaseSettings |
Validator migration example:
# v1
from pydantic import BaseModel, validator, root_validator
class User(BaseModel):
name: str
email: str
@validator("email")
def lowercase_email(cls, v):
return v.lower()
@root_validator
def check_consistency(cls, values):
if not values["name"]: raise ValueError("name required")
return values
# v2
from pydantic import BaseModel, field_validator, model_validator
class User(BaseModel):
name: str
email: str
@field_validator("email")
@classmethod
def lowercase_email(cls, v: str) -> str:
return v.lower()
@model_validator(mode="after")
def check_consistency(self) -> "User":
if not self.name: raise ValueError("name required")
return self
Output: equivalent validation; v2 model_validator(mode="after") operates on the constructed instance, mode="before" on the raw dict.
Optional default behavior:
# v1 — Optional implied default=None
class M(BaseModel):
x: Optional[int] # default None automatically
# v2 — must be explicit
class M(BaseModel):
x: Optional[int] = None
Output: v2 raises ValidationError on construction without x unless the default is declared.
Migration playbook:
pip install "pydantic>=2"
pip install pydantic-settings # if you used BaseSettings
# Use pydantic's bundled codemod
pip install bump-pydantic
bump-pydantic mypackage/
Output: the bump-pydantic tool rewrites most v1 patterns automatically; manual review still required for root_validator and computed fields.
Performance tuning
Pydantic v2's pydantic-core Rust engine is roughly 5-50× faster than v1, but you can still leave performance on the table with bad config choices. The hot path on most apps is request-deserialization in a web handler.
- Use
model_validate_json(bytes_or_str)overmodel_validate(json.loads(...)). The former passes raw bytes to Rust; the latter pays the Python-dict allocation cost first. - Strict mode skips coercion.
model_config = ConfigDict(strict=True)avoids "is this string actually an int?" branches; faster when callers already send the right types. computed_fieldis recomputed on every dump. Cache the result yourself if expensive, or use a@cached_propertyand exclude it from serialization.Field(validate_default=True)opts in to default-value validation. Off by default for speed; turn on only when defaults come from user input.- TypedDict instead of BaseModel. For internal transit between functions,
TypedDictskips the validator path entirely; useBaseModelonly at the boundary. msgspec.Structis faster than Pydantic for the hottest paths — 2-5× on serialization, 5-10× on deserialization. Pydantic-supporting frameworks (Litestar) accept both.model_dump(mode="json")vsmode="python"— choosejsonwhen sending the result over the wire (numbers, datetimes serialized to JSON-native forms);pythonwhen handing to another Pydantic model.- Discriminated unions are O(1). Annotated unions with
Field(discriminator="kind")dispatch on a single key rather than trying every variant; use them for polymorphic payloads.
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
class Dog(BaseModel):
kind: Literal["dog"]
bark: bool
class Cat(BaseModel):
kind: Literal["cat"]
purr: bool
Pet = Annotated[Union[Dog, Cat], Field(discriminator="kind")]
Output: validation dispatches on kind only — no fallback through Dog before Cat.
Real-world recipes
Patterns that come up the first week of any Pydantic v2 codebase — settings classes, custom serialization, computed fields, and tagged unions.
# Recipe 1 — Pydantic Settings v2 with nested env vars
from pydantic_settings import BaseSettings, SettingsConfigDict
class DBConfig(BaseSettings):
host: str = "localhost"
port: int = 5432
user: str
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter="__")
debug: bool = False
db: DBConfig
settings = Settings()
# .env: DEBUG=true, DB__USER=alice, DB__HOST=db
Output: nested config via DB__USER env vars; .env loaded on instantiation.
# Recipe 2 — Custom serializer per field
from pydantic import BaseModel, field_serializer
from datetime import datetime
class Event(BaseModel):
name: str
ts: datetime
@field_serializer("ts")
def fmt_ts(self, ts: datetime) -> str:
return ts.isoformat() + "Z"
Output: model_dump() returns ts as ISO-8601 with explicit Z suffix.
# Recipe 3 — model_validator for cross-field rules
from pydantic import BaseModel, model_validator
class Range(BaseModel):
low: int
high: int
@model_validator(mode="after")
def check_order(self) -> "Range":
if self.low > self.high:
raise ValueError("low must be <= high")
return self
Output: ValidationError on Range(low=10, high=5); passes for valid ranges.
# Recipe 4 — Dataclasses with Pydantic validation
from pydantic.dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(x="1.5", y="2.5") # strings coerced
Output: stdlib-style dataclass with full Pydantic validation; useful when you want __dataclass_fields__ introspection.
# Recipe 5 — TypeAdapter for non-Model types (lists, dicts, unions)
from pydantic import TypeAdapter
from typing import List
UserList = TypeAdapter(List[dict])
users = UserList.validate_python([{"id": 1}, {"id": 2}])
Output: validates a top-level list without wrapping in a BaseModel.
Ecosystem integrations
Pydantic is a foundational dependency — most modern Python web/data libraries are built on top of it. Key integrations to be aware of:
pydantic-settings—.env/ env-var driven config. Was core in v1, separate package in v2.pydantic-extra-types— additional domain types:Color,PhoneNumber,PaymentCardNumber, country codes, coordinates.pydantic-ai— agent framework by Pydantic Inc., builds on Pydantic for tool calling.logfire— observability platform from Pydantic Inc.; auto-instruments Pydantic validation.fastapi/litestar— request/response models use Pydantic by default.sqlmodel— Pydantic + SQLAlchemy in one model class.sqlalchemy 2.x— has Pydantic integration viafrom_attributes=True(formerlyorm_mode).openai/anthropicSDKs — use Pydantic for typed responses.langchain— built on Pydantic for tool definitions and structured output.bump-pydantic— codemod tool for v1 → v2 migration.datamodel-code-generator— generate Pydantic models from JSON Schema, OpenAPI, JSON sample.
When NOT to use this
Pydantic is overkill — or actively counterproductive — in a few scenarios:
- Internal data carriers between trusted functions. A
dataclass(stdlib) orattrsclass avoids the validation cost when there's no external boundary. - CPU-bound hot loops parsing millions of records.
msgspec.Structis 2-10× faster; Pydantic's lazy field validation still has overhead. Usemsgspecfor ETL hot paths and Pydantic at the API edge. - Schema-less or rapidly-evolving JSON. If the shape isn't known ahead of time,
dict+ targeted assertions are simpler. - Pure-Python environments where you can't ship a Rust wheel. Some restricted environments forbid binary deps; pure-Python Pydantic exists but is slow — consider
marshmallowordataclassesinstead. - Tiny scripts. A 50-line CLI doesn't need a validation framework;
argparse+ type hints are enough.
Testing & CI integration
Pydantic tests are mostly assertions against ValidationError shape and model_dump() output. Pydantic ships well-defined error structures that make assertions stable.
# pytest — asserting on ValidationError shape
import pytest
from pydantic import BaseModel, ValidationError
class M(BaseModel):
x: int
def test_invalid():
with pytest.raises(ValidationError) as exc:
M(x="not-an-int")
errs = exc.value.errors()
assert errs[0]["loc"] == ("x",)
assert errs[0]["type"] == "int_parsing"
Output: stable error shape; errors() returns a list of typed dicts.
# Schema regression test — guard against accidental breakages
def test_schema_stable():
schema = M.model_json_schema()
assert schema["properties"]["x"]["type"] == "integer"
Output: any future change to M that breaks the JSON schema fails the test.
CI checklist: pin pydantic-core along with pydantic (the Rust wheel may have its own breaking changes); run python -W error::PydanticDeprecatedSince20 to catch v1 leakage in your codebase.
Compatibility matrix
| Python | pydantic line | Notes |
|---|---|---|
| 3.8 | 2.0-2.5 | Floor of early v2 releases. |
| 3.9 | 2.6+ | Current minimum on recent releases. |
| 3.10 | 2.6+ | Pattern matching usable in validators. |
| 3.11 | 2.6+ | Best perf via tagged unions improvements. |
| 3.12 | 2.7+ | Type-parameter syntax (PEP 695) supported. |
| 3.13 | 2.10+ | Latest stable. |
Pair compatibility:
fastapi <0.100— requirespydantic<2.fastapi >=0.100— requirespydantic>=2.litestar 2.x— supports both v1 and v2 (v1 deprecated path).pydantic-settings— must matchpydanticmajor.sqlmodel— historically lagged pydantic v2; modern versions support both.
See also
- Python: pydantic — API tutorial, BaseModel, validators, serialization
- Concept: JSON — JSON data fundamentals
- Packages: pip-fastapi — the canonical Pydantic consumer
- Packages: pip-litestar — another Pydantic-aware framework