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

bash
pip install pydantic

Output: (none — exits 0 on success)

bash
uv add pydantic

Output: dependency resolved + added to pyproject.toml

bash
poetry add pydantic

Output: updated lockfile + virtualenv install

bash
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

bash
pip install "pydantic-settings"      # separate package for .env / settings management

Output: Pydantic-Settings installed alongside Pydantic

Versioning & Python support

  • Current line is the 2.x series (as of mid-2026). Pydantic 2.0 shipped in mid-2023 and was a near-complete rewrite — 1.x API is preserved under pydantic.v1 for migration, but new projects should use the 2.x API directly.
  • 1.x is in long-tail maintenance mode — security fixes only, no new features.
  • Supports Python 3.9+ on recent 2.x releases. 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] — installs email-validator for the EmailStr and NameEmail types.
  • pydantic[timezone] — installs tzdata (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-typestyping.Annotated constraint primitives
  • typing-extensions — backports for newer typing features

Sibling packages (not extras — separate installs):

  • pydantic-settingsBaseSettings for .env / env-var config. Was part of pydantic in 1.x; split into its own package in 2.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

PackageTrade-off
msgspecFaster than Pydantic v2 for hot paths; smaller surface; less ecosystem integration. Use for high-throughput serialization.
attrsPlain-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.
marshmallowSchema-first (define schema separately from class); the pre-Pydantic standard. Use in legacy Flask codebases.
cerberusDict-based validation. Use when you want schema-validate raw dicts without classes.
voluptuousLightweight dict validation. Use for small CLI / config validators.

Common gotchas

  1. v1 vs v2 is a major API change. BaseModel.parse_objmodel_validate, .dict()model_dump(), Config class → model_config = ConfigDict(...), @validator@field_validator. Tutorials older than 2023 are likely v1. Pin a major (pydantic>=2,<3).
  2. FastAPI version mismatch. fastapi<0.100 requires pydantic<2; fastapi>=0.100 requires pydantic>=2. Same constraint for Litestar and pydantic-settings — pin them together.
  3. pydantic-settings is a separate package now. In 1.x it was from pydantic import BaseSettings; in 2.x it's from pydantic_settings import BaseSettings. A missing pip install pydantic-settings produces an ImportError.
  4. Strict vs lax coercion. By default 2.x is lax — int("5") succeeds, int("5.5") does not, etc. Enable model_config = ConfigDict(strict=True) for per-model strict mode or use Annotated[int, Strict()] per-field.
  5. Optional[X] no longer implies a default of None. In 1.x, field: Optional[int] defaulted to None. In 2.x you must write field: Optional[int] = None explicitly.
  6. Validators run during construction. Avoid heavy I/O in @field_validator / @model_validator — every instantiation re-runs them. For one-time normalization, use a @classmethod constructor.
  7. JSON Schema generation differs by version. 2.x emits OpenAPI 3.1 / JSON Schema Draft 2020-12 by default; some legacy consumers expect Draft 7. Use mode="serialization" and the appropriate by_alias flag to match what FastAPI emits.
  8. Rust wheel availability. pydantic-core wheels cover all common platforms, but exotic ones (musllinux on uncommon arches, some BSDs) fall back to a slow pure-Python build. Check pip install output 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:

v1v2
class Config: orm_mode = Truemodel_config = ConfigDict(from_attributes=True)
class Config: allow_population_by_field_name = Truemodel_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 BaseSettingsfrom pydantic_settings import BaseSettings

Validator migration example:

python
# 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
python
# 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:

python
# 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:

bash
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) over model_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_field is recomputed on every dump. Cache the result yourself if expensive, or use a @cached_property and 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, TypedDict skips the validator path entirely; use BaseModel only at the boundary.
  • msgspec.Struct is 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") vs mode="python" — choose json when sending the result over the wire (numbers, datetimes serialized to JSON-native forms); python when 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.
python
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.

python
# 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.

python
# 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.

python
# 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.

python
# 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.

python
# 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 via from_attributes=True (formerly orm_mode).
  • openai / anthropic SDKs — 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) or attrs class avoids the validation cost when there's no external boundary.
  • CPU-bound hot loops parsing millions of records. msgspec.Struct is 2-10× faster; Pydantic's lazy field validation still has overhead. Use msgspec for 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 marshmallow or dataclasses instead.
  • 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.

python
# 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.

python
# 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

Pythonpydantic lineNotes
3.82.0-2.5Floor of early v2 releases.
3.92.6+Current minimum on recent releases.
3.102.6+Pattern matching usable in validators.
3.112.6+Best perf via tagged unions improvements.
3.122.7+Type-parameter syntax (PEP 695) supported.
3.132.10+Latest stable.

Pair compatibility:

  • fastapi <0.100 — requires pydantic<2.
  • fastapi >=0.100 — requires pydantic>=2.
  • litestar 2.x — supports both v1 and v2 (v1 deprecated path).
  • pydantic-settings — must match pydantic major.
  • sqlmodel — historically lagged pydantic v2; modern versions support both.

See also