__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
The instance dict is the biggest contributor to per-object memory in non-slotted classes. For 1M instances, that’s ~64 MB just in dict overhead.
Quick comparison with pympler or sys.getsizeof:
import sys
class A:
pass
class B:
__slots__ = ("x", "y")
a = A(); a.x = 1; a.y = 2
b = B(); b.x = 1; b.y = 2
sys.getsizeof(a) + sys.getsizeof(a.__dict__) # ~152 bytes
sys.getsizeof(b) # ~48 bytes
Roughly 2-3x memory savings for small data classes with many instances. Useful for: graph nodes, particle systems, ORM rows, trading order books, ML feature vectors.
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: int
Equivalent 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.)