skip to content

pydantic β€” Data Validation

Validate and parse data at runtime using Python type hints with Pydantic v2. Covers BaseModel, field validators, nested models, and JSON serialization.

3 min read 8 snippets yesterday intermediate

pydantic β€” Data Validation#

What it is#

Pydantic validates data against Python type annotations at runtime. You define a BaseModel with type-annotated fields; Pydantic enforces types, coerces compatible values, and raises detailed errors for invalid input. It is the foundation of FastAPI’s request/response handling and is widely used for configuration, API clients, and data parsing.

[!NOTE] This page covers Pydantic v2 (released 2023, current as of 2026). The API changed significantly from v1. Check your installed version: python -c "import pydantic; print(pydantic.__version__)".

Install#

pip install pydantic
# Optional: EmailStr and other validators
pip install "pydantic[email]"

Quick example#

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    name: str
    age: int
    active: bool = True

u = User(name="Alice", age=30)
print(u.model_dump())

try:
    User(name="Bob", age="not-a-number")
except ValidationError as e:
    print(f"{e.error_count()} validation error(s)")
    print(e.errors()[0]["msg"])

Output:

{'name': 'Alice', 'age': 30, 'active': True}
1 validation error(s)
Input should be a valid integer, unable to parse string as an integer

When / why to use it#

  • Parsing and validating API request bodies or responses.
  • Typed configuration objects loaded from env vars or YAML.
  • Any time you want Python type hints to be enforced at runtime, not just as documentation.

Common pitfalls#

[!WARNING] v1 β†’ v2 migration β€” Pydantic v2 renamed many methods: dict() β†’ model_dump(), json() β†’ model_dump_json(), parse_obj() β†’ model_validate(). Using v1 syntax on v2 raises AttributeError.

[!WARNING] Mutable defaults β€” use Field(default_factory=list) for mutable defaults like lists and dicts, not bare = []. Bare mutable defaults are shared across all instances (same as Python dataclass gotcha).

[!TIP] Use model_config = ConfigDict(strict=True) to disable Pydantic’s coercion. By default "30" is silently coerced to 30 for an int field, which can hide bugs.

Richer example β€” nested models and validators#

from pydantic import BaseModel, field_validator, Field
from typing import Optional

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class User(BaseModel):
    name: str
    age: int = Field(ge=0, le=130, description="Must be 0–130")
    email: str
    tags: list[str] = Field(default_factory=list)
    address: Optional[Address] = None

    @field_validator("email")
    @classmethod
    def email_must_contain_at(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("not a valid email address")
        return v.lower()

u = User(
    name="Alice",
    age=30,
    email="Alice@Example.COM",
    tags=["admin", "user"],
    address={"street": "123 Main St", "city": "Anytown", "zip_code": "12345"},
)
print(u.model_dump_json(indent=2))

Output:

{
  "name": "Alice",
  "age": 30,
  "email": "alice@example.com",
  "tags": [
    "admin",
    "user"
  ],
  "address": {
    "street": "123 Main St",
    "city": "Anytown",
    "zip_code": "12345"
  }
}

JSON and dict conversion#

# From dict
user = User.model_validate({"name": "Bob", "age": 25, "email": "b@b.com"})

# From JSON string
user = User.model_validate_json('{"name":"Bob","age":25,"email":"b@b.com"}')

# To dict
d = user.model_dump()
d = user.model_dump(exclude={"email"})        # exclude fields
d = user.model_dump(include={"name", "age"})  # include only

# To JSON string
j = user.model_dump_json()
j = user.model_dump_json(indent=2)

Settings from environment variables#

from pydantic_settings import BaseSettings  # pip install pydantic-settings

class Settings(BaseSettings):
    database_url: str
    api_key: str
    debug: bool = False

    class Config:
        env_file = ".env"

settings = Settings()  # reads DATABASE_URL, API_KEY, DEBUG from env / .env
print(settings.debug)

Output:

False