Data Validation with Pydantic in Python

· 4 min read · Updated March 14, 2026 · intermediate
pydantic validation data python schemas

Pydantic is a library that lets you define data models with type annotations and automatically validates data against those models. If you’ve ever written validation code by hand, you know it quickly becomes messy. Pydantic gives you a cleaner way.

Why Pydantic?

Traditional validation often looks like a pile of if statements:

def create_user(data):
    if "email" not in data:
        raise ValueError("email is required")
    if not isinstance(data["email"], str):
        raise ValueError("email must be a string")
    if "@" not in data["email"]:
        raise ValueError("invalid email format")

Pydantic replaces all that with declarative models:

from pydantic import BaseModel

class User(BaseModel):
    email: str
    age: int
    
user = User(email="alice@example.com", age=30)

Much cleaner. The validation happens automatically when you create an instance.

Defining Models

Create a class inheriting from BaseModel. Each class attribute becomes a field with its type as the validation rule:

from pydantic import BaseModel

class Product(BaseModel):
    name: str
    price: float
    in_stock: bool

product = Product(name="Widget", price=19.99, in_stock=True)

Pydantic validates on the way in. If the data does not match your types, you get a clear error.

Field Types and Validation

Pydantic supports many types out of the box, plus special validators for common patterns:

from pydantic import BaseModel, Field, validator
from typing import Optional

class User(BaseModel):
    username: str = Field(..., min_length=3, max_length=20)
    email: str
    age: int = Field(..., ge=0, le=120)
    bio: Optional[str] = None
    
    @validator("username")
    def username_alphanumeric(cls, v):
        assert v.isalnum(), "must be alphanumeric"
        return v

    @validator("email")
    def email_lowercase(cls, v):
        return v.lower()

The Field function adds constraints. The @validator decorator lets you write custom validation logic.

Nested Models

Models can contain other models:

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class Person(BaseModel):
    name: str
    address: Address

person = Person(
    name="Bob",
    address=Address(
        street="123 Main St",
        city="Boston",
        zip_code="02101"
    )
)

You can also use lists of models:

class Order(BaseModel):
    items: list[Product]
    shipping_address: Address

Validating External Data

Pydantic excels at validating data from APIs, databases, or configuration files:

from pydantic import BaseModel

class Config(BaseModel):
    debug: bool = False
    max_connections: int = 10
    allowed_hosts: list[str]
    
config_dict = {
    "debug": True,
    "allowed_hosts": ["localhost", "api.example.com"]
}
config = Config(**config_dict)

The ** unpacks the dictionary into keyword arguments.

Working with Validation Errors

When validation fails, Pydantic raises a ValidationError with useful information:

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    name: str
    age: int

try:
    user = User(name="Alice", age=-5)
except ValidationError as e:
    print(e)

You can also access errors programmatically:

try:
    User(name="", age=25)
except ValidationError as e:
    for error in e.errors():
        print(f"{error['loc']}: {error['msg']}")

Common Field Types

Pydantic provides many built-in types beyond the basics:

from pydantic import BaseModel, HttpUrl, EmailStr, SecretStr
from typing import List
from datetime import date

class UserProfile(BaseModel):
    email: EmailStr          # Validates email format
    website: HttpUrl         # Validates URL format
    password: SecretStr      # Masks the value when displayed
    birth_date: date        # ISO 8601 date format
    interests: List[str]    # List of strings

These specialized types handle common validation scenarios without writing custom validators.

Pydantic in FastAPI

Pydantic shines in web APIs, especially with FastAPI:

from pydantic import BaseModel, EmailStr
from fastapi import FastAPI

app = FastAPI()

class UserCreate(BaseModel):
    email: EmailStr
    username: str

@app.post("/users/")
def create_user(user: UserCreate):
    return {"email": user.email, "username": user.username}

FastAPI uses Pydantic models to automatically generate OpenAPI schemas, validate request bodies, and document your API. This is why Pydantic has become the standard for Python web development.

Performance Tips

Pydantic v2 is significantly faster than v1, but here are some tips to get the best performance:

class FastModel(BaseModel):
    model_config = {"str_strip_whitespace": True}
    
    name: str
    value: int

For very simple models, use BaseModel without validators. Validation overhead is minimal for straightforward types.

Summary

Pydantic gives you a clean, declarative way to validate data in Python. Define your models with type annotations, and Pydantic handles the rest. It handles nested models, custom validators, specialized types like email and URL, and integrates seamlessly with FastAPI and other frameworks. The key benefits are clearer code, fewer bugs from validation mistakes, and self-documenting data schemas.

See Also