Data Validation with Pydantic in Python
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
- Working with JSON in Python - Reading and parsing JSON data
- Type Hints in Python - Adding type annotations to your code
- pandas DataFrames Explained - Working with structured data