While descriptors provide a powerful mechanism for managing attribute access, Python classes inherently rely on a dictionary (__dict__) to store instance attributes. This offers tremendous flexibility but comes with a memory cost: every instance must allocate a dictionary, which can be significant when creating many small objects. The __slots__ class variable offers a high-performance alternative by fundamentally changing how instances store their data.

How slots Replaces dict

When you define __slots__ in a class, you are instructing the Python interpreter to create a fixed set of names (slots) for attributes on each instance. Instead of a dynamic dictionary, the interpreter reserves space for a small, fixed-length array (a C-style struct) within each instance to hold the values for these predefined attributes. This transformation happens deep within the object creation machinery.

class PointWithSlots:
    __slots__ = ('x', 'y')  # Defines the only two allowed attributes

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = PointWithSlots(5, 10)
print(p.x)  # Output: 5
print(p.y)  # Output: 10

# This will raise an AttributeError because 'z' is not in __slots__
try:
    p.z = 15
except AttributeError as e:
    print(e)  # Output: 'PointWithSlots' object has no attribute 'z'

# The instance no longer has a __dict__ attribute
print(hasattr(p, '__dict__'))  # Output: False

The Memory and Performance Advantages

The primary motivation for __slots__ is efficiency. A __dict__ has significant overhead: it is a hash table that must be allocated and, even when empty, consumes a non-trivial amount of memory (typically several hundred bytes on a 64-bit platform). In contrast, an instance with __slots__ only stores its attributes as direct references in the pre-allocated array. This can lead to dramatic memory savings when creating large numbers of instances. For example, a class holding two integers might see its instance size reduced from over 200 bytes to just 64 bytes.

This memory efficiency also translates into speed. Attribute access is faster because it involves a simple lookup in a fixed-size array at a known offset, bypassing the hash table lookup required for __dict__. This performance boost is most noticeable in tight loops.

Interaction with Descriptors and Inheritance

The mechanism behind __slots__ is implemented using descriptors. When you define __slots__ = ('x', 'y'), the class automatically creates a descriptor for each named slot. These descriptors are not the full-featured descriptors we write ourselves but lean, internal “member descriptors” that manage the storage and retrieval of values from the instance’s fixed array.

Inheritance with __slots__ requires careful consideration. If a subclass does not define its own __slots__, it will inherit the parent’s __slots__ but will also gain a __dict__, allowing for the dynamic assignment of new attributes and negating some of the memory benefits. If a subclass defines its own __slots__, it should include the slots from all parent classes. The interpreter handles this by merging the slots, and the instance reserves space for all of them.

class Base:
    __slots__ = ('base_attr',)

class Derived(Base):
    __slots__ = ('derived_attr',)  # Effectively __slots__ = ('base_attr', 'derived_attr')

d = Derived()
d.base_attr = 'value'
d.derived_attr = 'other_value'

# This will fail because 'new_attr' is not in the merged slots
try:
    d.new_attr = 'fail'
except AttributeError as e:
    print(e)

Common Pitfalls and Best Practices

  1. Incompatibility with Weak References: By default, classes using __slots__ do not support weak references. To enable them, you must explicitly include '__weakref__' in your __slots__ declaration.

  2. Default Values: You cannot provide default values for slotted attributes by assigning them at the class level. This assignment would create a class attribute, which is shadowed by the instance slot descriptor. Defaults must be set in the __init__ method.

    class BadExample:
        __slots__ = ('name',)
        name = "default"  # This is a class attribute, not an instance default.
    
    b = BadExample()
    # The instance starts out without its 'name' attribute defined.
    print(b.name)  # This actually prints the class attribute: "default"
    b.name = "instance"  # Now the instance slot is populated.
    print(b.name)  # Output: "instance"
    del b.name
    print(b.name)  # Falls back to the class attribute: "default"
    
  3. Tooling and Library Interoperability: Many libraries (e.g., ORMs like SQLAlchemy, serialization frameworks) rely on __dict__ to introspect and manipulate objects. Using __slots__ can break this functionality unless the library has explicit support for it.

  4. When to Use: Reserve __slots__ for classes that are instantiated abundantly (thousands to millions of times) and where memory profiling has confirmed a bottleneck. It is an optimization tool, not a default coding practice. For most general-purpose classes, the flexibility of __dict__ outweighs the performance gains.