Pydantic Data Validation
Pydantic - Data validation using Python type hints.
Installation
1pip install pydantic
2
3# With email validation
4pip install 'pydantic[email]'
5
6# With dotenv support
7pip install 'pydantic[dotenv]'
Basic Usage
1from pydantic import BaseModel
2
3class User(BaseModel):
4 id: int
5 name: str
6 email: str
7 age: int
8
9# Valid data
10user = User(id=1, name="Alice", email="alice@example.com", age=30)
11print(user)
12# id=1 name='Alice' email='alice@example.com' age=30
13
14# Type coercion
15user2 = User(id="2", name="Bob", email="bob@example.com", age="25")
16print(user2.age, type(user2.age)) # 25 <class 'int'>
17
18# Validation error
19try:
20 User(id="invalid", name="Charlie", email="charlie@example.com", age=30)
21except Exception as e:
22 print(e)
Validators
1from pydantic import BaseModel, field_validator, model_validator
2
3class User(BaseModel):
4 name: str
5 age: int
6 email: str
7
8 @field_validator('name')
9 @classmethod
10 def name_must_not_be_empty(cls, v):
11 if not v or not v.strip():
12 raise ValueError('Name cannot be empty')
13 return v.strip()
14
15 @field_validator('age')
16 @classmethod
17 def age_must_be_positive(cls, v):
18 if v < 0:
19 raise ValueError('Age must be positive')
20 if v > 150:
21 raise ValueError('Age must be realistic')
22 return v
23
24 @field_validator('email')
25 @classmethod
26 def email_must_be_valid(cls, v):
27 if '@' not in v:
28 raise ValueError('Invalid email')
29 return v.lower()
30
31 @model_validator(mode='after')
32 def check_adult_email(self):
33 if self.age < 18 and 'work' in self.email:
34 raise ValueError('Minors cannot have work email')
35 return self
36
37# Usage
38user = User(name=" Alice ", age=30, email="ALICE@EXAMPLE.COM")
39print(user.name) # "Alice" (trimmed)
40print(user.email) # "alice@example.com" (lowercased)
Field Configuration
1from pydantic import BaseModel, Field
2
3class Product(BaseModel):
4 name: str = Field(..., min_length=1, max_length=100)
5 price: float = Field(..., gt=0, le=1000000)
6 quantity: int = Field(default=0, ge=0)
7 description: str = Field(default="", max_length=500)
8 tags: list[str] = Field(default_factory=list)
9
10 # Alias for JSON keys
11 product_id: int = Field(..., alias="id")
12
13 # Exclude from export
14 internal_code: str = Field(default="", exclude=True)
15
16# Usage
17product = Product(
18 id=1,
19 name="Widget",
20 price=19.99,
21 quantity=100
22)
23
24print(product.model_dump())
25# {'name': 'Widget', 'price': 19.99, 'quantity': 100, 'description': '', 'tags': [], 'product_id': 1}
Nested Models
1from pydantic import BaseModel
2from typing import List
3
4class Address(BaseModel):
5 street: str
6 city: str
7 country: str
8 zip_code: str
9
10class User(BaseModel):
11 name: str
12 email: str
13 address: Address
14
15class Company(BaseModel):
16 name: str
17 employees: List[User]
18
19# Usage
20company = Company(
21 name="TechCorp",
22 employees=[
23 {
24 "name": "Alice",
25 "email": "alice@techcorp.com",
26 "address": {
27 "street": "123 Main St",
28 "city": "Springfield",
29 "country": "USA",
30 "zip_code": "12345"
31 }
32 }
33 ]
34)
35
36print(company.employees[0].address.city) # Springfield
JSON Serialization
1from pydantic import BaseModel
2import json
3
4class User(BaseModel):
5 id: int
6 name: str
7 email: str
8
9user = User(id=1, name="Alice", email="alice@example.com")
10
11# To JSON string
12json_str = user.model_dump_json()
13print(json_str)
14# {"id":1,"name":"Alice","email":"alice@example.com"}
15
16# To dict
17user_dict = user.model_dump()
18print(user_dict)
19# {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
20
21# From JSON
22user2 = User.model_validate_json(json_str)
23print(user2)
24
25# From dict
26user3 = User.model_validate(user_dict)
27print(user3)
Optional and Union Types
1from pydantic import BaseModel
2from typing import Optional, Union
3from datetime import datetime
4
5class Event(BaseModel):
6 name: str
7 date: datetime
8 location: Optional[str] = None
9 attendees: Union[int, str] = 0 # Can be int or str
10
11event1 = Event(name="Conference", date="2024-12-12T10:00:00")
12print(event1.location) # None
13
14event2 = Event(
15 name="Meetup",
16 date="2024-12-15T18:00:00",
17 location="Downtown",
18 attendees="50+"
19)
20print(event2.attendees) # "50+"
Custom Types
1from pydantic import BaseModel, field_validator
2from typing import Annotated
3
4# Custom validator as type
5def validate_positive(v: int) -> int:
6 if v <= 0:
7 raise ValueError('Must be positive')
8 return v
9
10PositiveInt = Annotated[int, field_validator(validate_positive)]
11
12class Product(BaseModel):
13 name: str
14 price: float
15 quantity: int
16
17 @field_validator('price', 'quantity')
18 @classmethod
19 def must_be_positive(cls, v):
20 if v <= 0:
21 raise ValueError('Must be positive')
22 return v
23
24product = Product(name="Widget", price=9.99, quantity=10)
Settings Management
1from pydantic_settings import BaseSettings
2
3class Settings(BaseSettings):
4 app_name: str = "MyApp"
5 debug: bool = False
6 database_url: str
7 api_key: str
8 max_connections: int = 10
9
10 class Config:
11 env_file = ".env"
12 env_file_encoding = "utf-8"
13
14# .env file:
15# DATABASE_URL=postgresql://localhost/mydb
16# API_KEY=secret123
17# DEBUG=true
18
19settings = Settings()
20print(settings.database_url)
21print(settings.debug) # True (from .env)
API Response Models
1from pydantic import BaseModel
2from typing import List, Optional
3from datetime import datetime
4
5class UserBase(BaseModel):
6 email: str
7 name: str
8
9class UserCreate(UserBase):
10 password: str
11
12class UserResponse(UserBase):
13 id: int
14 created_at: datetime
15 is_active: bool = True
16
17 class Config:
18 from_attributes = True # For ORM models
19
20class PaginatedResponse(BaseModel):
21 items: List[UserResponse]
22 total: int
23 page: int
24 page_size: int
25
26 @property
27 def total_pages(self) -> int:
28 return (self.total + self.page_size - 1) // self.page_size
29
30# Usage in FastAPI
31# @app.post("/users/", response_model=UserResponse)
32# def create_user(user: UserCreate):
33# ...
Computed Fields
1from pydantic import BaseModel, computed_field
2
3class Rectangle(BaseModel):
4 width: float
5 height: float
6
7 @computed_field
8 @property
9 def area(self) -> float:
10 return self.width * self.height
11
12 @computed_field
13 @property
14 def perimeter(self) -> float:
15 return 2 * (self.width + self.height)
16
17rect = Rectangle(width=10, height=5)
18print(rect.area) # 50.0
19print(rect.perimeter) # 30.0
20
21# Included in serialization
22print(rect.model_dump())
23# {'width': 10.0, 'height': 5.0, 'area': 50.0, 'perimeter': 30.0}
Generic Models
1from pydantic import BaseModel
2from typing import Generic, TypeVar, List
3
4T = TypeVar('T')
5
6class Response(BaseModel, Generic[T]):
7 data: T
8 message: str
9 success: bool = True
10
11class User(BaseModel):
12 id: int
13 name: str
14
15# Usage
16user_response = Response[User](
17 data=User(id=1, name="Alice"),
18 message="User retrieved successfully"
19)
20
21users_response = Response[List[User]](
22 data=[
23 User(id=1, name="Alice"),
24 User(id=2, name="Bob")
25 ],
26 message="Users retrieved successfully"
27)
Discriminated Unions
1from pydantic import BaseModel, Field
2from typing import Union, Literal
3
4class Cat(BaseModel):
5 pet_type: Literal["cat"]
6 meow: str
7
8class Dog(BaseModel):
9 pet_type: Literal["dog"]
10 bark: str
11
12class Pet(BaseModel):
13 animal: Union[Cat, Dog] = Field(..., discriminator="pet_type")
14
15# Correct discrimination
16cat_data = {"animal": {"pet_type": "cat", "meow": "meow"}}
17pet = Pet(**cat_data)
18print(pet.animal.meow) # "meow"
19
20dog_data = {"animal": {"pet_type": "dog", "bark": "woof"}}
21pet2 = Pet(**dog_data)
22print(pet2.animal.bark) # "woof"
FastAPI Integration
1from fastapi import FastAPI, HTTPException
2from pydantic import BaseModel, Field
3from typing import List
4
5app = FastAPI()
6
7class Item(BaseModel):
8 name: str = Field(..., min_length=1, max_length=100)
9 description: str = Field(default="", max_length=500)
10 price: float = Field(..., gt=0)
11 tax: float = Field(default=0, ge=0)
12
13class ItemResponse(Item):
14 id: int
15
16items_db: List[ItemResponse] = []
17
18@app.post("/items/", response_model=ItemResponse)
19def create_item(item: Item):
20 item_response = ItemResponse(id=len(items_db) + 1, **item.model_dump())
21 items_db.append(item_response)
22 return item_response
23
24@app.get("/items/", response_model=List[ItemResponse])
25def list_items():
26 return items_db
27
28@app.get("/items/{item_id}", response_model=ItemResponse)
29def get_item(item_id: int):
30 for item in items_db:
31 if item.id == item_id:
32 return item
33 raise HTTPException(status_code=404, detail="Item not found")
Validation Context
1from pydantic import BaseModel, field_validator, ValidationInfo
2
3class User(BaseModel):
4 name: str
5 role: str
6
7 @field_validator('name')
8 @classmethod
9 def validate_name(cls, v, info: ValidationInfo):
10 if info.context:
11 min_length = info.context.get('min_name_length', 1)
12 if len(v) < min_length:
13 raise ValueError(f'Name must be at least {min_length} chars')
14 return v
15
16# With context
17user = User.model_validate(
18 {'name': 'Al', 'role': 'admin'},
19 context={'min_name_length': 3}
20) # ValidationError!
Performance Tips
1from pydantic import BaseModel, ConfigDict
2
3class User(BaseModel):
4 model_config = ConfigDict(
5 # Validate on assignment
6 validate_assignment=True,
7
8 # Use slots for memory efficiency
9 use_enum_values=True,
10
11 # Strict types (no coercion)
12 strict=False,
13
14 # Allow arbitrary types
15 arbitrary_types_allowed=False,
16 )
17
18 name: str
19 age: int
20
21# Validate assignment
22user = User(name="Alice", age=30)
23user.age = 31 # Validated!
Related Snippets
- Click CLI Framework
Building CLI applications with Click in Python - FastAPI with OpenAPI
FastAPI with automatic OpenAPI documentation using Pydantic models and … - Flask Essentials
Flask web framework essentials for building web applications and APIs. … - Function Timing Decorator
Decorator to measure function execution time - LangChain Chatbot with Tools
Simple stdin chatbot using LangChain with tool calling (OpenRouter). … - Pandas DataFrames Essential Patterns
Essential patterns for working with Pandas DataFrames: creation, manipulation, … - Python Dataclasses
Python dataclasses for clean, boilerplate-free data structures. Basic Usage … - Python Metaclasses
Python metaclasses with visual explanations using Mermaid diagrams. What are … - Python Virtual Environments
Managing Python virtual environments and dependencies - Random Forests in Depth
Comprehensive guide to Random Forests: theory, implementation, tuning, and … - Scikit-learn Common Patterns
Common patterns and workflows for scikit-learn: preprocessing, model training, …