Backend / Python core / 25_slots.md

__slots__ — when it saves memory and what it breaks

Updated 3 min read source
On this page8
  1. What it does
  2. Memory impact
  3. What slots break
  4. Subclassing
  5. With @dataclass
  6. When to use
  7. When NOT to use
  8. Interview angle

__slots__ — when it saves memory and what it breaks

__slots__ declares a fixed set of attributes for a class, replacing the per-instance __dict__ with a compact array of slots.

What it does

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

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

p = Point(1, 2)
p.x = 10            #
p.z = 3             # AttributeError: 'Point' object has no attribute 'z'
p.__dict__          # AttributeError: 'Point' object has no attribute '__dict__'

Without slots:

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

p = Point2(1, 2)
p.z = 3             # — instance dict accommodates anything
p.__dict__          # {'x': 1, 'y': 2, 'z': 3}

Memory impact

Since Python 3.11 a regular instance stores its attributes inline and only builds a real __dict__ when something asks for it, so the saving is smaller than older articles claim. And sys.getsizeof(a.__dict__) is a misleading measurement: reading a.__dict__ materialises the dict it is measuring. Measured with tracemalloc over 200,000 two-attribute instances (each also holding one int of about 32 B):

text
3.12.6  regular 112 B  slotted 80 B  ratio 1.40x  (ints included)
3.14.5  regular 120 B  slotted 80 B  ratio 1.50x  (ints included)

The objects alone are about 88 B against 48 B on 3.14, roughly 1.8x, and the gap stays that size whatever the attributes hold. It matters for millions of small objects (graph nodes, order book entries, ORM rows); for thousands it is noise.

Attribute access is also slightly faster (slot is a fixed offset, dict requires hash lookup).

What slots break

  • No dynamic attributes. instance.new_attr = ... fails for anything not in slots.
  • No __dict__. Code that introspects via vars(obj) or obj.__dict__ breaks.
  • Pickling needs care — default pickling uses __dict__; slotted classes need __getstate__/__setstate__ or rely on pickle’s slot support.
  • Multiple inheritance is restricted — at most one base class with non-empty __slots__. Otherwise: TypeError: multiple bases have instance lay-out conflict.
  • @cached_property doesn’t work without explicitly adding the attribute name to __slots__.
  • Class-level defaults conflict — __slots__ = ("x",) with x = 5 at class level fails (slot descriptor masks the default).

Subclassing

If a slotted class is subclassed by a non-slotted class, the subclass instances do get a __dict__ (slots don’t propagate as a constraint, only as a memory layout hint).

python
class A:
    __slots__ = ("x",)

class B(A):   # no __slots__ declared
    pass

b = B()
b.x = 1       # from slot
b.y = 2       # B has __dict__

To keep memory savings in subclasses, declare __slots__ = () (empty) in the subclass.

With @dataclass

Python 3.10+:

python
from dataclasses import dataclass

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

Equivalent to writing __slots__ = ("x", "y") manually.

When to use

  • Many instances (tens of thousands+) → big memory win
  • Class is a value object / data container → no need for dynamic attrs anyway
  • Performance-critical attribute access in tight loops

When NOT to use

  • Few instances → negligible benefit, lose flexibility
  • Dynamic plugins / mixins that monkey-patch instances
  • You need pickling without thinking about it
  • Heavy multiple inheritance

Interview angle

“What does __slots__ do?” (Replaces __dict__ with a fixed array — saves memory.) Follow-up: “What are the trade-offs?” (No dynamic attrs, MI restrictions, pickling caveats.) Senior follow-up: “When would you use it?” (High-instance-count value classes — graph nodes, time-series records.)