skip to content

Object Overhead and __slots__

Why a million small objects cost more than the data inside them: each carries a header and usually a per-instance __dict__. Interviewers ask about __slots__ and sys.getsizeof to see if you can shrink.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

How much memory does adding __slots__ to a Python class actually save?

level: middleimportance: must knowfreq 60%

answer

  1. Declare the names, lose the flexibility
  2. No per-instance dictionary machinery
  3. The baseline moved in 3.11
  4. Roughly a third, not five times
  5. A subclass without slots gives it back

basics

~20 s

__slots__ fixes the attribute names up front so instances store values at known offsets and carry no per-instance dictionary. On CPython 3.14 that is roughly a 35-45% cut for a small class - real, but far less than the old folklore.

solid answer

~50 s

Declaring `__slots__` tells CPython the complete set of attribute names, so each value lives at a fixed offset in the instance and the per-instance dictionary machinery disappears. Measured on 3.14 with `tracemalloc` over 200,000 instances, a two-attribute class costs about 96 bytes per instance plain and about 56 with `__slots__`. The old "halves your memory" figure predates 3.11, where CPython started laying a plain instance's attribute values out with the object and materializing a dictionary only on demand - that narrowed the gap. The costs are real too: no undeclared attributes, no weak references unless you list `"__weakref__"`, no `vars(obj)`, and a subclass that omits its own `__slots__` gets a dictionary back along with the cost. It pays at millions of instances of one settled class, and pays nothing on a handful of service objects.

code

python · 22 lines
python
import tracemalloc

class Plain:
    def __init__(self, t, v):
        self.t = t
        self.v = v

class Slotted:
    __slots__ = ("t", "v")

    def __init__(self, t, v):
        self.t = t
        self.v = v

def per_instance(cls, n=200_000):
    tracemalloc.start()
    kept = [cls(1.5, 2.5) for _ in range(n)]
    used, _ = tracemalloc.get_traced_memory()
    tracemalloc.stop()
    return used / len(kept)

print(per_instance(Plain), per_instance(Slotted))

go deeper

for a junior

Know what the declaration does at the level of behaviour: with __slots__ you list the attribute names in the class body, instances become smaller, and setting a name you did not list raises AttributeError.

for a middle

Explain the mechanics and the honest number: fixed offsets instead of a per-instance dictionary, roughly a third off a small class on 3.14, and why the pre-3.11 folklore figures are too generous.

for a senior

Demonstrate the decision, not the syntax: measure per-instance cost on your own class, apply it to the few classes held in bulk, and recognise when the real answer is to stop making one object per record at all.

for a principal

Frame it as a flexibility budget. Locking attribute sets buys memory and loses runtime extensibility, so decide which layers of the system are data holders that may be frozen and which must stay open to extension.

### What `__slots__` changes Writing `__slots__ = ("t", "v")` in a class body tells CPython the complete set of attribute names its instances will ever have. The class then stores each of those values at a fixed offset inside the instance itself, and instances of that class get **no per-instance dictionary machinery at all** — no dictionary object, no inline values array sized from the class's key table, and no ability to bind a name that was not declared. That is the whole memory story: you trade "any attribute, discovered at runtime" for "these attributes, at known offsets". ### What it actually saves, measured Rules of thumb here are stale, because the baseline moved. Before 3.11 a plain instance often carried a separate dictionary object and the reported savings were dramatic — the "`__slots__` halves your memory" number comes from that era. Since 3.11, CPython keeps a plain instance's attribute values in a compact array laid out with the object and only materializes a dictionary when something asks for `obj.__dict__`, so the gap narrowed. On CPython 3.14, 64-bit, a two-attribute class measured with `tracemalloc` over 200,000 instances comes out at roughly **96 bytes per instance plain versus 56 with `__slots__`** — a saving in the 35–45% range, depending on the attribute count. It is real, it is worth having when you hold millions of instances, and it is not the 5x that folklore promises. Measure your own class rather than quoting a number. ### What it costs * **No undeclared attributes.** `obj.anything_else = 1` raises `AttributeError`, which is a feature for typo-catching and a problem for any code that decorates objects at runtime. * **No weak references** unless you list `"__weakref__"` in `__slots__` — the slot that made weak references possible is one of the things you removed. * **No `__dict__`**, so `vars(obj)` fails and anything that introspects an instance by reading its dictionary has to be rewritten to walk `__slots__` instead. * **A subclass that does not declare its own `__slots__` gets a dictionary back**, and with it the cost you were avoiding — a common way for the saving to quietly disappear months later. * **Pickling, copying and serialization helpers** that assume a dictionary need attention. ### When it is worth it The decision is a scale question, not a style question. `__slots__` pays when you hold enough instances of one class that 40 bytes each is a number you care about — a large in-memory index, a long-lived cache, a buffer of records — and when the class is a settled data holder rather than something callers extend. It pays nothing on a handful of service objects, and applying it project-wide is a cost with no benefit: you lose flexibility everywhere to save memory in the two places that hold bulk. There is also a ceiling. If your million objects each hold two floats, `__slots__` takes you from about 96 bytes to about 56 — but a typed buffer holding the same numbers costs 8 bytes each. When the shape is "lots of numbers", the right move is usually to stop making one object per row at all; `__slots__` is the right move when you genuinely need one Python object per row and want it lean. ### How to measure rather than guess `sys.getsizeof` on a single instance will mislead you: it is shallow and does not include the inline values array or the objects the attributes point at. Allocate a realistic number of instances under `tracemalloc.start()` / `tracemalloc.get_traced_memory()` and divide, or compare process RSS before and after, and do it on the interpreter build you actually deploy — the free-threaded build, officially supported from 3.14, carries extra per-object fields and will give different numbers.

  • What does `__slots__` cost you besides the ability to set undeclared attributes?
    Weak references stop working unless you list `"__weakref__"` explicitly, `vars(obj)` and anything that introspects an instance through its dictionary break, and pickling or copying helpers that assume a dictionary need attention. In practice the sharpest cost is organisational: the class can no longer be extended by adding an attribute at the call site.
  • How would you measure the saving on your own class rather than trusting a rule of thumb?
    Allocate a realistic number of instances inside `tracemalloc.start()` / `tracemalloc.get_traced_memory()` and divide, or compare process RSS before and after. `sys.getsizeof` on a single instance is shallow and will not include the inline values array or the objects the attributes point at, so it is the wrong instrument here.
  • When would you not reach for `__slots__` even though the objects are numerous?
    When the objects are mostly numbers. `__slots__` shrinks the instance that holds attributes; it does nothing about a million boxed floats. If each record is a few numeric fields, typed buffers - one per column - cut the cost to a few bytes per value instead of tens, which `__slots__` can never reach.

saying these in an interview costs you the question

  • Quotes a fixed multiple like halves memory without measuring
  • Applies __slots__ across an entire codebase by default
  • Forgets a subclass without __slots__ regains a dictionary
  • Thinks __slots__ makes attribute access dramatically faster
  • Assumes weak references still work with a minimal __slots__
  • Uses sys.getsizeof on one instance to prove the saving

context

open as a page

Why does a Python instance with three attributes cost far more memory than three raw values?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Every Python object starts with a header - a reference count and a pointer to its type - and a normal instance carries attribute storage on top of that. The values are separate objects too, each with its own header.

open as a page

Why does sys.getsizeof on a list of dicts under-report the memory it really uses?

level: middleimportance: should knowfreq 52%

basics

~10 s

sys.getsizeof is shallow: it reports only that object's own storage plus collector bookkeeping and never follows a pointer. For a list it counts the pointer array, not the dictionaries the pointers lead to.

open as a page

A sensor-telemetry collector buffers millions of float readings in a list - how do you cut the memory per reading?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Stop making one Python object per reading. A list of floats costs about 32 bytes per value on 3.14 - a pointer plus a boxed float; array.array("d") stores the same doubles contiguously at about 8 bytes each.

open as a page