This is Chapter 1 of the Pydantic v2 textbook. Pairs with the FastAPI textbook (Pydantic is FastAPI’s validator) and the SQLAlchemy textbooks .

Pydantic in one paragraph

Pydantic is a Python library that turns type hints into validation. You declare a model:

from pydantic import BaseModel

class User(BaseModel):
    id: int
    email: str
    name: str | None = None

You parse input:

user = User.model_validate({"id": 1, "email": "[email protected]"})

Or serialize:

user.model_dump()
user.model_dump_json()

Two operations: validation and serialization. v2 made both fast (Rust core).

What changed from v1

If you’ve used v1:

  • BaseModel API renamed: parse_objmodel_validate, dict()model_dump(), .json()model_dump_json().
  • Config class → model_config dict.
  • @validator@field_validator; @root_validator@model_validator.
  • Strict mode optional / per field.
  • Discriminated unions native; faster.
  • Generic models cleaner.
  • Settings moved to pydantic-settings package.
  • 5–50× faster validation (Rust core).

For 2026: write v2. v1 is in maintenance.

Architecture

Pydantic v2 is a Python frontend over a Rust core (pydantic-core). When you define a model, Pydantic compiles a schema; the Rust core uses that schema to validate at runtime.

This means:

  • Validation is fast.
  • Some flexibility from v1 is gone (e.g., post-init mutation patterns).
  • Errors are detailed and structured.

BaseModel

from pydantic import BaseModel, Field, EmailStr
from datetime import datetime

class User(BaseModel):
    id: int
    email: EmailStr
    name: str = Field(..., min_length=1, max_length=120)
    age: int | None = Field(default=None, ge=0, le=150)
    created_at: datetime = Field(default_factory=datetime.utcnow)
  • EmailStr validates email format (requires email-validator).
  • Field adds metadata (constraints, descriptions, defaults).
  • default_factory for mutable / dynamic defaults.

Validation

# From dict
user = User.model_validate({"id": 1, "email": "[email protected]", "name": "Alice"})

# From JSON
user = User.model_validate_json('{"id": 1, "email": "[email protected]", "name": "Alice"}')

# Direct
user = User(id=1, email="[email protected]", name="Alice")

User(...) calls model_validate(...) under the hood.

Validation errors

try:
    User.model_validate({"id": "abc"})
except ValidationError as e:
    print(e.errors())
[
    {
        "type": "int_parsing",
        "loc": ("id",),
        "msg": "Input should be a valid integer, unable to parse string as an integer",
        "input": "abc",
    },
    {
        "type": "missing",
        "loc": ("email",),
        "msg": "Field required",
        "input": {"id": "abc"},
    },
]

Structured errors. Each has type, loc (path), msg, input.

Serialization

user.model_dump()
# {"id": 1, "email": "[email protected]", "name": "Alice", "age": None, "created_at": ...}

user.model_dump(exclude={"age"})
user.model_dump(exclude_none=True)
user.model_dump(mode="json")  # JSON-friendly types

user.model_dump_json()
# '{"id":1,"email":"[email protected]","name":"Alice","age":null,"created_at":"..."}'

mode="json" converts datetimes to strings, etc. (vs mode="python" which keeps Python types).

Type coercion

By default, Pydantic v2 is lenient:

User.model_validate({"id": "1"})  # "1" → 1 (int)
User.model_validate({"id": 1.0})  # 1.0 → 1
User.model_validate({"age": "25"})  # "25" → 25

For strict:

class User(BaseModel):
    model_config = {"strict": True}
    
    id: int

Now "1" → ValidationError.

Or per-field via Strict types.

Why this matters

Pydantic sits between the outside world (JSON, dicts, env vars, query params) and your Python code. Validate at the boundary; once an instance exists, you can trust it.

This is the same role for FastAPI request validation, settings (config), structured output from LLMs, internal RPC, Kafka events, and more.

The textbook structure

ChTopic
1This chapter — introduction
2Fields, types, and constraints
3Validators (field, model, before, after)
4Serialization in depth
5Nested models, generics, discriminated unions
6Custom types and TypeAdapter
7Strict mode and coercion
8JSON Schema generation
9Settings (pydantic-settings)
10Performance, FastAPI integration, alternatives

Read this next


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .