Frozen + slots Dataclasses
`@dataclass(frozen=True)` makes instances immutable and hashable, which lets you use them as dict keys or set members. `slots=True` (3.10+) skips the `__dict__` allocation, saving roughly 40% of memory per instance and making attribute access slightly faster. This entry covers each flag separately and shows when combining them is the right call.
591 views
13
from dataclasses import dataclass
@dataclass(frozen=True)
class Coord:
x: int
y: int
c = Coord(3, 4)
print(c) # Coord(x=3, y=4)
try:
c.x = 99 # frozen instances reject attribute assignment
except Exception as exc:
print(type(exc).__name__, ':', exc)
# FrozenInstanceError: cannot assign to field 'x'
# Hashable for free, so they work as dict keys and set members.
seen = {Coord(0, 0), Coord(1, 0), Coord(0, 0)}
print(seen) # {Coord(x=0, y=0), Coord(x=1, y=0)}
print(len(seen)) # 2
weights = {Coord(0, 0): 1.0, Coord(1, 0): 2.5}
print(weights[Coord(0, 0)]) # 1.0frozen=True overrides __setattr__ to raise FrozenInstanceError on every assignment after construction. As a side effect Python also synthesizes __hash__ (mutable dataclasses are unhashable by default), which makes the instance usable as a dict key, a set member, or an lru_cache argument. Use frozen for value objects like coordinates, ranges, money amounts: anything where two instances with the same fields should be the same logical thing. The cost is dataclasses.replace(c, x=99) instead of c.x = 99 for updates.
from dataclasses import dataclass
import sys
@dataclass
class Wide:
a: int
b: int
c: int
@dataclass(slots=True)
class Narrow:
a: int
b: int
c: int
w = Wide(1, 2, 3)
n = Narrow(1, 2, 3)
# slots=True removes __dict__, so each instance is significantly smaller.
print('Wide has __dict__:', hasattr(w, '__dict__')) # True
print('Narrow has __dict__:', hasattr(n, '__dict__')) # False
print('size of Wide instance dict:', sys.getsizeof(w.__dict__))
print('Narrow.__slots__:', Narrow.__slots__)
# Trying to add an undeclared attribute fails on a slots class.
try:
n.d = 99
except AttributeError as exc:
print('AttributeError:', exc)
# A regular dataclass happily accepts new attributes (a common bug source).
w.d = 99
print('Wide accepted w.d =', w.d)Without slots, every instance carries a per-instance __dict__ so you can attach arbitrary attributes at runtime. slots=True declares the exact set of allowed attribute names, drops the __dict__, and cuts memory by roughly 40% per instance. The trade-off is that typos like n.scope = 1 (when you meant n.score) raise AttributeError instead of silently creating a new attribute, which is usually a feature, not a bug. Reach for slots when you allocate millions of instances of the same class (graph nodes, ECS components, parsed records) or when you want the class to refuse mystery attributes.
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class Money:
amount: int # store cents to avoid float drift
currency: str
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError(f'currency mismatch: {self.currency} vs {other.currency}')
return Money(self.amount + other.amount, self.currency)
fee = Money(150, 'EUR')
tax = Money(30, 'EUR')
total = fee.add(tax)
print(total) # Money(amount=180, currency='EUR')
# 'mutate' by producing a new instance with replace().
discounted = replace(total, amount=total.amount - 20)
print(discounted) # Money(amount=160, currency='EUR')
# Hashable, so you can build a price book.
price_book = {Money(100, 'EUR'): 'small', Money(500, 'EUR'): 'medium'}
print(price_book[Money(100, 'EUR')]) # small
print('total memory of one Money:', Money.__slots__)frozen=True, slots=True is the canonical 'value object' shape: immutable, hashable, memory-flat. frozen guarantees the object cannot drift after construction (no Money instance ever changes its amount), and slots keeps the per-instance footprint tiny when you have lots of them. Updates use dataclasses.replace(obj, field=new) which produces a new instance with the field changed, leaving the old one untouched. The combo is the right starting point for domain primitives in any non-trivial codebase: prices, intervals, identifiers, version numbers, geographic coordinates.
