Trading flexibility for memory — when instances number in the millions
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.
__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.
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.
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.
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.
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.
__slots__ — breaks code that uses instance.__dict__ directly."__weakref__" in the slots tuple explicitly.__slots__ via @dataclass(slots=True) (Python 3.10+) — the cleanest modern approach.