__slots__ — when it saves memory and what it breaks
__slots__ declares a fixed set of attributes for a class, replacing the per-instance __dict__ with a compact array of slots.
What it does
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.x = 10 #
p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
p.__dict__ # AttributeError: 'Point' object has no attribute '__dict__'Without slots:
class Point2:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point2(1, 2)
p.z = 3 # — instance dict accommodates anything
p.__dict__ # {'x': 1, 'y': 2, 'z': 3}Memory impact
Since Python 3.11 a regular instance stores its attributes inline and only
builds a real __dict__ when something asks for it, so the saving is smaller
than older articles claim. And sys.getsizeof(a.__dict__) is a misleading
measurement: reading a.__dict__ materialises the dict it is measuring.
Measured with tracemalloc over 200,000 two-attribute instances (each also
holding one int of about 32 B):
3.12.6 regular 112 B slotted 80 B ratio 1.40x (ints included)
3.14.5 regular 120 B slotted 80 B ratio 1.50x (ints included)The objects alone are about 88 B against 48 B on 3.14, roughly 1.8x, and the gap stays that size whatever the attributes hold. It matters for millions of small objects (graph nodes, order book entries, ORM rows); for thousands it is noise.
Attribute access is also slightly faster (slot is a fixed offset, dict requires hash lookup).
What slots break
- No dynamic attributes.
instance.new_attr = ...fails for anything not in slots. - No
__dict__. Code that introspects viavars(obj)orobj.__dict__breaks. - Pickling needs care — default pickling uses
__dict__; slotted classes need__getstate__/__setstate__or rely on pickle’s slot support. - Multiple inheritance is restricted — at most one base class with non-empty
__slots__. Otherwise:TypeError: multiple bases have instance lay-out conflict. @cached_propertydoesn’t work without explicitly adding the attribute name to__slots__.- Class-level defaults conflict —
__slots__ = ("x",)withx = 5at class level fails (slot descriptor masks the default).
Subclassing
If a slotted class is subclassed by a non-slotted class, the subclass instances do get a __dict__ (slots don’t propagate as a constraint, only as a memory layout hint).
class A:
__slots__ = ("x",)
class B(A): # no __slots__ declared
pass
b = B()
b.x = 1 # from slot
b.y = 2 # B has __dict__To keep memory savings in subclasses, declare __slots__ = () (empty) in the subclass.
With @dataclass
Python 3.10+:
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: int
y: intEquivalent to writing __slots__ = ("x", "y") manually.
When to use
- Many instances (tens of thousands+) → big memory win
- Class is a value object / data container → no need for dynamic attrs anyway
- Performance-critical attribute access in tight loops
When NOT to use
- Few instances → negligible benefit, lose flexibility
- Dynamic plugins / mixins that monkey-patch instances
- You need pickling without thinking about it
- Heavy multiple inheritance
Interview angle
“What does __slots__ do?” (Replaces __dict__ with a fixed array — saves memory.) Follow-up: “What are the trade-offs?” (No dynamic attrs, MI restrictions, pickling caveats.) Senior follow-up: “When would you use it?” (High-instance-count value classes — graph nodes, time-series records.)