__slots__ Optimization

8 min

Understanding Python's __slots__ for memory optimization and faster attribute access

Best viewed on desktop for optimal interactive experience

What is __slots__?

__slots__ is a class variable that can be used to explicitly declare which instance attributes you expect your class instances to have. When __slots__ is defined, Python uses a more memory-efficient internal structure for instances.

Interactive Visualization

Loading visualization...

Why Use __slots__?

1. Memory Efficiency

Without __slots__, Python stores instance attributes in a dictionary (__dict__). This is flexible but memory-intensive:

class RegularClass: def __init__(self, x, y): self.x = x self.y = y # Each instance has: # - Reference to class object (8 bytes) # - __dict__ dictionary (296+ bytes) # - __weakref__ (8 bytes if weak references are enabled) # - Actual attribute values

With __slots__:

class SlottedClass: __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y # Each instance has: # - Reference to class object (8 bytes) # - Direct slots for x and y (16 bytes for two references) # - No __dict__, no __weakref__ (unless explicitly added)

2. Faster Attribute Access

  • Dictionary lookup: O(1) average, but involves hash computation
  • Slot access: Direct memory offset, no hash computation needed

3. Attribute Access Control

__slots__ prevents creation of new attributes at runtime:

class Restricted: __slots__ = ['x', 'y'] obj = Restricted() obj.x = 10 # OK obj.z = 20 # AttributeError: 'Restricted' object has no attribute 'z'

How __slots__ Works Internally

Memory Layout

Without __slots__: Instance → PyObject Header → Type Pointer → Class Object → __dict__ → HashMap { 'x': value1, 'y': value2, ... } → __weakref__ With __slots__: Instance → PyObject Header → Type Pointer → Class Object → Slot 0: x value (direct) → Slot 1: y value (direct)

Descriptor Protocol

Each slot becomes a descriptor on the class:

class Point: __slots__ = ['x', 'y'] # Python creates: # Point.x = member_descriptor('x') # Point.y = member_descriptor('y') # These descriptors handle attribute access: print(type(Point.x)) # <class 'member_descriptor'>

Implementation Details

PyMemberDef Structure

In CPython, slots are implemented using the PyMemberDef structure:

typedef struct PyMemberDef { const char *name; // Attribute name int type; // Type code (T_OBJECT, T_INT, etc.) Py_ssize_t offset; // Offset in the instance structure int flags; // READONLY, etc. const char *doc; // Docstring } PyMemberDef;

Slot Allocation

  1. Parse __slots__: During class creation
  2. Calculate offsets: Determine memory layout
  3. Create descriptors: One for each slot
  4. Allocate instances: Fixed size based on slots

Best Practices

1. When to Use __slots__

Good use cases:

  • Classes with many instances (thousands+)
  • Fixed set of attributes known at design time
  • Performance-critical code
  • Preventing attribute typos

Avoid when:

  • Need dynamic attributes
  • Using metaclasses or multiple inheritance
  • Need __weakref__ support (unless explicitly added)
  • Pickling complex hierarchies

2. Inheritance Considerations

# Parent with __slots__ class Parent: __slots__ = ['x'] # Child must also define __slots__ class Child(Parent): __slots__ = ['y'] # Child has both x and y # Without __slots__ in child, __dict__ is re-added class ChildWithDict(Parent): pass # Has __dict__ again!

3. Common Patterns

# Include __dict__ for flexibility class Hybrid: __slots__ = ['x', 'y', '__dict__'] # x, y are fast; other attributes go in __dict__ # Include __weakref__ for weak references class WeakRefCapable: __slots__ = ['data', '__weakref__'] # Empty slots for abstract base class AbstractBase: __slots__ = () # No overhead in base class

Performance Comparison

Memory Usage

import sys class Regular: def __init__(self): self.a = 1 self.b = 2 self.c = 3 class Slotted: __slots__ = ['a', 'b', 'c'] def __init__(self): self.a = 1 self.b = 2 self.c = 3 # Memory comparison regular = Regular() slotted = Slotted() print(sys.getsizeof(regular.__dict__)) # ~296 bytes # slotted has no __dict__ print(sys.getsizeof(slotted)) # ~64 bytes

Access Speed

import timeit # Attribute access is ~10-20% faster with __slots__ regular_time = timeit.timeit('obj.x', setup='class R: pass\nobj = R()\nobj.x = 1', number=10000000) slotted_time = timeit.timeit('obj.x', setup='class S:\n __slots__ = ["x"]\nobj = S()\nobj.x = 1', number=10000000) print(f"Speedup: {regular_time / slotted_time:.2f}x")

Advanced Topics

1. Slots with Properties

class Temperature: __slots__ = ['_celsius'] @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): self._celsius = value @property def fahrenheit(self): return self._celsius * 9/5 + 32

2. Slots with Dataclasses

from dataclasses import dataclass @dataclass class Point: __slots__ = ['x', 'y'] x: float y: float

3. Dynamic Slot Creation

def create_slotted_class(name, attributes): """Dynamically create a slotted class""" return type(name, (), { '__slots__': attributes, '__init__': lambda self, **kwargs: [ setattr(self, k, v) for k, v in kwargs.items() ] }) Vector = create_slotted_class('Vector', ['x', 'y', 'z'])

Common Pitfalls

1. Iteration Over Slots

class Point: __slots__ = ['x', 'y'] def __iter__(self): # Wrong: __slots__ is on the class, not instance # return iter(self.__slots__) # Correct: Yield actual values for slot in self.__class__.__slots__: yield getattr(self, slot)

2. Default Values

# Wrong: Can't set class attributes with __slots__ class Wrong: __slots__ = ['x'] x = 10 # This becomes a class variable, not default # Correct: Use __init__ or descriptors class Correct: __slots__ = ['x'] def __init__(self, x=10): self.x = x

3. Multiple Inheritance

# Can't inherit from multiple classes with non-empty __slots__ class A: __slots__ = ['x'] class B: __slots__ = ['y'] # This raises TypeError # class C(A, B): # pass # Solution: Use empty __slots__ in base classes class Base: __slots__ = () class A(Base): __slots__ = ['x'] class B(Base): __slots__ = ['y']

Real-World Examples

1. NumPy-like Array Element

class ArrayElement: __slots__ = ['value', 'dtype', '_shape'] def __init__(self, value, dtype='float64'): self.value = value self.dtype = dtype self._shape = ()

2. Game Entity

class GameObject: __slots__ = ['x', 'y', 'health', 'sprite', '_id'] _next_id = 0 def __init__(self, x, y): self.x = x self.y = y self.health = 100 self.sprite = None self._id = GameObject._next_id GameObject._next_id += 1

3. Cache Entry

class CacheEntry: __slots__ = ['key', 'value', 'timestamp', 'hits'] def __init__(self, key, value): self.key = key self.value = value self.timestamp = time.time() self.hits = 0

Performance Tips

  1. Measure First: Profile memory usage before optimizing
  2. Batch Creation: Create many instances to see benefits
  3. Combine with Other Optimizations: Use with __init__ optimization, caching
  4. Consider Alternatives: NumPy arrays, struct, or C extensions for extreme cases

Summary

__slots__ is a powerful optimization technique that:

  • Reduces memory usage by 40-50% for simple objects
  • Speeds up attribute access by 10-20%
  • Prevents typos by restricting attribute names
  • Has trade-offs in flexibility and compatibility

Use __slots__ when you have many instances of a class with a fixed set of attributes, and memory or performance is a concern.

If you found this explanation helpful, consider sharing it with others.

Mastodon