Python — Slots
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.
| 1 | # Without __slots__ — uses __dict__ |
| 2 | class Point: |
| 3 | def __init__(self, x: float, y: float): |
| 4 | self.x = x |
| 5 | self.y = y |
| 6 | |
| 7 | p = Point(1.0, 2.0) |
| 8 | print(p.__dict__) # {'x': 1.0, 'y': 2.0} |
| 9 | p.z = 3.0 # ✓ can add new attributes |
| 10 | print(p.__dict__) # {'x': 1.0, 'y': 2.0, 'z': 3.0} |
| 11 | |
| 12 | # With __slots__ — no __dict__, fixed attributes only |
| 13 | class PointSlotted: |
| 14 | __slots__ = ("x", "y") |
| 15 | |
| 16 | def __init__(self, x: float, y: float): |
| 17 | self.x = x |
| 18 | self.y = y |
| 19 | |
| 20 | ps = PointSlotted(1.0, 2.0) |
| 21 | print(hasattr(ps, "__dict__")) # False |
| 22 | print(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 |
| 30 | class Base: |
| 31 | __slots__ = ("x",) |
| 32 | |
| 33 | class Child(Base): |
| 34 | __slots__ = ("y",) |
| 35 | |
| 36 | c = Child() |
| 37 | c.x = 1 |
| 38 | c.y = 2 |
| 39 | print(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 |
| 1 | import sys |
| 2 | |
| 3 | class Regular: |
| 4 | def __init__(self, x, y, z): |
| 5 | self.x = x |
| 6 | self.y = y |
| 7 | self.z = z |
| 8 | |
| 9 | class 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 | |
| 17 | r = Regular(1, 2, 3) |
| 18 | s = Slotted(1, 2, 3) |
| 19 | |
| 20 | print(f"Regular: {sys.getsizeof(r)} bytes + {sys.getsizeof(r.__dict__)} bytes dict") |
| 21 | # Regular: 48 bytes + 64 bytes dict → 112 bytes total |
| 22 | print(f"Slotted: {sys.getsizeof(s)} bytes") |
| 23 | # Slotted: 56 bytes (no dict needed) |
| 24 | |
| 25 | # Bulk comparison |
| 26 | import tracemalloc |
| 27 | |
| 28 | def 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 | |
| 35 | n = 100_000 |
| 36 | regular_mem = count_memory(Regular, n) |
| 37 | slotted_mem = count_memory(Slotted, n) |
| 38 | |
| 39 | print(f"Regular: {regular_mem / 1024 / 1024:.1f} MB") |
| 40 | print(f"Slotted: {slotted_mem / 1024 / 1024:.1f} MB") |
| 41 | print(f"Savings: {(1 - slotted_mem / regular_mem) * 100:.0f}%") |
| 42 | # Typically 40-60% memory reduction for simple classes |
info
| 1 | class Slotted: |
| 2 | __slots__ = ("x", "y") |
| 3 | |
| 4 | def __init__(self, x, y): |
| 5 | self.x = x |
| 6 | self.y = y |
| 7 | |
| 8 | s = 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 |
| 14 | import weakref |
| 15 | |
| 16 | class WeakRefFriendly: |
| 17 | __slots__ = ("x", "__weakref__") # must declare __weakref__ |
| 18 | |
| 19 | w = WeakRefFriendly(42) |
| 20 | ref = weakref.ref(w) # ✓ works now |
| 21 | |
| 22 | # Limitation 3: Multiple inheritance with non-empty slots is tricky |
| 23 | class A: |
| 24 | __slots__ = ("x",) |
| 25 | |
| 26 | class 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 |
| 33 | class 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 |
| 37 | class FlexibleSlotted: |
| 38 | __slots__ = ("x",) |
| 39 | |
| 40 | f = FlexibleSlotted() |
| 41 | f.x = 1 |
| 42 | # f.y = 2 # ✗ AttributeError |
| 43 | |
| 44 | # But you can explicitly allow dict: |
| 45 | class AllowDict: |
| 46 | __slots__ = ("x", "__dict__") |
| 47 | |
| 48 | ad = AllowDict() |
| 49 | ad.x = 1 |
| 50 | ad.y = 2 # ✓ goes into __dict__ |
| 51 | print(ad.__dict__) # {'y': 2} |
| 52 | |
| 53 | # Limitation 5: Class variables need to be defined |
| 54 | # outside __slots__ |
| 55 | class 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 | |
| 63 | print(WithClassVar.counter) # 0 |
| 64 | a = WithClassVar(1) |
| 65 | b = WithClassVar(2) |
| 66 | print(WithClassVar.counter) # 2 |
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.
| 1 | import sys |
| 2 | from functools import wraps |
| 3 | |
| 4 | def 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 |
| 21 | class 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 | |
| 34 | v = Vector(3.0, 4.0, 0.0) |
| 35 | print(v.magnitude()) # 5.0 |
| 36 | print(hasattr(v, "__dict__")) # False — slots are active |
| 37 | |
| 38 | # Compare with dataclass |
| 39 | from dataclasses import dataclass |
| 40 | |
| 41 | @dataclass |
| 42 | class VectorDataclass: |
| 43 | x: float |
| 44 | y: float |
| 45 | z: float |
| 46 | |
| 47 | @dataclass(slots=True) # dataclass also supports slots (3.10+) |
| 48 | class VectorDataclassSlots: |
| 49 | x: float |
| 50 | y: float |
| 51 | z: float |
| 52 | |
| 53 | v1 = Vector(1.0, 2.0, 3.0) |
| 54 | v2 = VectorDataclass(1.0, 2.0, 3.0) |
| 55 | v3 = VectorDataclassSlots(1.0, 2.0, 3.0) |
| 56 | |
| 57 | print(f"Custom slotclass: {sys.getsizeof(v1)} bytes") |
| 58 | print(f"Regular dataclass: {sys.getsizeof(v2)} bytes + {sys.getsizeof(v2.__dict__)} dict") |
| 59 | print(f"dataclass(slots=True): {sys.getsizeof(v3)} bytes") |
best practice
| 1 | import timeit |
| 2 | import sys |
| 3 | |
| 4 | class Regular: |
| 5 | def __init__(self, x, y): |
| 6 | self.x = x |
| 7 | self.y = y |
| 8 | |
| 9 | class Slotted: |
| 10 | __slots__ = ("x", "y") |
| 11 | def __init__(self, x, y): |
| 12 | self.x = x |
| 13 | self.y = y |
| 14 | |
| 15 | # Benchmark: instance creation |
| 16 | regular_time = timeit.timeit( |
| 17 | "Regular(1, 2)", |
| 18 | globals={"Regular": Regular}, |
| 19 | number=1_000_000, |
| 20 | ) |
| 21 | slotted_time = timeit.timeit( |
| 22 | "Slotted(1, 2)", |
| 23 | globals={"Slotted": Slotted}, |
| 24 | number=1_000_000, |
| 25 | ) |
| 26 | print(f"Create 1M instances:") |
| 27 | print(f" Regular: {regular_time:.3f}s") |
| 28 | print(f" Slotted: {slotted_time:.3f}s") |
| 29 | |
| 30 | # Benchmark: attribute access |
| 31 | r = Regular(1, 2) |
| 32 | s = Slotted(1, 2) |
| 33 | |
| 34 | regular_attr = timeit.timeit( |
| 35 | "r.x; r.y", |
| 36 | globals={"r": r}, |
| 37 | number=5_000_000, |
| 38 | ) |
| 39 | slotted_attr = timeit.timeit( |
| 40 | "s.x; s.y", |
| 41 | globals={"s": s}, |
| 42 | number=5_000_000, |
| 43 | ) |
| 44 | print(f"\n5M attribute accesses:") |
| 45 | print(f" Regular: {regular_attr:.3f}s") |
| 46 | print(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 |
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__.
| 1 | # Good use cases for __slots__: |
| 2 | |
| 3 | # 1. Data-heavy classes with many instances |
| 4 | class 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) |
| 12 | class 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 |
| 20 | class 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 |
| 30 | class 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 |