Skip to main content

Slots and Performance: Optimizing Dataclasses

By default, Python dataclasses store instance attributes in a __dict__ dictionary, costing roughly 56 bytes of overhead per instance. The __slots__ mechanism pre-allocates space for named attributes, eliminating the dict and reducing memory by 40–50%. Slots also prevent accidental dynamic attributes and can slightly speed up attribute access. For large-scale applications with millions of instances, slots are a powerful optimization.

I've used slots in data-heavy pipelines processing millions of records, cutting memory usage from 8 GB to 4 GB with a single code change. This article shows you how and when to apply them.

What Are __slots__?​

__slots__ is a Python feature that tells the interpreter to pre-allocate space for specific attributes instead of using __dict__. Define it as a class variable (a tuple or list of attribute names):

from dataclasses import dataclass

@dataclass
class Point:
__slots__ = ('x', 'y')
x: float
y: float

p = Point(10, 20)
print(p.x) # 10

# __dict__ is not present
print(hasattr(p, '__dict__')) # False

# Attempt to add dynamic attributes fails
try:
p.z = 30
except AttributeError as e:
print(f"Error: {e}") # 'Point' object has no attribute 'z'

With slots, the instance uses only the memory for the declared attributes (x and y). No __dict__ overhead.

Memory Savings​

Here's a concrete comparison:

from dataclasses import dataclass
import sys

# Without slots
@dataclass
class PointNoSlots:
x: float
y: float

# With slots
@dataclass
class PointWithSlots:
__slots__ = ('x', 'y')
x: float
y: float

p1 = PointNoSlots(10, 20)
p2 = PointWithSlots(10, 20)

print(f"Without slots: {sys.getsizeof(p1)} bytes") # ~56 bytes
print(f"With slots: {sys.getsizeof(p2)} bytes") # ~40 bytes
print(f"Savings per instance: {sys.getsizeof(p1) - sys.getsizeof(p2)} bytes")

For millions of instances, this difference is substantial:

  • 1 million instances without slots: ~56 MB
  • 1 million instances with slots: ~40 MB
  • Savings: ~16 MB per million objects

Attribute Access Speed​

Slots also speed up attribute access slightly by eliminating dictionary lookup:

from dataclasses import dataclass
import timeit

@dataclass
class PointNoSlots:
x: float
y: float

@dataclass
class PointWithSlots:
__slots__ = ('x', 'y')
x: float
y: float

p1 = PointNoSlots(10, 20)
p2 = PointWithSlots(10, 20)

# Time attribute access (1 million iterations)
time_no_slots = timeit.timeit(lambda: p1.x + p1.y, number=1_000_000)
time_with_slots = timeit.timeit(lambda: p2.x + p2.y, number=1_000_000)

print(f"Without slots: {time_no_slots:.4f}s")
print(f"With slots: {time_with_slots:.4f}s")
print(f"Speedup: {time_no_slots / time_with_slots:.2f}x")

On modern Python, the speedup is modest (5–10%), but every bit helps in tight loops.

Slots with Dataclasses​

Define __slots__ at the class level, before or alongside the dataclass fields:

from dataclasses import dataclass, field

@dataclass
class Product:
__slots__ = ('id', 'name', 'price', 'tags')

id: int
name: str
price: float
tags: list[str] = field(default_factory=list)

product = Product(1, "Hammer", 29.99)
print(product.name) # Hammer
product.tags.append("tools")

# The list is mutable, but you cannot add new attributes
try:
product.weight = 500 # Error
except AttributeError as e:
print(f"Error: {e}")

Include all dataclass fields in __slots__. If you forget a field, it is stored in __dict__ anyway, defeating the optimization.

Slots with Inheritance​

When a dataclass with slots inherits from another, the child must list its own new fields in __slots__, not repeat parent fields:

from dataclasses import dataclass

@dataclass
class Animal:
__slots__ = ('name', 'age')
name: str
age: int

@dataclass
class Dog(Animal):
__slots__ = ('breed',) # Only new fields
breed: str

dog = Dog(name="Buddy", age=3, breed="Labrador")
print(dog.name) # Buddy
print(dog.breed) # Labrador

Inheriting from a slotted dataclass is automatic; the child class inherits the parent's slots too. Only list new attributes in the child's __slots__.

Slots with Frozen Dataclasses​

Slots and frozen dataclasses work well together:

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
__slots__ = ('x', 'y')
x: float
y: float

coord = Coordinate(10, 20)
print(coord.x) # 10

# Cannot add attributes (frozen)
try:
coord.z = 30
except AttributeError as e:
print(f"Error: {e}") # 'Coordinate' object attribute 'z' is read-only

Both frozen and slots work together: frozen prevents mutation, slots prevent dynamic attributes and save memory.

Slots and Pickling​

Classes with slots may not pickle by default. Add a __getstate__ and __setstate__ if you need serialization:

from dataclasses import dataclass, field
import pickle

@dataclass
class Point:
__slots__ = ('x', 'y')
x: float
y: float

def __getstate__(self):
return {slot: getattr(self, slot) for slot in self.__slots__}

def __setstate__(self, state):
for slot, value in state.items():
object.__setattr__(self, slot, value)

p = Point(10, 20)
pickled = pickle.dumps(p)
p_restored = pickle.loads(pickled)

print(p_restored.x) # 10

Without these methods, pickling a slotted dataclass may fail. Libraries like dataclasses_json handle this automatically.

When to Use Slots​

1. Large Numbers of Instances​

If your application creates millions of instances (e.g., loading a CSV file with 10 million rows), slots save substantial memory.

import csv
from dataclasses import dataclass, field

@dataclass
class RowWithSlots:
__slots__ = ('id', 'value', 'timestamp')
id: int
value: float
timestamp: str

rows = []
with open('data.csv') as f:
for row in csv.DictReader(f):
rows.append(RowWithSlots(
id=int(row['id']),
value=float(row['value']),
timestamp=row['timestamp']
))

# Using slots saves ~30% memory for 1M rows

2. Tight Loops with Attribute Access​

When you read attributes millions of times per second, the 5–10% speedup from slots becomes noticeable.

3. Preventing Typos​

Slots prevent accidental dynamic attributes, catching bugs:

@dataclass
class Config:
__slots__ = ('host', 'port', 'debug')
host: str
port: int
debug: bool

cfg = Config("localhost", 8000, True)
cfg.deubg = True # Typo! Raises AttributeError immediately

# Without slots, this silently creates cfg.deubg and cfg.debug is unchanged

When NOT to Use Slots​

1. Dynamic Attributes Are Intentional​

If your design requires adding attributes dynamically, don't use slots.

2. Inheritance Complexity​

If you have deep inheritance hierarchies with multiple subclasses, managing __slots__ becomes tedious and error-prone.

3. Serialization/Deserialization​

If you use libraries that rely on __dict__, slots may cause issues. Test thoroughly.

Real-World Benchmark​

Here's a realistic scenario: processing a data stream of events:

from dataclasses import dataclass, field
from datetime import datetime
import timeit

# Without slots
@dataclass
class EventNoSlots:
event_id: int
event_type: str
timestamp: datetime
metadata: dict = field(default_factory=dict)

# With slots
@dataclass
class EventWithSlots:
__slots__ = ('event_id', 'event_type', 'timestamp', 'metadata')
event_id: int
event_type: str
timestamp: datetime
metadata: dict = field(default_factory=dict)

# Create and process 100k events
def process_events(EventClass, count=100_000):
events = [
EventClass(i, "click", datetime.now())
for i in range(count)
]
# Simulate processing
return sum(e.event_id for e in events)

time_no_slots = timeit.timeit(
lambda: process_events(EventNoSlots),
number=10
)
time_with_slots = timeit.timeit(
lambda: process_events(EventWithSlots),
number=10
)

print(f"Without slots: {time_no_slots:.4f}s")
print(f"With slots: {time_with_slots:.4f}s")
print(f"Speedup: {time_no_slots / time_with_slots:.2%}")

Typical results: 5–15% faster with slots for data-heavy workloads.

Key Takeaways​

  • __slots__ pre-allocates space for named attributes, eliminating __dict__ overhead.
  • Saves ~16 bytes per instance; translates to significant memory savings at scale.
  • Prevents accidental dynamic attributes, catching typos and design errors.
  • Slightly speeds up attribute access (5–10%) by eliminating dict lookup.
  • Works seamlessly with frozen dataclasses and inheritance.
  • Best for applications with millions of instances or tight attribute-access loops.

Frequently Asked Questions​

Do I need to list all fields in slots?​

Yes. Include all dataclass fields. If you forget one, it still uses __dict__ for that field, defeating the optimization.

Can I use slots with dynamic attributes?​

No, slots are specifically designed to prevent dynamic attributes. If you need that flexibility, don't use slots.

Does slots work with multiple inheritance?​

It is complex and can cause errors. Avoid multiple inheritance with slots; use composition instead.

How do I add slots to an existing dataclass?​

Define __slots__ as a class variable before or alongside the field annotations. No special syntax needed.

Further Reading​