Skip to main content

Python __slots__ Optimization

Learn when Python __slots__ reduces memory, how slot storage differs from __dict__, and the caveats for dataclasses and inheritance.

Where does the memory go?

Two classes hold the same coordinates. Why can one use less memory? Follow where Python stores their attributes, then see what changes when you add __slots__.

Python slots: a storage story

Step each scene independently. Diagrams explain storage; outputs replay recorded CPython results.

Recorded CPython 3.13.13 · 64-bit Linux. Python does not run in your browser.

Where the attributes live

Keep x and y the same. Change the class declaration, then inspect what the instance needs to store them.

Step 1 / 2
Start with a regular Point
hasattr(point, "__dict__")
True
slots_storage.py
class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

point = RegularPoint(1, 2)

A regular instance supports a dictionary interface for its attributes. The x and y fields refer to the values 1 and 2. Modern CPython can share attribute keys and use inline values; this diagram shows the logical storage, not a literal allocation map.

Fixed names, changeable values

Slots restrict which instance attributes exist. They do not make the values immutable.

Step 1 / 4
Update a declared attribute
print(point.x, point.y)
10 2
slots_assignment.py
point = SlottedPoint(1, 2)
point.x = 10
print(point.x, point.y)

The x field now refers to 10; y still refers to 2. A fixed layout allows normal assignment to declared fields.

When a small saving adds up

Compare retained allocations for equal populations. Every point refers to the same two pre-existing values in this controlled experiment.

Step 1 / 3
Recorded retained allocations
1 point
RegularPoint176 B
SlottedPoint136 B

Instances + holding list. Pre-existing values and class storage excluded. Bars start at zero and rescale for each population.

1 point
retained bytes saved
40 B
slots_population.py
for Point in (RegularPoint, SlottedPoint):
    points = [Point(x_value, y_value)
              for _ in range(1)]

Both captures include the instances and their holding list, after the same warm-up. Class storage and the two pre-existing values are excluded. The difference belongs to this interpreter and workload; it is not a universal size or speed claim.

A subclass can bring the dictionary back

Follow the same two fields into a child class. The child declaration determines whether dynamic instance attributes return.

Step 1 / 2
An ordinary child regains dynamic attributes
hasattr(point, "__dict__")
True
slots_inheritance.py
class PlainChild(SlottedPoint):
    pass

point = PlainChild(1, 2)
print(hasattr(point, "__dict__"))

The parent slots are still there, but this child also has an instance dictionary. Declaring slots only in the parent does not keep the whole hierarchy dictionary-free.

Allow observation without ownership

Weak references connect this layout choice back to garbage collection. A dictionary-free class needs explicit weak-reference support unless a parent provides it.

Step 1 / 2
The basic slotted Point cannot be weakly referenced
weakref.ref(point)
TypeError
slots_weakref.py
import weakref

point = SlottedPoint(1, 2)
weakref.ref(point)

The two declared fields do not include weak-reference support. This call raises TypeError; it does not create a weak reference.

How the memory measurement was captured

Retained tracemalloc allocation delta: instances plus their holding list, after a 64-instance warm-up and a collection before sampling. Two shared values and the class are allocated before tracing and excluded. Attribute dictionaries are not accessed during measurement. This is not process RSS or an attribute-speed benchmark.

Regenerate with python3 scripts/gen-slots-data.py. The values describe one controlled run; measure your actual classes and payloads before choosing a representation.

What the declaration changes

__slots__ = ("x", "y") declares instance field names when Python creates the class. Python installs a descriptor for each name on the class; that descriptor reads and writes the corresponding reference field in an instance. The values remain ordinary Python objects.

A regular instance offers a flexible __dict__ interface. Its physical storage depends on the CPython version: attribute keys can be shared between instances, and CPython 3.13 can keep values inline. The explorer therefore shows a logical layout, rather than claiming every instance owns a separate full hash table.

For a class whose parents provide neither feature, slots avoid automatically adding an instance dictionary and weak-reference support. You can request either explicitly. The trade-off is a fixed set of field names, with memory savings that depend on the interpreter, inheritance hierarchy and workload.

Measure the objects you actually create

The recorded experiment isolates storage overhead by sharing two pre-existing values. Your application may allocate a different value for every field. Measure that workload too:

python
import sys
import tracemalloc

class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedPoint:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

def build(cls, n=100_000):
    return [cls(i, i + 1) for i in range(n)]

for cls in (RegularPoint, SlottedPoint):
    tracemalloc.start()
    points = build(cls)
    current, peak = tracemalloc.get_traced_memory()
    print(cls.__name__, f"retained: {current / 1024 / 1024:.2f} MiB")
    print(f"peak: {peak / 1024 / 1024:.2f} MiB")
    print("instance size:", sys.getsizeof(points[0]))
    tracemalloc.stop()

Object size is only part of memory use

This example includes the list, instances and integers allocated while tracing. current reports retained traced allocations; peak also includes temporary allocations. Neither is process RSS. sys.getsizeof(instance) alone does not include every part of attribute storage or the objects referenced by the fields.

Declare the fields that belong to the class

python
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

This class now supports only the declared instance attributes. That is useful when the shape is part of the contract. It is wrong when callers are expected to attach extra fields for caching, instrumentation, or framework state.

Use a dataclass for a simple data container

python
from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: float
    y: float

For simple data containers, slots=True keeps the field declaration in one place and generates slot storage. It is available in Python 3.10 and later. Dataclass field defaults are handled by the decorator, so they do not have the conflict that manually declared slots have with same-named class defaults.

Keep dynamic attributes or weak references deliberately

python
class HybridPoint:
    __slots__ = ("x", "y", "__dict__")

    def __init__(self, x, y):
        self.x = x
        self.y = y

point = HybridPoint(1, 2)
point.label = "origin"  # stored in __dict__

Add __dict__ only when dynamic attributes are intentionally part of the API. Add __weakref__ when instances must work with weakref.ref(), weak caches, or observer registries.

For a dataclass on Python 3.11 or later, use @dataclass(slots=True, weakref_slot=True) when weak references are required. An inherited dictionary or weak-reference capability remains available even if the child declares slots.

Preserve the layout through inheritance

python
class BasePoint:
    __slots__ = ("x", "y")

class LabeledPoint(BasePoint):
    __slots__ = ("label",)

class Marker(BasePoint):
    __slots__ = ()

Slots declared in a parent are available to child instances. A child that adds fields should declare only the additional slot names. A child that adds no fields can use __slots__ = () to avoid bringing back an instance dictionary.

Know the limits

Slot names are not class defaults

python
class Wrong:
    __slots__ = ("x",)
    # x = 10  # would conflict with the slot descriptor

class Right:
    __slots__ = ("x",)

    def __init__(self, x=10):
        self.x = x

Multiple inheritance has layout limits

Only one parent class in a multiple-inheritance hierarchy can contribute non-empty slot storage. Other slotted bases need empty layouts such as __slots__ = ().

Slots are not packed numeric arrays

Slots store Python object references. For dense numeric data, arrays, NumPy, struct, or extension types may be a better representation.

The Python data model documents the slot and inheritance rules. The dataclasses reference describes the generated-layout options.

Choose the representation for the workload

SituationUse __slots__?Reason
Many small objects with fixed attributesYes, after measuringRemoves per-instance dictionary overhead
Objects need arbitrary attributesNoDynamic assignment is part of the behavior
Simple annotated data containerPrefer @dataclass(slots=True)Less duplication and fewer descriptor mistakes
Need weak referencesYes, with __weakref__Weak references are not automatic for slotted classes
Dense numeric vectorsUsually noSlots still store Python object references

If you found this explanation helpful, consider sharing it with others.

Mastodon