Python __slots__: Memory Optimization for Classes
Learn how __slots__ reduces memory usage in Python classes, when to use it, and the trade-offs involved.
What you'll learn
- ✓How __slots__ eliminates per-instance __dict__
- ✓Memory savings with real measurements
- ✓Inheritance and __slots__ interactions
- ✓When __slots__ is worth using and when it is not
Prerequisites
- •Python class fundamentals
- •Basic understanding of instance attributes
- •Familiarity with memory concepts
The Problem with Regular Classes
Every Python instance carries a __dict__ — a dictionary that stores its attributes. Dictionaries are flexible but memory-hungry. Each one allocates a hash table with room to grow, even if your class only has two attributes.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
For a class with just two float attributes, the __dict__ overhead can be larger than the actual data. When you create millions of instances, this adds up fast.
Enter slots
__slots__ tells Python exactly which attributes an instance will have. Python then uses a compact, fixed-size internal structure instead of a dictionary.
class SlottedPoint:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = SlottedPoint(1, 2)
# p.__dict__ # AttributeError: 'SlottedPoint' has no '__dict__'
print(p.x) # 1
With __slots__, each attribute is stored at a fixed offset in the instance, similar to a C struct. There is no hash table, no dynamic resizing, and no extra memory for unused dictionary capacity.
Measuring the Difference
Let us measure the actual memory savings:
import sys
class Regular:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
class Slotted:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
r = Regular(1, 2, 3)
s = Slotted(1, 2, 3)
print(f"Regular: {sys.getsizeof(r)} + {sys.getsizeof(r.__dict__)} bytes")
print(f"Slotted: {sys.getsizeof(s)} bytes")
Typical output on CPython 3.12:
Regular: 48 + 184 bytes
Slotted: 56 bytes
The regular instance uses about 232 bytes total. The slotted instance uses 56 bytes. That is roughly a 4x reduction.
For a more realistic benchmark with a million instances:
import tracemalloc
tracemalloc.start()
# Test with regular class
regular_list = [Regular(i, i+1, i+2) for i in range(1_000_000)]
snapshot = tracemalloc.take_snapshot()
regular_mem = sum(s.size for s in snapshot.statistics('filename'))
del regular_list
tracemalloc.clear_traces()
# Test with slotted class
slotted_list = [Slotted(i, i+1, i+2) for i in range(1_000_000)]
snapshot = tracemalloc.take_snapshot()
slotted_mem = sum(s.size for s in snapshot.statistics('filename'))
print(f"Regular: {regular_mem / 1e6:.1f} MB")
print(f"Slotted: {slotted_mem / 1e6:.1f} MB")
You will typically see 200+ MB for regular classes and around 70 MB for slotted ones.
Attribute Access Speed
Slots also make attribute access slightly faster because Python does not need to perform a dictionary lookup:
import timeit
r = Regular(1, 2, 3)
s = Slotted(1, 2, 3)
regular_time = timeit.timeit(lambda: r.x, number=10_000_000)
slotted_time = timeit.timeit(lambda: s.x, number=10_000_000)
print(f"Regular access: {regular_time:.3f}s")
print(f"Slotted access: {slotted_time:.3f}s")
The speed improvement is usually 10-20% for attribute access. Not dramatic, but it compounds in tight loops.
Slots and Inheritance
Inheritance with __slots__ requires care. Each class in the hierarchy should declare only its own new slots:
class Base:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
class Child(Base):
__slots__ = ('z',) # Only new attributes
def __init__(self, x, y, z):
super().__init__(x, y)
self.z = z
c = Child(1, 2, 3)
print(c.x, c.y, c.z) # 1 2 3
If Child redeclares x and y in its __slots__, it creates duplicate slot descriptors, wasting memory and potentially causing confusion.
Mixing Slotted and Non-Slotted Classes
If a parent class does not use __slots__, the child inherits __dict__, making its own __slots__ less effective:
class NoSlots:
pass
class WithSlots(NoSlots):
__slots__ = ('x',)
obj = WithSlots()
obj.x = 1 # Uses the slot
obj.y = 2 # Works! Uses __dict__ from NoSlots
print(obj.__dict__) # {'y': 2}
For maximum memory savings, the entire class hierarchy should use __slots__.
Including dict and weakref in Slots
If you need the flexibility of a dictionary alongside slots, explicitly include __dict__:
class Hybrid:
__slots__ = ('x', 'y', '__dict__')
def __init__(self, x, y, **kwargs):
self.x = x
self.y = y
for key, value in kwargs.items():
setattr(self, key, value)
h = Hybrid(1, 2, color="red", size=10)
print(h.x) # 1 (from slot)
print(h.color) # "red" (from __dict__)
Similarly, if you need weak references to slotted objects, include __weakref__:
class Weakrefable:
__slots__ = ('value', '__weakref__')
Common Pitfalls
1. Default Values on Slots
You cannot assign mutable default values directly to slots:
class Wrong:
__slots__ = ('items',)
items = [] # This creates a CLASS attribute, not an instance default
# Instead, set defaults in __init__
class Right:
__slots__ = ('items',)
def __init__(self, items=None):
self.items = items if items is not None else []
2. Pickling
Slotted objects need __getstate__ and __setstate__ for pickling:
import pickle
class Pickled:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def __getstate__(self):
return {slot: getattr(self, slot) for slot in self.__slots__}
def __setstate__(self, state):
for slot, value in state.items():
setattr(self, slot, value)
original = Pickled(1, 2)
restored = pickle.loads(pickle.dumps(original))
print(restored.x, restored.y) # 1 2
3. No Dynamic Attributes
You cannot add arbitrary attributes to slotted instances:
class Strict:
__slots__ = ('name',)
s = Strict()
s.name = "hello" # Fine
# s.age = 30 # AttributeError!
This is actually a feature — it catches typos in attribute names at assignment time.
When to Use slots
Use slots when:
- You create many instances (thousands or millions) of a class.
- The attributes are known and fixed.
- Memory is a concern (data processing pipelines, game entities, scientific computing).
Skip slots when:
- You have few instances.
- You need dynamic attributes.
- You are prototyping and flexibility matters more than performance.
- You are using multiple inheritance with non-slotted parent classes.
Dataclasses with Slots
Python 3.10 added slots=True to dataclasses, making this optimization trivial:
from dataclasses import dataclass
@dataclass(slots=True)
class Record:
name: str
value: float
count: int = 0
r = Record("test", 3.14)
print(r.name) # "test"
# r.extra = 1 # AttributeError
This is the cleanest way to combine dataclasses with slots.
Wrapping Up
__slots__ is a straightforward optimization that can dramatically reduce memory usage when you have many instances of a class. The trade-off is losing dynamic attribute assignment and needing extra care with inheritance and pickling. For data-heavy applications, especially combined with Python 3.10+ dataclasses, slots should be a standard part of your toolkit.
Related articles
- Python Python Concurrency: asyncio vs threading vs multiprocessing
Compare Python's concurrency models side by side. Learn when to use asyncio, threading, or multiprocessing with practical benchmarks and real-world examples.
- Python Python Profiling: Find and Fix Performance Bottlenecks
Learn how to profile Python code with cProfile, line_profiler, memory_profiler, and timeit to identify slow functions, memory leaks, and optimize runtime performance.
- Python The Python GIL Explained: What It Is and When It Matters
Understand the Global Interpreter Lock in CPython, why it exists, how it affects concurrency, and strategies to work around it.
- Python Python itertools Recipes: Practical Patterns for Efficient Iteration
Master Python's itertools module with practical recipes for grouping, windowing, chaining, and processing iterables efficiently.