Skip to content
Codeloom
Python

Python __slots__: Memory Optimization for Classes

Learn how __slots__ reduces memory usage in Python classes, when to use it, and the trade-offs involved.

·6 min read · By Codeloom
Intermediate 10 min read

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.