Introduction
Every Python object stores its attributes in a __dict__ dictionary by default, adding significant per-instance overhead. The __slots__ mechanism replaces this dictionary with a fixed-size struct, drastically reducing memory for classes with many instances.
Key Concepts
__slots__: A class-level declaration that tells Python to allocate space for a fixed set of attributes without creating a per-instance__dict__.__dict__overhead: The default attribute dictionary costs roughly 100+ bytes per instance (the dict object itself plus its internal hash table).- Descriptor-based access: Slotted attributes are implemented as data descriptors on the class, providing direct memory offsets instead of hash lookups.
dataclass(slots=True): Python 3.10+ shorthand that automatically generates__slots__for dataclass fields.
Real World Context
When loading millions of records into memory (e.g., a geospatial dataset of city coordinates, an in-memory graph of user nodes, or a particle simulation), reducing per-object overhead from ~160 bytes to ~56 bytes can mean the difference between 1.5 GB and 500 MB of RAM.
Deep Dive
The standard Python object stores attributes in a per-instance dictionary. We can measure the cost directly:
pythonimport sys class WithDict: def __init__(self, x, y, z): self.x = x self.y = y self.z = z obj = WithDict(1, 2, 3) print(sys.getsizeof(obj)) # ~48 bytes (object shell) print(sys.getsizeof(obj.__dict__)) # ~104 bytes (attribute dict) # Total: ~152 bytes per instance
The object shell is small, but each instance carries its own dictionary. Now compare with __slots__:
pythonclass WithSlots: __slots__ = ('x', 'y', 'z') def __init__(self, x, y, z): self.x = x self.y = y self.z = z obj = WithSlots(1, 2, 3) print(sys.getsizeof(obj)) # ~64 bytes total, no __dict__
Because there is no __dict__, you cannot add attributes that are not declared in __slots__:
pythonp = WithSlots(1, 2, 3) p.w = 4 # Raises AttributeError: 'WithSlots' object has no attribute 'w'
This restriction is the tradeoff: you lose dynamic attribute assignment in exchange for lower memory and slightly faster access.
Inheritance Rules
When subclassing a slotted class, declare only the new attributes in the child:
pythonclass Base: __slots__ = ('a', 'b') class Derived(Base): __slots__ = ('c',) # Only new attributes; 'a' and 'b' are inherited # Repeating parent slots creates redundant descriptors and wastes memory: class Bad(Base): __slots__ = ('a', 'b', 'c') # 'a' and 'b' are duplicated!
If any class in the hierarchy does not define __slots__, instances will still get a __dict__ from that class, negating the memory savings.
Modern Approach: dataclass(slots=True)
Python 3.10+ makes slotted classes trivial:
pythonfrom dataclasses import dataclass @dataclass(slots=True) class Point: x: float y: float z: float p = Point(1.0, 2.0, 3.0) print(sys.getsizeof(p)) # ~64 bytes, same savings
Common Pitfalls
- Forgetting that
__slots__prevents__dict__: Code that relies onvars(obj)orobj.__dict__will break. If you need both slots and a dict, include'__dict__'in__slots__, but this defeats much of the purpose. - Duplicating parent slots in child classes: Repeating a parent's slot name in a subclass creates a redundant descriptor. Always declare only new attributes.
- Assuming slots speed up attribute access significantly: The primary benefit is memory reduction. Access speed improvement is marginal (5-10%) and should not be the sole motivation.
Best Practices
- Use
__slots__when you will create thousands or millions of instances with a fixed set of attributes. - Prefer
@dataclass(slots=True)for new code in Python 3.10+ to avoid manual__slots__declarations. - Always measure with
sys.getsizeof()ortracemallocto confirm actual savings in your specific use case.
Summary
__slots__replaces the per-instance__dict__with fixed-offset attribute storage, saving ~100 bytes per object.- Slotted classes cannot accept dynamic attributes unless
'__dict__'is explicitly included in__slots__. - In inheritance hierarchies, only declare new attributes in child
__slots__to avoid redundant descriptors. @dataclass(slots=True)is the modern, clean way to create slotted classes in Python 3.10+.
Code Examples
import sys
class WithDict:
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
class WithSlots:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x, self.y, self.z = x, y, z
dict_obj = WithDict(1, 2, 3)
slot_obj = WithSlots(1, 2, 3)
dict_total = sys.getsizeof(dict_obj) + sys.getsizeof(dict_obj.__dict__)
slot_total = sys.getsizeof(slot_obj)
print(f"WithDict total: {dict_total} bytes")
print(f"WithSlots total: {slot_total} bytes")
print(f"Savings per obj: {dict_total - slot_total} bytes")
print(f"For 1M objects: {(dict_total - slot_total) * 1_000_000 / 1024 / 1024:.1f} MB saved")from dataclasses import dataclass
import sys
@dataclass(slots=True)
class Point:
x: float
y: float
z: float
p = Point(1.0, 2.0, 3.0)
print(f"Slotted dataclass size: {sys.getsizeof(p)} bytes")
print(f"Has __dict__: {hasattr(p, '__dict__')}")
print(f"Has __slots__: {hasattr(p, '__slots__')}")
print(f"Slots: {p.__slots__}")