🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Python Pydantic v2 Complete Guide 2026: Validation, Serialization and FastAPI

⏱️5 min read  ·  1,057 words

Pydantic v2 is the most important Python library upgrade in 2026. Rewritten in Rust (via pydantic-core), it is 5-50x faster than v1 with a cleaner API, strict mode, and model validators. This guide covers every Pydantic v2 feature with practical examples.

Installation and Migration

pip install pydantic>=2.0

# Auto-migrate from v1
pip install bump-pydantic
bump-pydantic .

# Key changes from v1:
# - class Config -> model_config = ConfigDict(...)
# - @validator -> @field_validator + @model_validator
# - .dict() -> .model_dump()
# - .json() -> .model_dump_json()
# - __fields__ -> model_fields

Basic Models

from pydantic import BaseModel, Field, ConfigDict, EmailStr
from datetime import datetime
from typing import Optional

class User(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,     # strip whitespace from strings
        str_min_length=1,              # no empty strings
        validate_assignment=True,      # validate on attribute assignment
        populate_by_name=True,         # allow field name OR alias
        use_enum_values=True,          # store enum.value not enum
    )

    id: int
    name: str = Field(min_length=2, max_length=50)
    email: EmailStr
    age: Optional[int] = Field(default=None, ge=13, le=120)
    role: str = Field(default="user", pattern=r"^(user|admin|moderator)$")
    created_at: datetime = Field(default_factory=datetime.utcnow)
    tags: list[str] = Field(default_factory=list, max_length=10)

# Create
user = User(id=1, name="Alice Chen", email="alice@example.com", age=30)
print(user.model_dump())
print(user.model_dump_json(indent=2))

# Partial update
updated = user.model_copy(update={"name": "Alice Smith"})

# From dict
user2 = User.model_validate({"id": 2, "name": "Bob", "email": "bob@example.com"})

# From JSON string
user3 = User.model_validate_json('{"id": 3, "name": "Carol", "email": "carol@example.com"}')

Validators

from pydantic import BaseModel, field_validator, model_validator, Field
import re

class PasswordChange(BaseModel):
    current_password: str
    new_password: str = Field(min_length=8)
    confirm_password: str

    @field_validator("new_password")
    @classmethod
    def password_strength(cls, v: str) -> str:
        if not re.search(r"[A-Z]", v):
            raise ValueError("Must contain uppercase letter")
        if not re.search(r"[0-9]", v):
            raise ValueError("Must contain digit")
        if not re.search(r"[!@#$%^&*]", v):
            raise ValueError("Must contain special character")
        return v

    @model_validator(mode="after")  # runs after all field validators
    def passwords_match(self) -> "PasswordChange":
        if self.new_password != self.confirm_password:
            raise ValueError("Passwords do not match")
        if self.new_password == self.current_password:
            raise ValueError("New password must differ from current")
        return self

# Validate and get all errors at once
from pydantic import ValidationError

try:
    PasswordChange(
        current_password="old123",
        new_password="short",
        confirm_password="different"
    )
except ValidationError as e:
    print(e.error_count(), "errors")
    for error in e.errors():
        print(f"  {error['loc']}: {error['msg']}")

Strict Mode

from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)  # no coercion!

    id: int
    name: str
    active: bool

# Without strict: "1" coerced to int, "true" to bool
# With strict: exact types required
try:
    StrictUser(id="1", name="Alice", active="true")  # raises!
except Exception as e:
    print(e)

# Strict per-field
from pydantic import Strict
from typing import Annotated

class PartiallyStrict(BaseModel):
    id: Annotated[int, Strict()]  # strict for this field only
    name: str  # coercion allowed

Aliases and Serialization

from pydantic import BaseModel, Field, AliasPath
from pydantic.functional_serializers import model_serializer

class APIResponse(BaseModel):
    # Accept camelCase from API, use snake_case internally
    model_config = ConfigDict(populate_by_name=True)

    user_id: int = Field(alias="userId")
    first_name: str = Field(alias="firstName")
    last_name: str = Field(alias="lastName")
    created_at: str = Field(alias="createdAt")

    # Nested alias path
    city: str = Field(validation_alias=AliasPath("address", "city"))

# Parse camelCase JSON
data = {
    "userId": 1,
    "firstName": "Alice",
    "lastName": "Chen",
    "createdAt": "2026-01-15",
    "address": {"city": "Sydney", "country": "AU"}
}
response = APIResponse.model_validate(data)
print(response.user_id, response.first_name, response.city)

# Serialize back to camelCase
print(response.model_dump(by_alias=True))  # {"userId": 1, ...}

Pydantic + FastAPI (2026 Pattern)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, ConfigDict
from typing import Annotated
from datetime import datetime

app = FastAPI()

class UserCreate(BaseModel):
    name: Annotated[str, Field(min_length=2, max_length=50)]
    email: Annotated[str, Field(pattern=r"^[^@]+@[^@]+\.[^@]+$")]
    password: Annotated[str, Field(min_length=8, exclude=True)]  # exclude from response

class UserResponse(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # allows ORM model input

    id: int
    name: str
    email: str
    created_at: datetime

    @classmethod
    def from_orm_with_extras(cls, obj, **extras) -> "UserResponse":
        return cls.model_validate({**obj.__dict__, **extras})

@app.post("/users", response_model=UserResponse, status_code=201)
async def create_user(data: UserCreate) -> UserResponse:
    # data.password available but won't appear in response
    user = await db.users.create({
        "name": data.name,
        "email": data.email,
        "password_hash": hash_password(data.password)
    })
    return UserResponse.model_validate(user)

# Automatic OpenAPI docs show the schema
# Pydantic v2 JSON Schema generation is much better than v1

Performance: v1 vs v2

import timeit
from pydantic import BaseModel

class BenchModel(BaseModel):
    id: int
    name: str
    email: str
    score: float

data = {"id": 1, "name": "Alice", "email": "alice@example.com", "score": 9.5}

n = 100_000
t = timeit.timeit(lambda: BenchModel(**data), number=n)
print(f"Pydantic v2: {t:.2f}s for {n:,} validations ({n/t:,.0f}/sec)")
# Typical: ~2-5x faster than v1, up to 50x for complex nested models

Pydantic v2 in 2026 is the validation standard for Python. The Rust core delivers significant performance gains. Field validators catch data issues early, model validators enforce cross-field constraints, and strict mode eliminates silent coercion bugs. Migrate from v1 using bump-pydantic — it handles 90% of changes automatically.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা