__slots__ Optimization
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
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
- Parse
__slots__
: During class creation - Calculate offsets: Determine memory layout
- Create descriptors: One for each slot
- 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
- Measure First: Profile memory usage before optimizing
- Batch Creation: Create many instances to see benefits
- Combine with Other Optimizations: Use with
__init__
optimization, caching - 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.