|$ curl https://forge-ai.dev/api/markdown?path=docs/python/slots
$cat docs/python-—-slots.md
updated Last week·18 min read·published

Python — Slots

PythonAdvanced🎯Free Tools
Introduction

By default, Python stores instance attributes in a per-instance __dict__ dictionary. This is flexible but memory-expensive — each object carries its own dict even if the attributes are the same for every instance. __slots__ replaces this dict with a fixed set of named attributes stored in a compact array, dramatically reducing memory usage.

Slots also slightly speed up attribute access since there is no hash table lookup. The tradeoff is less flexibility: you cannot add new attributes at runtime and some metaclass patterns break.

__slots__ Basics
slots_basics.py
Python
1# Without __slots__ — uses __dict__
2class Point:
3 def __init__(self, x: float, y: float):
4 self.x = x
5 self.y = y
6
7p = Point(1.0, 2.0)
8print(p.__dict__) # {'x': 1.0, 'y': 2.0}
9p.z = 3.0 # ✓ can add new attributes
10print(p.__dict__) # {'x': 1.0, 'y': 2.0, 'z': 3.0}
11
12# With __slots__ — no __dict__, fixed attributes only
13class PointSlotted:
14 __slots__ = ("x", "y")
15
16 def __init__(self, x: float, y: float):
17 self.x = x
18 self.y = y
19
20ps = PointSlotted(1.0, 2.0)
21print(hasattr(ps, "__dict__")) # False
22print(ps.x, ps.y) # 1.0 2.0
23
24# ps.z = 3.0 # ✗ AttributeError: 'PointSlotted' object has no attribute 'z'
25
26# Access is slightly faster — direct descriptor lookup
27# No hash table overhead for attribute access
28
29# __slots__ with inheritance
30class Base:
31 __slots__ = ("x",)
32
33class Child(Base):
34 __slots__ = ("y",)
35
36c = Child()
37c.x = 1
38c.y = 2
39print(c.x, c.y) # 1 2
40
41# Each level adds its own slots
42# Child has slots for both x (from Base) and y
Memory Savings
memory.py
Python
1import sys
2
3class Regular:
4 def __init__(self, x, y, z):
5 self.x = x
6 self.y = y
7 self.z = z
8
9class Slotted:
10 __slots__ = ("x", "y", "z")
11
12 def __init__(self, x, y, z):
13 self.x = x
14 self.y = y
15 self.z = z
16
17r = Regular(1, 2, 3)
18s = Slotted(1, 2, 3)
19
20print(f"Regular: {sys.getsizeof(r)} bytes + {sys.getsizeof(r.__dict__)} bytes dict")
21# Regular: 48 bytes + 64 bytes dict → 112 bytes total
22print(f"Slotted: {sys.getsizeof(s)} bytes")
23# Slotted: 56 bytes (no dict needed)
24
25# Bulk comparison
26import tracemalloc
27
28def count_memory(cls, n: int) -> int:
29 tracemalloc.start()
30 objects = [cls(i, i * 2, i * 3) for i in range(n)]
31 _, peak = tracemalloc.get_traced_memory()
32 tracemalloc.stop()
33 return peak
34
35n = 100_000
36regular_mem = count_memory(Regular, n)
37slotted_mem = count_memory(Slotted, n)
38
39print(f"Regular: {regular_mem / 1024 / 1024:.1f} MB")
40print(f"Slotted: {slotted_mem / 1024 / 1024:.1f} MB")
41print(f"Savings: {(1 - slotted_mem / regular_mem) * 100:.0f}%")
42# Typically 40-60% memory reduction for simple classes

info

Memory savings scale with the number of instances. If you create millions of objects (ORM models, graph nodes, particles), slots can reduce memory usage by 40-60%.
Limitations
limitations.py
Python
1class Slotted:
2 __slots__ = ("x", "y")
3
4 def __init__(self, x, y):
5 self.x = x
6 self.y = y
7
8s = Slotted(1, 2)
9
10# Limitation 1: Cannot add arbitrary attributes
11# s.z = 3 # ✗ AttributeError
12
13# Limitation 2: Weak references need explicit slot
14import weakref
15
16class WeakRefFriendly:
17 __slots__ = ("x", "__weakref__") # must declare __weakref__
18
19w = WeakRefFriendly(42)
20ref = weakref.ref(w) # ✓ works now
21
22# Limitation 3: Multiple inheritance with non-empty slots is tricky
23class A:
24 __slots__ = ("x",)
25
26class B:
27 __slots__ = ("y",)
28
29# class C(A, B): # ✗ TypeError if both have non-empty __slots__
30# __slots__ = ()
31
32# Workaround — only one base can have slots in diamond
33class D(A): # A has slots
34 pass # D inherits A's slots, no new ones needed
35
36# Limitation 4: __dict__ can be re-added if needed
37class FlexibleSlotted:
38 __slots__ = ("x",)
39
40f = FlexibleSlotted()
41f.x = 1
42# f.y = 2 # ✗ AttributeError
43
44# But you can explicitly allow dict:
45class AllowDict:
46 __slots__ = ("x", "__dict__")
47
48ad = AllowDict()
49ad.x = 1
50ad.y = 2 # ✓ goes into __dict__
51print(ad.__dict__) # {'y': 2}
52
53# Limitation 5: Class variables need to be defined
54# outside __slots__
55class WithClassVar:
56 __slots__ = ("x",)
57 counter = 0 # class variable — not in slots
58
59 def __init__(self, x):
60 self.x = x
61 WithClassVar.counter += 1
62
63print(WithClassVar.counter) # 0
64a = WithClassVar(1)
65b = WithClassVar(2)
66print(WithClassVar.counter) # 2
slotclass Decorator

Manually writing __slots__ is repetitive. This decorator automatically generates __slots__ from type-annotated fields, combining the ergonomics of @dataclass with the memory efficiency of slots.

slotclass.py
Python
1import sys
2from functools import wraps
3
4def slotclass(cls):
5 """Decorator that auto-generates __slots__ from type annotations."""
6 slots = []
7 annotations = getattr(cls, "__annotations__", {})
8 for name in annotations:
9 if not name.startswith("_"):
10 slots.append(name)
11 cls.__slots__ = tuple(slots)
12
13 # Remove __dict__ if present (from class body before decoration)
14 if "__dict__" in cls.__dict__:
15 del cls.__dict__["__dict__"]
16
17 return cls
18
19# Usage — just add type hints, decorator handles __slots__
20@slotclass
21class Vector:
22 x: float
23 y: float
24 z: float
25
26 def __init__(self, x: float, y: float, z: float):
27 self.x = x
28 self.y = y
29 self.z = z
30
31 def magnitude(self) -> float:
32 return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
33
34v = Vector(3.0, 4.0, 0.0)
35print(v.magnitude()) # 5.0
36print(hasattr(v, "__dict__")) # False — slots are active
37
38# Compare with dataclass
39from dataclasses import dataclass
40
41@dataclass
42class VectorDataclass:
43 x: float
44 y: float
45 z: float
46
47@dataclass(slots=True) # dataclass also supports slots (3.10+)
48class VectorDataclassSlots:
49 x: float
50 y: float
51 z: float
52
53v1 = Vector(1.0, 2.0, 3.0)
54v2 = VectorDataclass(1.0, 2.0, 3.0)
55v3 = VectorDataclassSlots(1.0, 2.0, 3.0)
56
57print(f"Custom slotclass: {sys.getsizeof(v1)} bytes")
58print(f"Regular dataclass: {sys.getsizeof(v2)} bytes + {sys.getsizeof(v2.__dict__)} dict")
59print(f"dataclass(slots=True): {sys.getsizeof(v3)} bytes")

best practice

Python 3.10+ dataclasses support @dataclass(slots=True)natively. Use that if you don't need custom behavior. The decorator pattern is useful for non-dataclass classes that still want slot benefits.
Performance Benchmarks
benchmarks.py
Python
1import timeit
2import sys
3
4class Regular:
5 def __init__(self, x, y):
6 self.x = x
7 self.y = y
8
9class Slotted:
10 __slots__ = ("x", "y")
11 def __init__(self, x, y):
12 self.x = x
13 self.y = y
14
15# Benchmark: instance creation
16regular_time = timeit.timeit(
17 "Regular(1, 2)",
18 globals={"Regular": Regular},
19 number=1_000_000,
20)
21slotted_time = timeit.timeit(
22 "Slotted(1, 2)",
23 globals={"Slotted": Slotted},
24 number=1_000_000,
25)
26print(f"Create 1M instances:")
27print(f" Regular: {regular_time:.3f}s")
28print(f" Slotted: {slotted_time:.3f}s")
29
30# Benchmark: attribute access
31r = Regular(1, 2)
32s = Slotted(1, 2)
33
34regular_attr = timeit.timeit(
35 "r.x; r.y",
36 globals={"r": r},
37 number=5_000_000,
38)
39slotted_attr = timeit.timeit(
40 "s.x; s.y",
41 globals={"s": s},
42 number=5_000_000,
43)
44print(f"\n5M attribute accesses:")
45print(f" Regular: {regular_attr:.3f}s")
46print(f" Slotted: {slotted_attr:.3f}s")
47
48# Typical results:
49# Create 1M instances:
50# Regular: 0.280s
51# Slotted: 0.190s (~32% faster)
52#
53# 5M attribute accesses:
54# Regular: 0.150s
55# Slotted: 0.100s (~33% faster)
56#
57# Memory: Slotted uses ~40% less per instance
When to Use Slots

Use slots when: You create many instances of the same class (ORM models, data points, AST nodes). Memory is constrained. You want faster attribute access. You are building a library where instance structure is fixed.

Skip slots when: You need dynamic attributes. You use __dict__-based patterns (pickle, copy). The class is rarely instantiated. You need weak references without declaring __weakref__.

when_to_use.py
Python
1# Good use cases for __slots__:
2
3# 1. Data-heavy classes with many instances
4class Particle:
5 __slots__ = ("x", "y", "z", "vx", "vy", "vz", "mass")
6 def __init__(self, x, y, z, vx, vy, vz, mass):
7 self.x, self.y, self.z = x, y, z
8 self.vx, self.vy, self.vz = vx, vy, vz
9 self.mass = mass
10
11# 2. AST nodes (Python's own AST uses slots)
12class BinOp:
13 __slots__ = ("left", "op", "right")
14 def __init__(self, left, op, right):
15 self.left = left
16 self.op = op
17 self.right = right
18
19# 3. Named tuples alternative with methods
20class Color:
21 __slots__ = ("r", "g", "b", "a")
22 def __init__(self, r, g, b, a=255):
23 self.r, self.g, self.b, self.a = r, g, b, a
24 def to_hex(self) -> str:
25 return f"#{self.r:02x}{self.g:02x}{self.b:02x}{self.a:02x}"
26 def __repr__(self):
27 return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"
28
29# 4. Caching with fixed attributes
30class CacheEntry:
31 __slots__ = ("key", "value", "expires")
32 def __init__(self, key, value, expires):
33 self.key = key
34 self.value = value
35 self.expires = expires
$Blueprint — Engineering Documentation·Section ID: PYTHON-SLOTS·Revision: 1.0