A normal Python class stores its attributes in a per-instance dict — obj.__dict__. That dict has overhead. For a class with millions of instances, the overhead matters:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.x, p.y) # 3 4
# p.z = 99 # AttributeError — z is not in __slots__What does __slots__ actually do?
Two things. (1) No per-instance __dict__. Instead of a dict mapping names to values, the instance gets fixed slots — basically named array positions. Memory drops from ~100 bytes per instance to ~50 for a small class. (2) Fixed attribute set. Trying to set p.z = 99 raises AttributeError — z isn't declared. Typos that would silently create new attributes on a dict-backed class are caught.
When does the memory actually matter?
Only when you have a lot of instances — millions of points, millions of records in memory. For 100 instances, the saving is meaningless and __slots__ just makes the class less flexible. Default to no slots; reach for slots only when memory profiling shows the class is a hot spot.
Anything that breaks?
A few things. (1) Can't add new attributes at runtime (which is sometimes the point, sometimes a regression). (2) Multiple inheritance is fiddly — only one parent in the chain can have non-empty __slots__. (3) Doesn't compose cleanly with __dict__ unless you opt in by including "__dict__" in slots. Most of these don't matter for simple @dataclass-like containers.
__slots__ — declare attributes, save memoryNormally, every instance gets a __dict__ to store its attributes:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.__dict__) # {'x': 3, 'y': 4}
p.z = 99 # works — silently adds to the dictWith __slots__ you declare the allowed attributes upfront:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
# p.__dict__ # AttributeError — no __dict__
p.z = 99 # AttributeError: 'Point' object has no attribute 'z'__slots__ with @dataclassPython 3.10+ has @dataclass(slots=True) — same effect as writing __slots__ by hand:
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(3, 4)
p.z = 99 # AttributeErrorIf you're already using @dataclass, this is the cleaner way to add slots.
__dict__ — can't add attributes at runtime, can't use class-level introspection that expects a dict__slots__; the others must use __slots__ = ()__dict__ — slots only work end-to-end if every ancestor is slotted (or empty-slotted)Yes:
No:
A simple two-attribute class:
| Variant | Bytes per instance (CPython 3.11) |
|---|---|
Plain class (with __dict__) | ~100-120 |
With __slots__ | ~50-60 |
The difference is dominated by the __dict__. For 1M instances: ~50MB vs ~110MB. Real win at scale; invisible at small N.
Define a Point class with __slots__ = ("x", "y") and verify the strict-attribute behaviour: p.x and p.y work, p.z = 99 raises.
A normal Python class stores its attributes in a per-instance dict — obj.__dict__. That dict has overhead. For a class with millions of instances, the overhead matters:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.x, p.y) # 3 4
# p.z = 99 # AttributeError — z is not in __slots__What does __slots__ actually do?
Two things. (1) No per-instance __dict__. Instead of a dict mapping names to values, the instance gets fixed slots — basically named array positions. Memory drops from ~100 bytes per instance to ~50 for a small class. (2) Fixed attribute set. Trying to set p.z = 99 raises AttributeError — z isn't declared. Typos that would silently create new attributes on a dict-backed class are caught.
When does the memory actually matter?
Only when you have a lot of instances — millions of points, millions of records in memory. For 100 instances, the saving is meaningless and __slots__ just makes the class less flexible. Default to no slots; reach for slots only when memory profiling shows the class is a hot spot.
Anything that breaks?
A few things. (1) Can't add new attributes at runtime (which is sometimes the point, sometimes a regression). (2) Multiple inheritance is fiddly — only one parent in the chain can have non-empty __slots__. (3) Doesn't compose cleanly with __dict__ unless you opt in by including "__dict__" in slots. Most of these don't matter for simple @dataclass-like containers.
__slots__ — declare attributes, save memoryNormally, every instance gets a __dict__ to store its attributes:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.__dict__) # {'x': 3, 'y': 4}
p.z = 99 # works — silently adds to the dictWith __slots__ you declare the allowed attributes upfront:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
# p.__dict__ # AttributeError — no __dict__
p.z = 99 # AttributeError: 'Point' object has no attribute 'z'__slots__ with @dataclassPython 3.10+ has @dataclass(slots=True) — same effect as writing __slots__ by hand:
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(3, 4)
p.z = 99 # AttributeErrorIf you're already using @dataclass, this is the cleaner way to add slots.
__dict__ — can't add attributes at runtime, can't use class-level introspection that expects a dict__slots__; the others must use __slots__ = ()__dict__ — slots only work end-to-end if every ancestor is slotted (or empty-slotted)Yes:
No:
A simple two-attribute class:
| Variant | Bytes per instance (CPython 3.11) |
|---|---|
Plain class (with __dict__) | ~100-120 |
With __slots__ | ~50-60 |
The difference is dominated by the __dict__. For 1M instances: ~50MB vs ~110MB. Real win at scale; invisible at small N.
Define a Point class with __slots__ = ("x", "y") and verify the strict-attribute behaviour: p.x and p.y work, p.z = 99 raises.
Create a free account to get started. Paid plans unlock all tracks.