Python Dataclasses: A Complete Guide
Master Python dataclasses — automatic __init__, __repr__, ordering, immutability, default factories, and post-init processing.
What you'll learn
- ✓How dataclasses reduce boilerplate for data-holding classes
- ✓Field options: defaults, factories, metadata
- ✓Frozen dataclasses for immutability
- ✓Post-init processing and inheritance
Prerequisites
- •Python classes and type hints
Dataclasses, introduced in Python 3.7, generate __init__, __repr__, __eq__, and more from class annotations. They eliminate boilerplate for classes that primarily hold data.
Before vs after
Without dataclasses:
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
With dataclasses:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
You get __init__, __repr__, and __eq__ automatically.
p = Point(3.0, 4.0)
print(p) # Point(x=3.0, y=4.0)
print(p == Point(3.0, 4.0)) # True
Default values
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
config = Config()
print(config) # Config(host='localhost', port=8080, debug=False)
Fields with defaults must come after fields without defaults.
Default factories
For mutable defaults (lists, dicts), use field(default_factory=...):
from dataclasses import dataclass, field
@dataclass
class User:
name: str
tags: list[str] = field(default_factory=list)
metadata: dict = field(default_factory=dict)
u1 = User("Alice")
u2 = User("Bob")
u1.tags.append("admin")
print(u2.tags) # [] — separate list, not shared
Frozen (immutable) dataclasses
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
c = Coordinate(40.7128, -74.0060)
# c.lat = 0 # FrozenInstanceError
Frozen dataclasses are hashable, so they can be used as dict keys or set members.
Ordering
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
versions = [Version(2, 0, 0), Version(1, 5, 3), Version(1, 5, 2)]
print(sorted(versions))
# [Version(major=1, minor=5, patch=2), Version(major=1, minor=5, patch=3), Version(major=2, minor=0, patch=0)]
Post-init processing
__post_init__ runs after the generated __init__.
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
r = Rectangle(3, 4)
print(r.area) # 12.0
Field options
@dataclass
class Product:
name: str
price: float
_id: str = field(repr=False) # hidden from repr
_cache: dict = field(init=False, # not in __init__
repr=False, # not in repr
compare=False, # not in __eq__
default_factory=dict)
Inheritance
@dataclass
class Animal:
name: str
species: str
@dataclass
class Pet(Animal):
owner: str
vaccinated: bool = True
pet = Pet(name="Rex", species="Dog", owner="Alice")
print(pet) # Pet(name='Rex', species='Dog', owner='Alice', vaccinated=True)
Converting to dicts and tuples
from dataclasses import asdict, astuple
@dataclass
class User:
name: str
age: int
email: str
user = User("Alice", 30, "alice@example.com")
print(asdict(user)) # {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
print(astuple(user)) # ('Alice', 30, 'alice@example.com')
Slots
Python 3.10+ supports slots=True for memory efficiency and faster attribute access.
@dataclass(slots=True)
class Point:
x: float
y: float
Dataclass vs NamedTuple vs Pydantic
| Feature | dataclass | NamedTuple | Pydantic |
|---|---|---|---|
| Mutable by default | Yes | No | Yes |
| Type validation at runtime | No | No | Yes |
| JSON serialization | Manual | Manual | Built-in |
| Default values | Yes | Yes | Yes |
| Inheritance | Yes | Limited | Yes |
| Performance | Fast | Fastest | Slower (validation) |
Use dataclasses for internal data structures, NamedTuple for immutable lightweight records, and Pydantic for API boundaries where validation matters.
Summary
Dataclasses eliminate boilerplate for data-holding classes. Use frozen=True for immutability, field(default_factory=...) for mutable defaults, __post_init__ for computed fields, and slots=True for performance. They are the standard way to define structured data in modern Python.
Related articles
- Python Python Dataclasses vs Pydantic: When to Use Which
Compare Python dataclasses and Pydantic models side by side. Learn their strengths, performance traits, and how to pick the right tool for your project.
- Python Advanced Python Dataclasses: Beyond the Basics
Go beyond basic dataclass usage with post-init processing, field factories, inheritance, frozen instances, and custom serialization.
- Python Python asyncio Tutorial
Learn Python's asyncio — coroutines, tasks, event loops, async generators, and building concurrent I/O-bound applications.
- Python Python Generators Deep Dive
A thorough exploration of Python generators: yield mechanics, send and throw, generator pipelines, memory efficiency, coroutine basics, and practical patterns.