Pydantic — The Complete Notebook
Comprehensive guide on Pydantic — The Complete Notebook.
Pydantic
1. Overview#
Pydantic is a data validation library that uses Python type hints to define the shape of data — and then actually enforces that shape at runtime, unlike plain type hints (see type-hints.md). It's the validation engine behind FastAPI's request/response handling, but it's equally useful on its own for config loading, parsing external API responses, or validating any structured data.
This note covers Pydantic v2 (the current major version). The API changed meaningfully from v1 — if you see
@validatororclass Config:in older code/tutorials, that's v1 syntax; v2 uses@field_validatorandmodel_config.
2. Defining and Using Models#
2.1 Basic Model#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True # default value — optional field
user = User(id=1, name="Kamal", email="kamal@example.com")
print(user) # id=1 name='Kamal' email='kamal@example.com' is_active=True
print(user.model_dump()) # {'id': 1, 'name': 'Kamal', ...} — convert to a plain dict
print(user.model_dump_json())# '{"id":1,"name":"Kamal",...}' — convert to a JSON string
2.2 Automatic Type Coercion#
Pydantic tries to convert compatible input into the declared type instead of rejecting it outright.
🐍 PythonInteractive WebAssemblyclass Product(BaseModel):
name: str
price: float
quantity: int
p = Product(name="Keyboard", price="1499.50", quantity="3")
print(p.price, type(p.price)) # 1499.5 <class 'float'>
print(p.quantity, type(p.quantity)) # 3 <class 'int'>
2.3 Validation Errors#
🐍 PythonInteractive WebAssemblyfrom pydantic import ValidationError
try:
User(id="not-a-number", name="Kamal", email="kamal@example.com")
except ValidationError as e:
print(e)
# Shows exactly which field failed and why, e.g.:
# id
# Input should be a valid integer, unable to parse string as an integer
3. Field Customization with Field#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=2, max_length=100)
price: float = Field(gt=0, description="Price must be positive")
quantity: int = Field(default=0, ge=0, le=10_000)
sku: str = Field(alias="SKU") # accept "SKU" from input, but expose as .sku
product = Product(name="Mouse", price=799.0, SKU="MSE-001")
print(product.sku) # MSE-001
| Constraint | Applies To | Example |
|---|---|---|
gt, ge, lt, le | Numbers | price: float = Field(gt=0) |
min_length, max_length | Strings, lists | name: str = Field(min_length=2) |
pattern | Strings (regex) | code: str = Field(pattern=r"^[A-Z]{3}\d{3}$") |
default | Any field | quantity: int = Field(default=0) |
alias | Any field | Accept a different input key name |
4. Nested Models#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
pincode: str
class User(BaseModel):
name: str
address: Address # a model can contain another model
user = User(
name="Asha",
address={"street": "MG Road", "city": "Bengaluru", "pincode": "560001"}
)
print(user.address.city) # Bengaluru
print(user.model_dump()) # nested dict output, fully validated at every level
4.1 Lists of Models#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class OrderItem(BaseModel):
product_name: str
quantity: int
class Order(BaseModel):
order_id: str
items: list[OrderItem]
order = Order(
order_id="ORD-001",
items=[
{"product_name": "Laptop", "quantity": 1},
{"product_name": "Mouse", "quantity": 2},
]
)
print(order.items[1].product_name) # Mouse
5. Optional Fields and None#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class Profile(BaseModel):
name: str
bio: str | None = None # optional — defaults to None if omitted
age: int | None = Field(default=None, ge=0)
p1 = Profile(name="Kamal")
print(p1.bio) # None
p2 = Profile(name="Asha", bio="Engineer", age=29)
A field is only truly optional in the request sense if it has a default value.
bio: str | Nonewithout= Nonestill requires the key to be present (it can just benull).
6. Custom Validators#
6.1 field_validator — Validate a Single Field#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, field_validator
class User(BaseModel):
username: str
email: str
@field_validator("username")
@classmethod
def username_must_be_alphanumeric(cls, value: str) -> str:
if not value.isalnum():
raise ValueError("Username must be alphanumeric")
return value.lower() # validators can also transform the value
@field_validator("email")
@classmethod
def email_must_contain_at(cls, value: str) -> str:
if "@" not in value:
raise ValueError("Invalid email address")
return value
user = User(username="Kamal123", email="kamal@example.com")
print(user.username) # kamal123 — lowercased by the validator
6.2 model_validator — Validate Across Multiple Fields#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, model_validator
class DateRange(BaseModel):
start_date: str
end_date: str
@model_validator(mode="after")
def check_dates_ordered(self) -> "DateRange":
if self.start_date > self.end_date:
raise ValueError("start_date must be before end_date")
return self
DateRange(start_date="2026-01-01", end_date="2026-01-10") # OK
# DateRange(start_date="2026-02-01", end_date="2026-01-10") # raises ValidationError
7. Computed Fields#
Values derived from other fields, included automatically in serialized output.
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
r = Rectangle(width=4, height=5)
print(r.area) # 20.0
print(r.model_dump()) # {'width': 4.0, 'height': 5.0, 'area': 20.0}
8. Enums for Constrained Choices#
🐍 PythonInteractive WebAssemblyfrom enum import Enum
from pydantic import BaseModel
class OrderStatus(str, Enum):
PENDING = "pending"
SHIPPED = "shipped"
DELIVERED = "delivered"
class Order(BaseModel):
order_id: str
status: OrderStatus
order = Order(order_id="ORD-001", status="shipped")
print(order.status) # OrderStatus.SHIPPED
print(order.status.value) # shipped
# Order(order_id="ORD-002", status="lost") # raises ValidationError — "lost" isn't a valid choice
9. Model Configuration with model_config#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(
str_strip_whitespace=True, # auto-trims leading/trailing whitespace from strings
frozen=True, # makes the model immutable after creation
extra="forbid", # reject any fields not defined on the model
)
name: str
email: str
user = User(name=" Kamal ", email="kamal@example.com")
print(repr(user.name)) # 'Kamal' — whitespace stripped automatically
# user.name = "New Name" # raises an error — model is frozen
# User(name="A", email="a@b.com", extra_field="x") # raises — extra="forbid"
extra Setting | Behavior on Unknown Fields |
|---|---|
"ignore" (default) | Silently drops unknown fields |
"forbid" | Raises a ValidationError |
"allow" | Keeps unknown fields on the model |
10. Serialization Control#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, Field
class User(BaseModel):
name: str
password: str = Field(exclude=True) # never included in serialized output
email: str
user = User(name="Kamal", password="secret123", email="kamal@example.com")
print(user.model_dump())
# {'name': 'Kamal', 'email': 'kamal@example.com'} — password excluded automatically
🐍 PythonInteractive WebAssembly# Selective serialization
user.model_dump(include={"name", "email"})
user.model_dump(exclude={"email"})
user.model_dump(by_alias=True) # use field aliases as keys instead of Python names
11. Parsing From Different Sources#
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel
class User(BaseModel):
name: str
age: int
# From a dict
user = User.model_validate({"name": "Kamal", "age": 30})
# From a JSON string
user = User.model_validate_json('{"name": "Kamal", "age": 30}')
# Validating a list of items at once
from pydantic import TypeAdapter
adapter = TypeAdapter(list[User])
users = adapter.validate_python([{"name": "Asha", "age": 29}, {"name": "Ravi", "age": 34}])
12. Settings Management with pydantic-settings#
A common real-world use: loading and validating environment variables/config at startup.
bashpip install pydantic-settings
🐍 PythonInteractive WebAssemblyfrom pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
database_url: str
api_key: str
debug: bool = False
max_connections: int = 10
settings = Settings() # reads from environment variables / .env, validated automatically
print(settings.debug)
Mathematical Formulation# .env DATABASE_URL=postgresql://user:pass@localhost/mydb API_KEY=your-secret-key DEBUG=true
13. Pydantic + FastAPI (Quick Recap)#
Pydantic models are what FastAPI uses to validate request bodies and shape response payloads — see fastapi.md for the full picture.
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class CreateUserRequest(BaseModel):
name: str
email: str
@app.post("/users")
def create_user(payload: CreateUserRequest):
# payload is already validated by the time this line runs
return {"created": payload.name}
14. Common Pitfalls#
INCORRECT: Using a Mutable Default Directly#
🐍 PythonInteractive WebAssemblyclass Cart(BaseModel):
items: list = [] # Pydantic actually handles this safely, unlike a plain Python function default
Unlike the mutable-default-argument trap in plain functions (see
basic.md), Pydantic models are safe with= []or= {}as defaults — Pydantic creates a fresh instance per object. Still,Field(default_factory=list)is the more explicit, conventional style.
🐍 PythonInteractive WebAssemblyfrom pydantic import BaseModel, Field
class Cart(BaseModel):
items: list[str] = Field(default_factory=list)
INCORRECT: Forgetting @classmethod on a field_validator#
🐍 PythonInteractive WebAssemblyclass User(BaseModel):
name: str
@field_validator("name")
def check_name(cls, value): # missing @classmethod — works but triggers linter warnings/type errors
return value
CORRECT: Fix#
🐍 PythonInteractive WebAssembly @field_validator("name")
@classmethod
def check_name(cls, value: str) -> str:
return value
INCORRECT: Assuming Type Hints Alone Validate Data#
🐍 PythonInteractive WebAssemblydef process(user: User): # a plain function — NOT a Pydantic model
...
process("not a user") # no error at all; type hints alone don't enforce anything
CORRECT: Fix — Validation Only Happens When You Actually Construct/Validate a Model#
🐍 PythonInteractive WebAssemblydata = User.model_validate(raw_input) # THIS is what performs validation
15. Summary & Best Practices Checklist#
- Use
Field(...)constraints (gt,min_length,pattern, etc.) instead of writing manualifchecks for simple rules. - Use
field_validatorfor single-field logic andmodel_validatorfor cross-field logic. - Use
Field(default_factory=list/dict)for mutable defaults, for clarity even though Pydantic handles= []safely. - Use
Field(exclude=True)to keep sensitive fields (passwords, tokens) out of serialized output. - Set
extra="forbid"on models where unexpected fields should be rejected outright (e.g., strict API contracts). - Use
pydantic-settingsfor typed, validated environment configuration instead of rawos.environaccess. - Remember: Pydantic validates at construction time (
Model(...),.model_validate(...)) — bare type hints elsewhere in your code still aren't enforced.
Pydantic V2 Data Validation Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.