Before dataclasses, Python developers wrote dozens of lines of boilerplate for simple data containers. __init__, __repr__, __eq__ — all by hand. Dataclasses generate them automatically. Pydantic goes further: validation, serialization, JSON schema, and FastAPI integration. This guide covers both with real production patterns.
⚡ TL;DR: Use dataclasses for internal data structures where you trust the data. Use Pydantic for external data (API requests, config files, user input) where validation is critical. Pydantic v2 is 5-50x faster than v1. Both integrate with FastAPI.
Python dataclasses — zero-boilerplate data containers
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
id: int
name: str
email: str
created_at: datetime = field(default_factory=datetime.now)
tags: list[str] = field(default_factory=list)
def __post_init__(self):
# Validation after auto-generated __init__
if not self.email or '@' not in self.email:
raise ValueError(f'Invalid email: {self.email}')
# Auto-generated: __init__, __repr__, __eq__
user = User(id=1, name='Alice', email='alice@example.com')
print(user) # User(id=1, name='Alice', email='alice@example.com', ...)
# Immutable dataclass:
@dataclass(frozen=True) # __hash__ generated, fields immutable
class Point:
x: float
y: float
Pydantic v2 — validation + serialization
from pydantic import BaseModel, Field, field_validator, model_validator
from typing import Optional
from datetime import datetime
class CreateUserRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: str = Field(pattern=r'^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$')
age: int = Field(ge=18, le=120)
role: str = Field(default='user')
@field_validator('email')
@classmethod
def lowercase_email(cls, v: str) -> str:
return v.lower().strip()
@field_validator('role')
@classmethod
def valid_role(cls, v: str) -> str:
if v not in ('user', 'admin', 'moderator'):
raise ValueError(f'Invalid role: {v}')
return v
# Parse and validate:
try:
user = CreateUserRequest(name='Alice', email='ALICE@Example.COM', age=25)
print(user.email) # 'alice@example.com' (auto-lowercased)
except ValidationError as e:
print(e.errors()) # Structured error list with field details
FastAPI integration
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
age: int
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post('/users', response_model=UserResponse, status_code=201)
async def create_user(body: UserCreate): # Auto-validated from request JSON
# body is already validated UserCreate instance
user = await db.create_user(body.model_dump())
return UserResponse(**user) # Only returns fields in response_model
# FastAPI generates OpenAPI docs automatically from Pydantic models
dataclasses vs Pydantic — when to use each
- ✅ dataclasses: internal data structures, performance-critical, trusted data
- ✅ Pydantic: API request bodies, config files, external data sources
- ✅ Pydantic v2: 5-50x faster than v1 — upgrade if still on v1
- ✅ Use model_dump() for dict conversion, model_json() for JSON string
- ❌ Never use plain dicts for structured data — no validation, no type hints
- ❌ Never use Pydantic for pure performance-critical inner loops — overhead
Pydantic models power FastAPI which is covered in the production API patterns. For validation in AWS Lambda, Lambda cold start shows why keeping Pydantic import lean matters. External reference: Pydantic v2 documentation.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
