Skip to content
← All Python articles

Python explainer

Save Memory with slots=True

About 4 min to read. Allow extra time to work through examples.

Make each small record take less memory with slots=True, measure the saving yourself, and check what a slotted class gives up before you switch.

This article uses Python. You can make it your practice mode to browse related content across the site.

Your program keeps a million sensor readings in memory. Each one is a small dataclass with two fields. A plain Python object stores its attributes in its own dictionary, __dict__, so you can add any attribute at any time. Across a million objects, that storage costs tens of megabytes.

from dataclasses import dataclass


@dataclass
class Reading:
    sensor: int
    value: float


r = Reading(1, 0.5)
r.note = "checked"  # adds a third attribute
print(r.__dict__)   # {'sensor': 1, 'value': 0.5, 'note': 'checked'}

mypy reports the r.note line, but Python runs it.

Store the fields in fixed slots

With slots=True, each object gets a fixed slot for each field and no __dict__. A slot is room for one named attribute, reserved inside the object itself. The decorator builds the class with __slots__, the class attribute that tells Python which attributes the objects have room for.

from dataclasses import dataclass


@dataclass(slots=True)
class SlimReading:
    sensor: int
    value: float


r = SlimReading(1, 0.5)
print(SlimReading.__slots__)  # ('sensor', 'value')
r.value = 0.7                 # fields can still change
r.note = "checked"            # raises AttributeError

The object now has room for its two fields and nothing else. That makes it smaller. It also makes a typo such as r.vaule = 0.7 raise AttributeError instead of adding a new attribute. Slots do not freeze the fields, so add frozen=True as well if they should not change.

Measure the saving

Measure your own records before you switch. tracemalloc counts the memory Python allocates while tracing is on:

import tracemalloc
from dataclasses import dataclass


@dataclass
class Reading:
    sensor: int
    value: float


@dataclass(slots=True)
class SlimReading:
    sensor: int
    value: float


def bytes_per_record(cls: type[Reading] | type[SlimReading]) -> float:
    tracemalloc.start()
    records = [cls(1, 0.5) for _ in range(100_000)]
    size, _peak = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    return size / len(records)


print(f"Reading:     {bytes_per_record(Reading):.0f} bytes")
print(f"SlimReading: {bytes_per_record(SlimReading):.0f} bytes")

On Python 3.12 this prints 88 bytes for Reading and 56 for SlimReading. On 3.14 and 3.15 a Reading takes 96 bytes. For a million records, that is 32 to 40 MB saved. If each record also held a long string, the string would take most of the memory and the saving would matter less.

Check your understanding Why measure with tracemalloc instead of comparing sys.getsizeof(r)? Show answer

sys.getsizeof returns 48 for both a Reading and a SlimReading on Python 3.12 to 3.15. It counts only the object's own block of memory, and a plain object keeps its attribute storage outside that block. tracemalloc counts every allocation, so it shows the difference.

Check what the class gives up

A slotted class behaves differently in a few ways. Check each one before you add slots=True:

  • Code that sets an attribute that is not a field, or reads self.__dict__, raises AttributeError.
  • cached_property stores its result in __dict__, so reading one raises TypeError. Use a plain property, or a field you fill in yourself.
  • weakref.ref(r) raises TypeError, because a weak reference also needs a slot. Add weakref_slot=True next to slots=True if code needs weak references.
  • A subclass that is not slotted gets a __dict__ again, and so does a slotted class whose base is a plain class. Give every class in the hierarchy slots=True.
  • Python reads __slots__ only when it creates a class, so the decorator cannot add slots to your class. It builds a second class and returns that one. Code that captured the first class during creation still points at the old one.

The next exercise, Check a Class Before You Add slots=True, looks for these problems in code.

Check your understanding You add slots=True to a base dataclass Shape, but class Circle(Shape) is a plain subclass. Do Circle objects save memory? Show answer

Less than you expect. Circle has no __slots__ of its own, so Python gives its objects a __dict__ again. That takes back some or all of the saving, depending on the Python version. Decorate Circle with @dataclass(slots=True) too.

Going further

  • You can write __slots__ = ("sensor", "value") by hand in an ordinary class. slots=True writes it from the fields for you.
  • Read __slots__ to see a class's storage, not its fields. Since Python 3.11 it leaves out names a base class already has a slot for. Call dataclasses.fields() for the field list.

See Python's slots reference, the dataclass options and the tracemalloc documentation.

step 13 of 28

Modelling Data with Dataclasses

Up next Check a Class Before You Add slots=True