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