Python Dataclasses (@dataclass)
A dataclass is a class that mainly stores data. The @dataclass@dataclass decorator (from the dataclassesdataclasses module, Python 3.7+) writes the boilerplate for you: __init____init__, __repr____repr__, __eq____eq__, and more — generated from the field annotations.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(3, 4)
print(p) # Point(x=3, y=4) <- free __repr__
print(p.x, p.y) # 3 4
print(p == Point(3, 4)) # True <- free __eq__from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(3, 4)
print(p) # Point(x=3, y=4) <- free __repr__
print(p.x, p.y) # 3 4
print(p == Point(3, 4)) # True <- free __eq__What it generates for you
Without dataclasses, that PointPoint would need a hand-written __init____init__, __repr____repr__, and __eq____eq__. The decorator generates them from the annotated fields:
| You write | @dataclass@dataclass generates |
|---|---|
x: intx: int | A parameter in __init____init__ and an attribute. |
| (nothing) | __repr____repr__ → Point(x=3, y=4)Point(x=3, y=4). |
| (nothing) | __eq____eq__ → field-by-field equality. |
order=Trueorder=True | <<, <=<=, >>, >=>= comparisons. |
frozen=Truefrozen=True | Immutable, hashable instances. |
Default values
Give fields defaults just like function parameters. Fields with defaults must come after those without.
from dataclasses import dataclass
@dataclass
class User:
name: str
active: bool = True
role: str = "member"
print(User("Ada")) # User(name='Ada', active=True, role='member')
print(User("Bo", role="admin")) # User(name='Bo', active=True, role='admin')from dataclasses import dataclass
@dataclass
class User:
name: str
active: bool = True
role: str = "member"
print(User("Ada")) # User(name='Ada', active=True, role='member')
print(User("Bo", role="admin")) # User(name='Bo', active=True, role='admin')Mutable defaults — use default_factory
You cannot use a mutable default (like [][] or {}{}) directly — it would be shared across all instances. Use field(default_factory=...)field(default_factory=...).
from dataclasses import dataclass, field
@dataclass
class Cart:
items: list = field(default_factory=list) # a fresh list per instance
tags: dict = field(default_factory=dict)
a = Cart()
a.items.append("apple")
b = Cart()
print(a.items) # ['apple']
print(b.items) # [] <- not shared, thanks to default_factoryfrom dataclasses import dataclass, field
@dataclass
class Cart:
items: list = field(default_factory=list) # a fresh list per instance
tags: dict = field(default_factory=dict)
a = Cart()
a.items.append("apple")
b = Cart()
print(a.items) # ['apple']
print(b.items) # [] <- not shared, thanks to default_factoryWriting
items: list = []items: list = []raises aValueErrorValueErrorin dataclasses precisely to prevent the shared-mutable-default bug.
Frozen (immutable) dataclasses
frozen=Truefrozen=True makes instances read-only and hashable — usable as dict keys or set members.
from dataclasses import dataclass
@dataclass(frozen=True)
class Coord:
lat: float
lon: float
c = Coord(51.5, -0.1)
print(c) # Coord(lat=51.5, lon=-0.1)
locations = {c: "London"} # works: frozen instances are hashable
# c.lat = 0.0 # would raise FrozenInstanceErrorfrom dataclasses import dataclass
@dataclass(frozen=True)
class Coord:
lat: float
lon: float
c = Coord(51.5, -0.1)
print(c) # Coord(lat=51.5, lon=-0.1)
locations = {c: "London"} # works: frozen instances are hashable
# c.lat = 0.0 # would raise FrozenInstanceErrorOrdered dataclasses
order=Trueorder=True adds comparison operators based on the fields (compared as a tuple, top to bottom).
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(2, 0), Version(1, 5), Version(1, 9)]
print(sorted(versions))
# [Version(major=1, minor=5), Version(major=1, minor=9), Version(major=2, minor=0)]from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(2, 0), Version(1, 5), Version(1, 9)]
print(sorted(versions))
# [Version(major=1, minor=5), Version(major=1, minor=9), Version(major=2, minor=0)]post_init — validation and derived fields
__post_init____post_init__ runs right after the generated __init____init__, perfect for validation or computing derived values.
from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
area: float = 0.0
def __post_init__(self):
if self.width < 0 or self.height < 0:
raise ValueError("dimensions must be non-negative")
self.area = self.width * self.height
r = Rectangle(3, 4)
print(r.area) # 12.0from dataclasses import dataclass
@dataclass
class Rectangle:
width: float
height: float
area: float = 0.0
def __post_init__(self):
if self.width < 0 or self.height < 0:
raise ValueError("dimensions must be non-negative")
self.area = self.width * self.height
r = Rectangle(3, 4)
print(r.area) # 12.0Converting to dict or tuple
from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(asdict(p)) # {'x': 1, 'y': 2}
print(astuple(p)) # (1, 2)from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(asdict(p)) # {'x': 1, 'y': 2}
print(astuple(p)) # (1, 2)When to use what
| Need | Use |
|---|---|
| A mutable record with methods | @dataclass@dataclass |
| An immutable, hashable record | @dataclass(frozen=True)@dataclass(frozen=True) |
| A tiny immutable record, tuple-like | namedtuplenamedtuple |
| Arbitrary, schemaless data | a plain dictdict |
Common pitfalls
- Mutable defaults — never
x: list = []x: list = []; usefield(default_factory=list)field(default_factory=list). - Field order — non-default fields must precede default ones.
- Equality is field-based — two instances with equal fields are
====(usually what you want). frozen=Truefrozen=Trueblocks attribute assignment — set everything via__init____init__/__post_init____post_init__.
Practice Exercises
Exercise 1 – Define a dataclass
Exercise 2 – Safe mutable default
Exercise 3 – Convert to a dict
Summary
@dataclass@dataclassauto-generates__init____init__,__repr____repr__, and__eq____eq__from field annotations.- Use defaults like function parameters; use
field(default_factory=...)field(default_factory=...)for mutable defaults. frozen=Truefrozen=Truemakes immutable, hashable records;order=Trueorder=Trueadds comparisons.__post_init____post_init__validates or computes derived fields;asdictasdict/astupleastupleconvert instances.- Reach for dataclasses whenever a class is mostly structured data.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
