Core Concepts of Pydantic

At the heart of Pydantic is the BaseModel class. By subclassing BaseModel, you can define a data structure with type hints, and Pydantic will automatically validate any input data against those types.

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool = True  # default value

In the example above, any attempt to instantiate the User class with invalid data will raise a ValidationError.

user = User(id="not-an-integer", name="Alice", email="[email protected]")
# Raises: ValueError: invalid literal for int() with base 10: 'not-an-integer'

Data Parsing and Conversion

Pydantic also supports data parsing and conversion. It can convert input data into the expected types, making it easier to work with unstructured data like JSON or dictionaries.

data = {
    "id": "123",
    "name": "Bob",
    "email": "[email protected]",
    "is_active": "false"
}

user = User(**data)
print(user.id)        # 123 (converted from string)
print(user.is_active) # False (converted from string)

This feature is particularly useful when dealing with APIs, where data types may not be consistent.

Custom Validation with @validator

Pydantic allows custom validation logic using @validator decorators. This is useful for enforcing business rules or complex constraints.

from pydantic import BaseModel, validator

class Product(BaseModel):
    name: str
    price: float
    tax_rate: float = 0.1  # default tax rate

    @validator("price")
    def price_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError("Price must be positive.")
        return v

In this example, the price field is validated to ensure it is a positive number.

Field Configuration

Pydantic provides a way to configure fields using Field and Config classes. These are used to set default values, aliases, and field metadata.

from pydantic import BaseModel, Field

class OrderItem(BaseModel):
    product_id: int
    quantity: int = Field(default=1, ge=1, le=10)
    unit_price: float = Field(..., description="Price per unit", example=10.99)

    class Config:
        frozen = True  # prevents modification after creation

This configuration ensures that quantity is an integer between 1 and 10, and that the OrderItem object is immutable after creation.

Settings Management with BaseSettings

Pydantic supports environment-based configuration via the BaseSettings class, which is ideal for managing application settings.

from pydantic import BaseSettings, Field

class AppConfig(BaseSettings):
    app_name: str
    debug: bool = False
    api_key: str = Field(..., env="API_KEY")

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

In this example, AppConfig reads environment variables from a .env file. If API_KEY is not provided, Pydantic raises a ValueError.

Data Models and API Development

Pydantic is especially valuable in API development. It can be used to define request and response models, ensuring that data is validated before and after processing.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class ItemCreateRequest(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.post("/items/")
def create_item(item: ItemCreateRequest):
    return item.dict()

In this FastAPI example, ItemCreateRequest is used to validate the input of the /items/ endpoint.

Advanced Features

Pydantic supports advanced features such as root models, recursive models, and arbitrary types.

Root Model

A RootModel is used when the data structure is not a dictionary or list.

from pydantic import RootModel

class UserList(RootModel):
    root: list[str]

users = UserList(root=["Alice", "Bob"])
print(users.root)  # ['Alice', 'Bob']

Recursive Models

Pydantic allows models to reference themselves, which is useful for hierarchical data.

class Category(BaseModel):
    id: int
    name: str
    subcategories: list["Category"] = []

category1 = Category(id=1, name="Books", subcategories=[])
category2 = Category(id=2, name="Fiction", subcategories=[category1])

Arbitrary Type Support

Pydantic supports arbitrary types using the arbitrary_types_allowed option in the Config class.

from datetime import datetime
from pydantic import BaseModel

class Event(BaseModel):
    name: str
    start_time: datetime

    class Config:
        arbitrary_types_allowed = True

This allows Pydantic to handle complex types like datetime or custom classes.

Performance Considerations

Pydantic is optimized for performance and is generally faster than other validation libraries. However, for large-scale applications, consider using caching and batch validation where possible.

Learn more with useful resources