__slots__

Trading flexibility for memory — when instances number in the millions

Why

By default, every Python instance carries a __dict__ — a full hash map just for that object's attributes. For small programs this is fine. For classes instantiated millions of times (graph nodes, feature vectors, event records in a stream processor), the per-instance dict overhead is significant: typically 200–400 bytes per object that you're paying for nothing.

__slots__ is one of the few Python optimisations worth knowing by name. It matters whenever memory-efficient data structures or high-throughput systems are involved.

Intuition

__slots__ tells Python: "this class will only ever have these exact attributes — no dynamic addition allowed." Python replaces the per-instance __dict__ with a fixed array of slots — like a C struct. No hash map, no rehashing, no wasted capacity.

The tradeoff: you gain memory and access speed, but lose the ability to add arbitrary attributes at runtime. It's a deliberate choice to lock down the class contract in exchange for efficiency.

Explanation
1. Defining and using __slots__

Declaring __slots__ replaces each instance's dynamic __dict__ with a fixed array of named slots — eliminating per-instance hash map overhead and dramatically reducing memory use at scale.

class Point: __slots__ = ("x", "y") # tuple or list of attribute names def __init__(self, x, y): self.x = x self.y = y p = Point(3, 4) p.x # 3 — normal access p.z = 5 # AttributeError — no __dict__, can't add new attributes # Memory comparison import sys class WithDict: def __init__(self, x, y): self.x = x; self.y = y class WithSlots: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x; self.y = y d = WithDict(1, 2) s = WithSlots(1, 2) sys.getsizeof(d) + sys.getsizeof(d.__dict__) # ~280 bytes sys.getsizeof(s) # ~56 bytes

The savings compound at scale: 1 million instances of WithSlots save roughly 200 MB vs WithDict. Attribute access is also slightly faster since Python uses fixed offsets rather than a hash lookup.

2. Slots with inheritance

Every class in the inheritance chain must declare __slots__ for the benefit to hold. One class without it re-introduces a __dict__ for all instances in the hierarchy.

class Base: __slots__ = ("x",) class Child(Base): __slots__ = ("y",) # only NEW slots — don't repeat "x" # Child instances have x (from Base) and y (from Child) # Both slot arrays are used — no __dict__ on either class ChildWithDict(Base): pass # no __slots__ — this child has __dict__ again! # The benefit is lost if ANY class in the MRO lacks __slots__

For __slots__ to work through an inheritance chain, every class in the hierarchy must define it. One missing __slots__ anywhere re-introduces a __dict__ on all instances.

3. Gotchas and limits
  • Cannot add attributes not listed in __slots__ — breaks code that uses instance.__dict__ directly.
  • Mixins and multiple inheritance get complicated when slots collide — prefer composition in those cases.
  • Weak references require "__weakref__" in the slots tuple explicitly.
  • dataclasses work with __slots__ via @dataclass(slots=True) (Python 3.10+) — the cleanest modern approach.
from dataclasses import dataclass @dataclass(slots=True) # Python 3.10+ class Coordinate: x: float y: float z: float = 0.0 # Gets __init__, __repr__, __eq__ AND __slots__ for free