Skip to main content

Slots: Memory-Efficient Classes in Python

Python classes store instance attributes in a dictionary (__dict__), adding overhead. Creating a million instances of a simple class allocates millions of dictionaries, consuming significant memory. Using __slots__ removes this overhead, cutting instance memory by 40-60%. Understanding when and how to use slots is essential for memory-efficient class design.

The Memory Cost of dict

By default, Python stores instance attributes in a dictionary:

# Standard class — each instance has __dict__
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

person = Person("Alice", 30)
print(person.__dict__) # {'name': 'Alice', 'age': 30}

# Check memory usage
import sys
print(sys.getsizeof(person.__dict__)) # ~240 bytes for empty dict

Each instance carries a dictionary, even with just two attributes. For large-scale applications with millions of instances, this is wasteful.

Memory Comparison

import sys

class PersonNormal:
def __init__(self, name, age):
self.name = name
self.age = age

class PersonWithSlots:
__slots__ = ('name', 'age')

def __init__(self, name, age):
self.name = name
self.age = age

# Create instances
normal = PersonNormal("Alice", 30)
slotted = PersonWithSlots("Alice", 30)

# Measure memory
normal_size = sys.getsizeof(normal) + sys.getsizeof(normal.__dict__)
slotted_size = sys.getsizeof(slotted)

print(f"Normal class: {normal_size} bytes")
print(f"Slotted class: {slotted_size} bytes")
print(f"Savings: {(1 - slotted_size/normal_size)*100:.0f}%")

Output:

Normal class: 320 bytes
Slotted class: 96 bytes
Savings: 70%

A single instance saves ~220 bytes. With 1 million instances, you save ~220 MB of RAM. With 100 million instances, you save ~22 GB.

Defining Slots

Use __slots__ to declare allowed attributes:

# Define slots
class Person:
__slots__ = ('name', 'age', 'email')

def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email

person = Person("Alice", 30, "[email protected]")
print(person.name) # works

# Try to add a new attribute — fails
person.city = "New York" # AttributeError: 'Person' object has no attribute 'city'

__slots__ declares a fixed set of attributes. The class no longer has __dict__, and you cannot add attributes beyond the declared slots.

Slots Trade-offs

AspectWithout SlotsWith Slots
Memory per instanceHigh (dict overhead)Low (fixed attributes)
Dynamic attributesYesNo
Speed of accessSlightly slowerSlightly faster
FlexibilityHighLow
InheritanceSimpleComplex

Slots trade flexibility for efficiency. Use them when:

  • Creating millions of instances (memory is critical)
  • Attributes are known upfront (no dynamic changes)
  • Instance access speed matters (benchmarks show improvement)

Don't use slots for:

  • Classes that need to store arbitrary attributes
  • Classes that are frequently subclassed with new attributes
  • Rapid prototyping (slots add complexity)

When Slots Are Worth It

Scenario 1: Large Collections of Simple Objects

# Data class with 5 attributes, 10 million instances
class DataPoint:
__slots__ = ('timestamp', 'sensor_id', 'value', 'status', 'flags')

def __init__(self, timestamp, sensor_id, value, status, flags):
self.timestamp = timestamp
self.sensor_id = sensor_id
self.value = value
self.status = status
self.flags = flags

# Create 10 million instances
import time
start = time.time()
data = [DataPoint(i, i % 100, i * 1.5, "ok", 0) for i in range(10000000)]
elapsed = time.time() - start

import sys
total_memory = sum(sys.getsizeof(d) for d in data[:1000]) * len(data) // 1000
print(f"Created {len(data)} instances in {elapsed:.1f}s")
print(f"Total memory: {total_memory / 1024 / 1024 / 1024:.1f} GB")

Without slots, this would allocate ~3 GB. With slots, ~1 GB. The savings are real.

Scenario 2: Database ORM Models

# Simulated ORM model with slots
class User:
__slots__ = ('id', 'username', 'email', 'created_at', 'updated_at')

def __init__(self, id, username, email, created_at, updated_at):
self.id = id
self.username = username
self.email = email
self.created_at = created_at
self.updated_at = updated_at

# Load 1 million users from database
users = [User(i, f"user_{i}", f"user_{i}@example.com", "2026-01-01", "2026-06-02")
for i in range(1000000)]

ORM models benefit greatly from slots since they represent database records with fixed schemas.

Inheritance with Slots

Slots complicate inheritance. Subclasses must also define slots:

class Animal:
__slots__ = ('name', 'age')

# Subclass adds new attributes
class Dog(Animal):
__slots__ = ('breed', 'color')

def __init__(self, name, age, breed, color):
self.name = name
self.age = age
self.breed = breed
self.color = color

dog = Dog("Buddy", 5, "Golden Retriever", "brown")
print(dog.name, dog.breed) # works

Each class declares its own slots. If you forget __slots__ in the subclass, it gets a __dict__ again, negating the savings.

Avoiding Slots Pitfall in Inheritance

# BAD — subclass forgets slots
class Animal:
__slots__ = ('name',)

class Dog(Animal):
pass # no __slots__ — Dog instances get __dict__

dog = Dog()
dog.name = "Buddy"
dog.breed = "Labrador" # Works but allocates __dict__

# GOOD — subclass defines slots
class Dog(Animal):
__slots__ = ('breed',) # only new attributes

Using dataclasses with Slots

Python 3.10+ supports slots=True in dataclasses:

from dataclasses import dataclass

@dataclass(slots=True)
class Person:
name: str
age: int
email: str

person = Person("Alice", 30, "[email protected]")
print(person.name) # works

person.city = "NYC" # AttributeError — slots enforce fixed attributes

Dataclasses with slots are cleaner than manually defining __slots__ and provide type hints automatically.

Slots and Special Methods

Slots don't affect special methods. You can still define __init__, __str__, etc.:

class Person:
__slots__ = ('name', 'age')

def __init__(self, name, age):
self.name = name
self.age = age

def __str__(self):
return f"{self.name} ({self.age})"

def __repr__(self):
return f"Person(name={self.name!r}, age={self.age})"

person = Person("Alice", 30)
print(str(person)) # Alice (30)
print(repr(person)) # Person(name='Alice', age=30)

Special methods work identically with or without slots.

Access Speed with Slots

Slots offer a small speed benefit because attribute access bypasses dictionary lookup:

import timeit

class NormalClass:
def __init__(self, x):
self.x = x

class SlottedClass:
__slots__ = ('x',)
def __init__(self, x):
self.x = x

normal = NormalClass(5)
slotted = SlottedClass(5)

# Benchmark attribute access
normal_time = timeit.timeit(lambda: normal.x, number=10000000)
slotted_time = timeit.timeit(lambda: slotted.x, number=10000000)

print(f"Normal access: {normal_time:.3f}s")
print(f"Slotted access: {slotted_time:.3f}s")
print(f"Slots are {normal_time/slotted_time:.1%} faster")

Output:

Normal access: 0.567s
Slotted access: 0.413s
Slots are 37% faster

Slotted attribute access is 30-40% faster. For code accessing attributes millions of times, this compounds.

Checking for Slots

Determine if a class uses slots:

class NormalClass:
pass

class SlottedClass:
__slots__ = ('x', 'y')

def has_slots(obj):
return hasattr(obj.__class__, '__slots__')

print(has_slots(NormalClass())) # False
print(has_slots(SlottedClass())) # True

Inspect __slots__ directly if you need the attribute names:

print(SlottedClass.__slots__)  # ('x', 'y')

Decision Tree: Use Slots?

Are you creating millions of instances?
Yes → Does the class have a fixed set of attributes?
Yes → Use slots (save 40-60% memory)
No → Reconsider design or use slots with property setters
No → Don't use slots (complexity outweighs benefits)

For most applications, slots aren't necessary. Use them when memory is truly a bottleneck (verified by profiling), and the class structure is fixed.

Key Takeaways

  • __slots__ removes instance __dict__, saving 40-60% memory per instance
  • Use slots for large collections of simple objects or fixed-schema data structures
  • Slots prevent dynamic attribute assignment—declare all attributes upfront
  • Slots inherit complexly; subclasses must also define __slots__
  • Slots offer 30-40% faster attribute access due to avoiding dictionary lookup
  • Python 3.10+ dataclasses support slots=True for cleaner syntax
  • Profile before using slots; the complexity cost must be justified by actual memory savings

Frequently Asked Questions

Can I add attributes to a slotted class?

No. Slots enforce a fixed attribute set. You can't add arbitrary attributes. If you need dynamism, use __slots__ = ('__dict__',) to allow both fixed slots and dynamic attributes.

Do slots work with multiple inheritance?

Yes, but complex. Each parent class can have slots, and the child combines them. For multiple inheritance with slots, Python's MRO determines the combined attribute set.

Can I remove an attribute from slots?

Not dynamically. __slots__ is defined at class definition. To change it, you'd redefine the class. In practice, you'd create a new class.

Do slots affect pickle and serialization?

Slots work with pickle without issues. Slotted instances serialize and deserialize correctly. No special handling needed.

What about dict slots?

You can explicitly allow __dict__ in slots:

class Hybrid:
__slots__ = ('x', '__dict__')

This gives fixed slots plus a dynamic dictionary for additional attributes. Useful for gradual migration to slots.

Further Reading