Your service keeps a few million small records in memory, and you want to
know whether slots=True is worth the change. T3's article
Save Memory with slots=True
explains what slots do and what a slotted class gives up. This article is
about the measurement. A percentage from someone else's benchmark does not
tell you what your records on your Python will save.
Name what you are measuring
Each question has its own tool, and one answer can change while another stays the same:
| Question | Tool |
|---|---|
| How big is this one object, without what it points at? | sys.getsizeof, which gives the shallow size |
| How much memory do these records hold while they are alive? | The change in tracemalloc's current traced memory |
| How high does memory get while you build them? | tracemalloc's peak |
| How much RAM does the process use? | The operating system's RSS |
| How long does it take to build or read a record? | A separate timing run, with tracing off |
Compare two batches of records
This script builds 100,000 records of each class and measures the traced memory they hold:
import gc
import platform
import tracemalloc
from dataclasses import dataclass
@dataclass
class PlainPoint:
x: int
y: int
@dataclass(slots=True)
class SlottedPoint:
x: int
y: int
def measure(factory: type[PlainPoint] | type[SlottedPoint],
count: int) -> tuple[int, int]:
if count <= 0:
raise ValueError("count must be positive")
records: list[object] = [None] * count
gc.collect()
tracemalloc.start()
before, _ = tracemalloc.get_traced_memory()
tracemalloc.reset_peak()
for index in range(count):
records[index] = factory(1, 2)
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current - before, peak - before
print(platform.python_implementation(), platform.python_version(), platform.platform())
count = 100_000
for factory in (PlainPoint, SlottedPoint):
retained, peak = measure(factory, count)
print(factory.__name__)
print("retained bytes:", retained, "per record:", retained / count)
print("peak bytes above baseline:", peak)
Two choices keep the comparison fair. The script builds the records list
before tracing starts, so the list itself is not part of the result. And
every record holds the same two small integers, which Python shares, so the
loop allocates no new integers.
On a Mac with Python 3.12, a PlainPoint takes about 80 bytes and a
SlottedPoint about 48. On Python 3.15 the plain record takes about 88
bytes, and the slotted one stays at 48. The T3 article reports about 8
bytes more per record, because it builds the list while tracing is on.
Run the script in a fresh process, with no other tracing session. Then vary the experiment:
- Swap the order of the two classes, and try several batch sizes. Report the batch totals, not only the bytes per record.
- Run it again with your real data, such as distinct strings or nested lists. When each record holds a long string, the string takes most of the memory, and the saving from slots matters less.
Check your understanding Traced memory falls by 30 MB after the switch, but the process's RSS barely changes. Is the measurement wrong? Show answer Hide answer
Not necessarily. The records may be a small part of the process's memory. Python also keeps freed memory for reuse instead of giving it back to the operating system at once, so RSS can stay high. Check the application's memory under real load as a separate measurement.
Measure speed separately
A smaller record is not automatically a faster one. Building a record and reading its attributes are separate operations, and you have to time each one. Turn tracing off first, because it slows every allocation. The timing articles later in this track show how to measure small pieces of code.
Decide from the application
Before you switch, read the T3 article's list of what a slotted class gives
up, such as extra attributes, cached_property and weak references.
Then measure the application under a realistic load. A useful result says
how much memory or time changed, at how many records, and whether that
change matters for the service.
Going further
The Python documentation covers tracemalloc and sys.getsizeof.