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: floatPydantic 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 detailsFastAPI 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 modelsWhat @dataclass generates, and the class features it builds on
A dataclass is an ordinary Python class with generated methods: __init__, __repr__ and __eq__ by default. A few behaviors catch people out. With the default eq=True and no frozen=True, the class sets __hash__ to None, so instances can’t go into sets or be dict keys. With frozen=True and eq=True, a hash is generated from the fields. Mutable defaults must use field(default_factory=...), and validation goes in __post_init__.
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
import math
@dataclass
class Account:
owner: str
balance: float = 0.0
tags: list[str] = field(default_factory=list) # never a shared mutable default
def __post_init__(self):
if self.balance < 0:
raise ValueError(f"balance cannot be negative: {self.balance}")
@dataclass(frozen=True)
class Money:
amount: int
currency: str
def __add__(self, other: "Money") -> "Money":
if other.currency != self.currency:
raise ValueError("currency mismatch")
return Money(self.amount + other.amount, self.currency)
print(Account("ana")) # generated __repr__
print(Account("ana") == Account("ana")) # generated __eq__: True
print(Account.__hash__) # None: eq=True without frozen makes it unhashable
print({Money(5, "USD") + Money(7, "USD")}) # frozen + eq: hashable, usable in sets
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
def describe(self) -> str:
return f"{type(self).__name__}: area={self.area():.2f}"
@dataclass
class Circle(Shape):
r: float
def area(self) -> float:
return math.pi * self.r ** 2
print(Circle(5).describe()) # Circle: area=78.54
class A:
def method(self): return "A"
class B(A):
def method(self): return "B" + super().method()
class C(A):
def method(self): return "C" + super().method()
class D(B, C):
pass
print(D().method(), [k.__name__ for k in D.__mro__]) # BCA ['D', 'B', 'C', 'A', 'object']
try:
Shape()
except TypeError as e:
print("TypeError:", e)The output confirms each point: the generated repr and equality, Account.__hash__ is None, the frozen Money works in a set, and an abstract base class can’t be instantiated until a subclass implements every abstract method.
Dataclasses still use the normal class machinery. super() follows the method resolution order (MRO), which Python computes with C3 linearization: for class D(B, C) the order is D, B, C, A, so D().method() returns 'BCA'. Dunder methods such as __add__ work on dataclasses as on any class. If you write __eq__ yourself on a regular class, you must also define __hash__ if instances should be hashable. See the dataclasses documentation and the Python data model.
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.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Related: Python OOP: Classes, Inheritance, Dunder Methods, and Dataclasses Explained · Python Dataclasses: 10 Advanced Features That Make __init__ Obsolete · Python Generators and Yield: Lazy Evaluation for Memory-Efficient Data Processing
Related: How to Build a Modern AWS Data Lake for Operational and Business Analytics with S3, Glue, Athena, Kinesis, and EventBridge · Python Decorators Explained: From Simple Wrappers to Production Patterns · Python asyncio vs Threading vs Multiprocessing: When to Use Each
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
