Pydantic Cheatsheet

A visual guide to Pydantic covering BaseModel fields, validating input, type coercion and Field constraints, reading a ValidationError, field and model validators, nested and union models, serialization with model_dump, and config with pydantic-settings.

python
pydantic
cheatsheet
Author

James Balamuta

Published

June 22, 2026

Pydantic is the de-facto standard for turning untrusted input into trusted, typed Python objects. You declare a schema as an ordinary class whose annotated attributes (id: int, name: str) become validated fields, and pydantic does the rest: it coerces sensible values, enforces your constraints, and raises a single ValidationError listing every problem when the input is wrong. The recurring mental model in this sheet is one picture: a raw { } dict or JSON string on the left flows through a pink validator gate (your model) and emerges as either a green typed instance or a red ValidationError. Where this looks like the requests sheet, the contrast is the point: requests fetches JSON over the wire; pydantic validates and shapes that JSON into objects you can trust. The conventional import is from pydantic import BaseModel, Field, ValidationError, and everything here is pydantic v2 (v1 spellings are flagged per section).

Complete Pydantic cheatsheet (light mode): eight panels covering declaring a BaseModel, validating input, coercing and constraining with Field, reading a ValidationError, field and model validators, nesting and reusing models, serializing out, and config with pydantic-settings.

Complete Pydantic cheatsheet (dark mode): eight panels covering declaring a BaseModel, validating input, coercing and constraining with Field, reading a ValidationError, field and model validators, nesting and reusing models, serializing out, and config with pydantic-settings.

Download the full cheatsheet

All eight panels in a single, printable SVG.

Light SVG Dark SVG

Declare a BaseModel

A pydantic model is just a class that subclasses BaseModel, where each annotated attribute becomes a validated field. Defaults and None-able types are written the normal Python way (name: str = "anon", nickname: str | None = None), you construct instances with keywords, and the result is a typed object whose attributes are guaranteed to match their annotations. User.model_fields lets you introspect the schema you declared.

Pydantic BaseModel panel: define from type hints, default value, nullable field, construct an instance, model_fields, typed attribute access.

Subclass BaseModel; your type hints become the schema.

Pydantic BaseModel panel: define from type hints, default value, nullable field, construct an instance, model_fields, typed attribute access.

Subclass BaseModel; your type hints become the schema.
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str = "anon"            # field with a default
    nickname: str | None = None   # nullable, defaults to None

u = User(id=1, name="ada")        # construct from keywords
User.model_fields                 # introspect the declared fields
u.id                              # 1   (a real int)

See Models. The v1 inner class Config: is replaced by model_config = ConfigDict(...).

Validate Input

The job of a model is to turn untrusted input into a trusted object. User.model_validate(data) validates a Python dict and User.model_validate_json(text) parses and validates raw JSON in one step. Bad input never passes silently: it raises a ValidationError. TypeAdapter extends the same validation to non-model types like list[User], and strict=True forbids type coercion.

Pydantic validate panel: validate a dict, validate JSON, construct from kwargs, validate a list with TypeAdapter, reject bad input, strict mode.

model_validate parses untrusted dicts and JSON into typed objects.

Pydantic validate panel: validate a dict, validate JSON, construct from kwargs, validate a list with TypeAdapter, reject bad input, strict mode.

model_validate parses untrusted dicts and JSON into typed objects.
User.model_validate({"id": 1, "name": "ada"})          # validate a Python dict
User.model_validate_json('{"id": 1, "name": "ada"}')   # parse + validate JSON
User(id=1, name="ada")                                 # construct from keywords

from pydantic import TypeAdapter
TypeAdapter(list[User]).validate_python(rows)          # validate non-model types

User.model_validate({"id": "oops"})                    # -> raises ValidationError
User.model_validate({"id": "1"}, strict=True)          # strict: no str -> int coercion

See Validating data. model_validate / model_validate_json replace v1 parse_obj / parse_raw.

Coerce and Constrain

By default pydantic coerces sensible values (the string "30" becomes the int 30), and Field() layers on constraints (ge, le, min_length, max_length, pattern) plus metadata like defaults and aliases. Use default_factory=list for mutable defaults so each instance gets its own fresh container, and Annotated[type, Field(...)] to attach constraints without losing the type.

Pydantic constrain panel: coerce a string, numeric bounds with ge/le, string length, default_factory, Annotated regex, validation alias.

Field() adds bounds, defaults, and metadata to a type.

Pydantic constrain panel: coerce a string, numeric bounds with ge/le, string length, default_factory, Annotated regex, validation alias.

Field() adds bounds, defaults, and metadata to a type.
from typing import Annotated
from pydantic import Field, AliasChoices

age: int                                                    # "30" coerces to 30
age: int = Field(ge=0, le=120)                              # numeric bounds
name: str = Field(min_length=1, max_length=50)             # string length
tags: list[str] = Field(default_factory=list)              # safe mutable default
zip: Annotated[str, Field(pattern=r"^\d{5}$")]             # regex via Annotated
Field(validation_alias=AliasChoices("user_id", "userId"))  # accept either key

See Fields. The v1 Field(regex=...) is now Field(pattern=...).

Read a ValidationError

When validation fails, pydantic does not stop at the first problem: it collects every error into one ValidationError. e.errors() returns a list of dicts with loc (the field path), type, msg, and input, and e.error_count() tells you how many there were. Catch it with except ValidationError as e, print it for humans, or call e.json() to ship a machine-readable report back to a caller.

Pydantic errors panel: catch the failure, error_count, structured errors table, pinpoint the loc, human-readable print, JSON error report.

One exception lists every problem with loc, msg, and type.

Pydantic errors panel: catch the failure, error_count, structured errors table, pinpoint the loc, human-readable print, JSON error report.

One exception lists every problem with loc, msg, and type.
from pydantic import ValidationError

try:
    User.model_validate({"id": "oops"})
except ValidationError as e:
    e.error_count()          # how many problems were found
    e.errors()               # [{'loc': ..., 'type': ..., 'msg': ..., 'input': ...}]
    e.errors()[0]["loc"]     # ('id',)  -> the offending field
    print(e)                 # multiline, human-readable summary
    e.json()                 # machine-readable report for a client

See Validation errors. Each error dict carries loc, type, msg, and input.

Field and Model Validators

When the built-in constraints are not enough, @field_validator("name") runs your own rule on a single field (wrap a classmethod), while @model_validator(mode="after") runs across the whole instance for cross-field rules like “passwords must match”. Use mode="before" to reshape raw input ahead of type coercion, @computed_field to expose a derived read-only property in the output, and raise ValueError(...) inside any validator to reject a value into the normal error report.

Pydantic validators panel: normalize a field, validate after coercion, cross-field model validator, before-mode pre-processing, computed_field, raise ValueError.

Hook custom rules per field or across the whole model.

Pydantic validators panel: normalize a field, validate after coercion, cross-field model validator, before-mode pre-processing, computed_field, raise ValueError.

Hook custom rules per field or across the whole model.
from pydantic import field_validator, model_validator, computed_field

@field_validator("name")
@classmethod
def normalize(cls, v: str) -> str:
    return v.strip().lower()          # per-field rule

@model_validator(mode="after")
def passwords_match(self):
    if self.password != self.password2:
        raise ValueError("passwords do not match")   # cross-field rule
    return self

@computed_field                       # derived, read-only, included in the dump
@property
def area(self) -> int:
    return self.width * self.height

See Validators. @field_validator / @model_validator replace v1 @validator / @root_validator.

Nest and Reuse Models

Models compose like any other type: annotate a field as another model (addresses: list[Address]) and pydantic validates the nested dicts all the way down into real sub-objects. Unions (int | str) accept any listed member, a discriminator makes tagged unions fast and unambiguous, inheritance reuses fields across models, and model_rebuild() resolves forward references for recursive structures.

Pydantic nested panel: nest a model, validate nested dicts, union types, discriminated union, inheritance, recursive self-reference.

Models compose: nest them, list them, union them.

Pydantic nested panel: nest a model, validate nested dicts, union types, discriminated union, inheritance, recursive self-reference.

Models compose: nest them, list them, union them.
from typing import Literal, Union
from pydantic import Field

class Person(BaseModel):
    addresses: list[Address]                       # nest one model in another
    pet: Union[Cat, Dog] = Field(discriminator="kind")   # tagged union, routed by "kind"

value: int | str                                   # union: tries each member

class Admin(User):                                 # inherit and extend
    level: int

children: list["Node"]                             # recursive; then Node.model_rebuild()

See Unions and the models guide.

Serialize Out

Going the other way, model_dump() produces a Python dict (keeping rich types like datetime) and model_dump_json() produces a JSON string (rendering those types as strings); mode="json" gives you JSON-safe values inside a dict. Trim payloads with exclude_none=True, hide sensitive fields with Field(exclude=True), and emit a standards-compliant schema for docs or OpenAPI with model_json_schema().

Pydantic serialize panel: model_dump to a dict, model_dump_json, mode json, exclude_none, Field exclude, model_json_schema.

model_dump for a dict, model_dump_json for a JSON string.

Pydantic serialize panel: model_dump to a dict, model_dump_json, mode json, exclude_none, Field exclude, model_json_schema.

model_dump for a dict, model_dump_json for a JSON string.
u.model_dump()                         # -> dict (keeps real datetime, etc.)
u.model_dump_json()                    # -> JSON string (datetimes -> ISO)
u.model_dump(mode="json")              # -> dict with JSON-safe values
u.model_dump(exclude_none=True)        # drop None fields, slimmer payload
secret: str = Field(exclude=True)      # never serialized
User.model_json_schema()               # emit JSON Schema (for OpenAPI / docs)

See Serialization. model_dump / model_dump_json replace v1 .dict() / .json().

Config with pydantic-settings

Application config is just another model: BaseSettings (from the separate pydantic-settings package in v2) reads typed values from environment variables and .env files, coercing and validating them exactly like a BaseModel. Configure the source with SettingsConfigDict(env_prefix=..., env_file=...), rely on the source-precedence ladder (init args beat env vars beat .env beat defaults), and let a bad value fail fast at startup with a ValidationError.

Pydantic settings panel: install the package, declare a BaseSettings class, read from the environment, env_prefix and env_file, source precedence ladder, validate config.

BaseSettings loads typed config from env vars and .env files.

Pydantic settings panel: install the package, declare a BaseSettings class, read from the environment, env_prefix and env_file, source precedence ladder, validate config.

BaseSettings loads typed config from env vars and .env files.
# uv add pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env")
    port: int = 8000
    database_url: str

settings = Settings()    # reads env (APP_PORT=9000), then .env, then defaults
settings.port            # 9000  (coerced str -> int)
# APP_PORT=oops -> ValidationError at startup, not deep in production

See Pydantic Settings. In v2 BaseSettings lives in the separate pydantic-settings package.

Quick Reference

Key Pydantic calls.
Command What it does Area
class M(BaseModel): x: int Declare a model from type hints Model
M.model_fields Introspect declared fields Model
M.model_validate(data) Validate a Python dict Validate
M.model_validate_json(text) Parse + validate JSON Validate
M.model_validate(data, strict=True) Validate without coercion Validate
TypeAdapter(list[M]).validate_python(rows) Validate non-model types Validate
Field(ge=0, le=120) Numeric bounds Constrain
Field(min_length=1, pattern=r"…") String length / regex Constrain
Field(default_factory=list) Safe mutable default Constrain
@field_validator("x") Custom per-field rule Validators
@model_validator(mode="after") Cross-field rule Validators
@computed_field Derived read-only field Validators
Field(discriminator="kind") Tagged union routing Nesting
M.model_dump() Serialize to a Python dict Serialize
M.model_dump_json() Serialize to a JSON string Serialize
M.model_json_schema() Emit JSON Schema Serialize
class S(BaseSettings) Typed config from env / .env Settings
What a ValidationError exposes.
Member Type Meaning
e.error_count() int How many problems were found
e.errors() list[dict] One dict per error
e.errors()[i]["loc"] tuple Field path to the bad value
e.errors()[i]["type"] str Error code, e.g. int_parsing
e.errors()[i]["msg"] str Human-readable message
e.errors()[i]["input"] any The value that failed
e.json() str Machine-readable error report
Pydantic v1 to v2 migration map.
Pydantic v1 (deprecated) Pydantic v2 (current)
Model.parse_obj(d) Model.model_validate(d)
Model.parse_raw(s) Model.model_validate_json(s)
m.dict() m.model_dump()
m.json() m.model_dump_json()
Model.schema() Model.model_json_schema()
@validator @field_validator (wraps a @classmethod)
@root_validator @model_validator
class Config: model_config = ConfigDict(...)
Field(regex=...) Field(pattern=...)
from pydantic import BaseSettings from pydantic_settings import BaseSettings

Appendix: Sample Code

The raw-input to model to output mental model

from pydantic import BaseModel, ValidationError


class User(BaseModel):
    id: int
    name: str = "anon"
    active: bool = True


# Coercion: the string "1" becomes the int 1
u = User.model_validate({"id": "1", "name": "ada"})
u.id            # 1            (a real int)
u.model_dump()  # {'id': 1, 'name': 'ada', 'active': True}

# Bad input never passes silently
try:
    User.model_validate({"id": "oops"})
except ValidationError as e:
    print(e.error_count())          # 1
    print(e.errors()[0]["type"])    # 'int_parsing'

Constraints, validators, and a computed field

from typing import Annotated
from pydantic import BaseModel, Field, field_validator, model_validator, computed_field


class Account(BaseModel):
    username: Annotated[str, Field(min_length=1, max_length=50)]
    age: int = Field(ge=0, le=120)
    tags: list[str] = Field(default_factory=list)   # fresh list per instance
    password: str
    password2: str

    @field_validator("username")
    @classmethod
    def normalize(cls, v: str) -> str:
        return v.strip().lower()

    @model_validator(mode="after")
    def passwords_match(self):
        if self.password != self.password2:
            raise ValueError("passwords do not match")
        return self

    @computed_field
    @property
    def adult(self) -> bool:
        return self.age >= 18


a = Account(username="  ADA ", age="30", password="x", password2="x")
a.username                 # 'ada'   (validator ran)
a.age                      # 30      (coerced str -> int)
a.model_dump()["adult"]    # True    (computed field included)

Nested models and a discriminated union

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


class Address(BaseModel):
    city: str
    zip: str = Field(pattern=r"^\d{5}$")


class Cat(BaseModel):
    kind: Literal["cat"]
    meows: bool


class Dog(BaseModel):
    kind: Literal["dog"]
    barks: bool


class Person(BaseModel):
    name: str
    addresses: list[Address]
    pet: Union[Cat, Dog] = Field(discriminator="kind")


p = Person.model_validate({
    "name": "ada",
    "addresses": [{"city": "NYC", "zip": "10001"}],
    "pet": {"kind": "dog", "barks": True},
})
type(p.addresses[0]).__name__   # 'Address'  (real nested instance)
type(p.pet).__name__            # 'Dog'      (routed by discriminator)

Typed application config (pydantic-settings)

The pattern to copy for any real app: one typed Settings class that fails fast at startup if the environment is wrong.

# uv add pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_", env_file=".env")

    debug: bool = False
    port: int = 8000
    database_url: str


# With APP_PORT=9000 and APP_DEBUG=true in the environment:
settings = Settings()       # reads env vars, then .env, then defaults
settings.port               # 9000  (coerced str -> int)
settings.debug              # True
# A bad value (APP_PORT=oops) raises ValidationError at startup, not in prod.

Behavior notes

  • Coercion is on by default, strict is opt-in. model_validate({"id": "1"}) happily coerces "1" to 1; pass strict=True (or use strict types) when an exact type match matters.
  • @field_validator must wrap a @classmethod. The decorator order matters: @field_validator on top, @classmethod beneath, then the function that takes (cls, v).
  • Mutable defaults need default_factory. Writing tags: list[str] = [] would share one list across instances; Field(default_factory=list) mints a fresh one each time.
  • model_dump() keeps Python types; model_dump_json() stringifies them. A datetime stays a datetime in the dict but becomes an ISO string in JSON; use mode="json" for JSON-safe values in a dict.
  • v1 spellings still work but warn. .dict(), .json(), parse_obj, @validator, and friends emit deprecation warnings in v2; prefer the model_* names and the new decorators.
  • BaseSettings moved out of core. In v2 it lives in pydantic-settings (uv add pydantic-settings); importing it from pydantic is a v1 habit.

References

Pydantic documentation (v2)

Project and related