Python's __slots__: Trading Flexibility for Memory
- By default, every instance carries its own __dict__, which is flexible but has real memory overhead per object.
- Declaring __slots__ = (...) tells Python to allocate fixed storage for named attributes instead, removing the per-instance dict.
- Once __slots__ is set, assigning to an attribute not listed in it raises AttributeError rather than silently succeeding.
- The saving only matters at scale: thousands or millions of instances, not a handful of long-lived objects.
Why a plain object carries a __dict__ by default
An ordinary Python object stores its instance attributes in a dictionary, accessible as obj.__dict__. That's what makes the language's dynamic attribute assignment work — you can set a new attribute on an instance at any point, and Python just adds a key to that dict:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
p.z = 3 # works fine, adds a new key
This flexibility has a cost: a dictionary needs more memory than a fixed-size structure would, both for the dict object itself and for the hash table it maintains internally. For a class you'll only ever create a handful of, that overhead is irrelevant. For a class you might instantiate by the million — parsed rows, graph nodes, particles in a simulation — it adds up fast.
Declaring __slots__ on a class
Setting __slots__ to a tuple of attribute names tells Python to allocate fixed, dict-free storage for exactly those attributes instead:
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 1 2
Each named slot gets a fixed, descriptor-based storage location on the object, comparable in spirit to a fixed-size C struct rather than a dictionary. No __dict__ is created for the instance at all unless you deliberately add "__dict__" to the slots list, which defeats most of the purpose.
What stops working once you add __slots__
The trade-off is that instances lose the ability to gain new attributes on the fly. Trying to set anything not listed in __slots__ raises an error immediately:
p = Point(1, 2)
p.z = 3
# AttributeError: 'Point' object has no attribute 'z'
This is often a feature rather than a nuisance — it catches typos in attribute names that would otherwise silently create a new attribute instead of raising an error. But it also means monkey-patching extra state onto instances from outside the class, a pattern some codebases lean on, no longer works without changing the class definition itself.
Measuring the actual memory saving
sys.getsizeof() makes the difference concrete, though it's worth checking on your own interpreter version since the exact numbers shift between releases:
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
a = WithDict(1, 2)
b = WithSlots(1, 2)
print(sys.getsizeof(a) + sys.getsizeof(a.__dict__)) # object + its dict
print(sys.getsizeof(b)) # no separate dict at all
The slotted version typically comes out at roughly half the total memory footprint per instance once the dict's own overhead is counted, though the exact ratio depends on the number of attributes and the Python version. Multiply that by a few million instances and it's the difference between a dataset fitting in memory and not.
__slots__ and inheritance
Every class in an inheritance chain needs its own __slots__ declaration for the saving to hold all the way through — if any class in the chain omits it, that class (and therefore all its subclasses) gets a __dict__ again by default, silently cancelling the benefit further down:
class Base:
__slots__ = ("a",)
class Derived(Base):
__slots__ = ("b",) # must also declare its own slots
d = Derived()
d.a = 1
d.b = 2
A subclass's __slots__ only needs to list the attributes it adds, not the ones it inherits — but leaving it off a class in the middle of a hierarchy reintroduces a __dict__ for that branch, which is an easy thing to miss in a larger codebase.
When the saving is actually worth it
Reach for __slots__ when a class is instantiated at real scale and the memory it uses actually matters to the program — large in-memory datasets, graph or tree nodes in the tens of thousands, objects created and discarded in a tight loop. For everyday classes with a handful of instances, the memory difference is immaterial and the loss of dynamic attributes is a real cost with nothing to show for it. Dataclasses can combine with __slots__ directly since Python 3.10 via @dataclass(slots=True), which is usually the easiest way to get the saving without writing the boilerplate by hand. The exact rules around slots and multiple inheritance are laid out in the Python data model reference.