Data Classes and Pydantic for Robust Applications
Python’s dataclasses (introduced in Python 3.7) and Pydantic (a third-party library) both reduce boilerplate code when defining data containers, but they serve different purposes. Dataclasses auto-generate __init__, __repr__, __eq__, and __hash__ methods based on class annotations. Pydantic goes further by adding runtime type validation, serialization, and parsing — making it the backbone of FastAPI and the go-to choice for any application that handles external data.
Python Dataclasses
Dataclasses are ideal for internal data structures where you want concise, readable code without writing boilerplate. The @dataclass decorator inspects the class’s type annotations and automatically generates the standard dunder methods. You can control behavior with parameters like frozen=True (immutable instances), order=True (sortable), and slots=True (Python 3.10+, memory efficient).
from dataclasses import dataclass, field
@dataclass(frozen=True, order=True)
class Point:
x: float
y: float
label: str = field(default="", compare=False)
p1 = Point(1.0, 2.0, "start")
p2 = Point(1.0, 2.0, "end")
print(p1) # Point(x=1.0, y=2.0, label='start')
print(p1 == p2) # True (label excluded from comparison)
# p1.x = 3.0 # Error: frozen instance
Pydantic BaseModel
Pydantic’s BaseModel adds validation and serialization on top of the type annotation system. When you create an instance, Pydantic validates every field against its type annotation — coercing values when safe (e.g., "123" becomes 123 for an int field) and raising a detailed validation error when coercion is not possible. This catches data quality issues at the boundary of your application rather than deep in your logic.
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
class User(BaseModel):
id: int
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=0, le=150, default=0)
created_at: datetime = Field(default_factory=datetime.now)
tags: list[str] = Field(default_factory=list)
# Valid data — works
user = User(id=1, name="Alice", email="alice@example.com", age=30, tags=["admin"])
print(user.model_dump_json(indent=2))
# Invalid data — raises ValidationError with clear message
try:
User(id="not-a-number", name="", email="not-an-email", age=200)
except Exception as e:
print(e)
# Shows all validation errors at once, not just the first one
The Field() function lets you add constraints (min_length, ge for greater-or-equal, le for less-or-equal), default values, and metadata. Pydantic V2, powered by a Rust core (pydantic-core), is significantly faster than V1 and supports complex types like UUID, IPv4Address, Decimal, and Path out of the box.
Nested Models and Validation
Pydantic models can be nested to represent complex hierarchies. Each nested model is validated recursively. You can also define custom validators using @field_validator and @model_validator decorators to enforce business rules that go beyond simple type checks.
from pydantic import BaseModel, field_validator
class Address(BaseModel):
street: str
city: str
zip_code: str
class Employee(BaseModel):
name: str
address: Address
salary: float
@field_validator('salary')
@classmethod
def salary_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Salary must be positive')
return v
emp = Employee(
name="Bob",
address={"street": "123 Main St", "city": "NYC", "zip_code": "10001"},
salary=75000.0
)
print(emp.model_dump())
# {'name': 'Bob', 'address': {'street': '123 Main St', ...}, 'salary': 75000.0}
Serialization and Parsing
Pydantic makes it easy to convert between Python objects and JSON/dict representations. This is essential for API development: parse incoming request bodies with model_validate() and serialize responses with model_dump() or model_dump_json(). The model_config lets you control serialization behavior like excluding unset values, using aliases, or populating by name vs by position.
# Parse from JSON string json_data = '{"name": "Charlie", "email": "charlie@example.com", "age": 25}' user = User.model_validate_json(json_data) # Parse from dict user = User.model_validate(dict(name="Diana", email="diana@example.com", age=28)) # Serialize to dict (exclude unset fields) data = user.model_dump(exclude_unset=True) # Serialize to JSON with formatting print(user.model_dump_json(indent=2))Settings Management with Pydantic
Pydantic's
BaseSettings(frompydantic-settings) extendsBaseModelto read configuration from environment variables,.envfiles, and secret stores. This is the recommended way to manage application configuration in Python. Define a settings class with typed fields and default values, and Pydantic automatically reads from the environment — validating types, providing clear error messages for missing required values, and supporting nested settings for complex configurations.from pydantic_settings import BaseSettings class Settings(BaseSettings): database_url: str redis_url: str = 'redis://localhost:6379' debug: bool = False api_key: str max_connections: int = 10 class Config: env_file = '.env' env_file_encoding = 'utf-8' settings = Settings() # Reads DATABASE_URL, REDIS_URL, API_KEY from environment or .env fileThis pattern eliminates the need for ad-hoc environment variable parsing, type coercion bugs, and inconsistent configuration handling across your application. When combined with FastAPI, Pydantic provides end-to-end type safety from the HTTP request boundary through your application logic to the response serialization.
Use dataclasses for simple internal data holding in your own codebase where validation is not critical. Use Pydantic whenever data enters or leaves your application — API requests/responses, configuration files, database records — because the validation layer prevents corrupted data from propagating through your system.
Advanced Pydantic Features
Pydantic V2, rewritten in Rust with pydantic-core, is significantly faster than V1. Advanced features include: discriminated unions for parsing polymorphic JSON, strict mode (no implicit string-to-int coercion), computed fields, model validators with before/after/wrap modes, and serialization aliases. Pydantic's integration with FastAPI is seamless: request bodies and response models automatically validate and serialize. BaseModel.model_dump() and model_validate() replace V1 methods. Pydantic supports JSON Schema generation for OpenAPI documentation. For enterprise applications, StrictBool, PaymentCardNumber, and EmailStr provide domain-specific validation out of the box.
