In standard Python, every class instance is essentially a wrapper around a hash table (the __dict__).
When you write:
class Point:
def __init__(self, x, y):
self.x = x
self.y = yCPython allocates the instance struct, and then allocates a separate, dynamic dictionary object to hold 'x' and 'y'. Dictionaries have significant memory overhead because they over-allocate buckets to maintain fast O(1) lookups and handle dynamic resizing. If you create 1,000,000 Point objects, you are creating 1,000,000 dictionaries.
When you define __slots__ = ('x', 'y'), CPython alters the memory layout of the instances.
- No Dict Pointer: The C struct for the instance (
PyObject) no longer has a pointer allocated for a__dict__. - Fixed Struct: The instance struct is allocated with exactly enough space for an array of pointers (one for
x, one fory). - Class-Level Descriptors: The binding happens on the class. CPython automatically generates
member_descriptorobjects and attaches them to the class dictionary for each slot.
class SlotPoint:
__slots__ = ('x', 'y')
print(type(SlotPoint.x)) # <class 'member_descriptor'>When you execute p.x on a slotted object:
- Python checks the instance
__dict__(it doesn't exist). - It falls back to the class
SlotPointand finds themember_descriptorforx. - The descriptor holds a memory offset integer.
- The descriptor executes C code that basically says: "Take the memory address of the instance
p, add the offset forx, and return the PyObject pointer located there."
__slots__ replaces dynamic hash-table lookups with hardcoded C-struct memory offset arithmetic. This is why it reduces memory overhead (no dicts) and slightly improves attribute access speed, but limits dynamic flexibility.