Blog

__slots__, and When Memory Actually Matters

Every ordinary Python object carries a dictionary holding its attributes. That is what lets you add an attribute at any time — and it is why objects cost more memory than you would guess.

__slots__ opts out.

The measurement

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

class Slotted:
    __slots__ = ('x', 'y')
    def __init__(self, x, y): self.x, self.y = x, y

p, s = Plain(1,2), Slotted(1,2)
print('plain  ', sys.getsizeof(p) + sys.getsizeof(p.__dict__))
print('slotted', sys.getsizeof(s))
print('slotted has no __dict__:', not hasattr(s, '__dict__'))

It prints:

plain   344
slotted 48
slotted has no __dict__: True

The slotted object stores its two attributes in a fixed array instead of a dict.

An honest note about that number

Measured across many instances rather than one, the gap is smaller:

N = 200_000
plains = [Plain(i, i) for i in range(N)]
slots  = [Slotted(i, i) for i in range(N)]
pb = sum(sys.getsizeof(o) + sys.getsizeof(o.__dict__) for o in plains[:1000]) / 1000
sb = sum(sys.getsizeof(o) for o in slots[:1000]) / 1000
print(f'per instance: plain {pb:.0f}b, slotted {sb:.0f}b')
print(f'for {N:,}: plain {pb*N/1e6:.1f} MB, slotted {sb*N/1e6:.1f} MB')

It prints:

per instance: plain 144b, slotted 48b
for 200,000: plain 28.8 MB, slotted 9.6 MB

144 bytes, not 344. The single-object measurement was misleading because CPython shares the key storage between instances of the same class — the first instance pays for the keys, later ones do not. Measure one object and you attribute the shared cost to it alone.

That is worth knowing generally: a getsizeof on one object is not a per-instance cost.

Real saving at 200,000 objects: about 19 MB, roughly a third. Meaningful if you are holding millions of rows in memory. Invisible below tens of thousands.

The free benefit: typos fail

p.z = 99
print('plain accepts a typo:', p.z)
try:
    s.z = 99
except AttributeError as err:
    print('slotted rejects it:', type(err).__name__ + ':', err)

It prints:

plain accepts a typo: 99
slotted rejects it: AttributeError: 'Slotted' object has no attribute 'z'

This is the reason to use __slots__ on a class that will never have a memory problem. Part 8 showed a plain object silently accepting an undeclared attribute — a misspelled assignment that creates a field nobody reads. __slots__ makes that an error at the line where you wrote it.

What you give up

class SlottedSub(Slotted):
    pass
sub = SlottedSub(1,2)
sub.anything = 5             # subclass without __slots__ regains a __dict__
print('subclass regained __dict__:', hasattr(sub, '__dict__'), '| set', sub.anything)

It prints:

subclass regained __dict__: True | set 5

A subclass that does not declare its own __slots__ gets a __dict__ back, and both the saving and the typo protection are gone. Every class in the chain must declare it.

You also lose the ability to attach attributes dynamically, which some libraries do — caching, ORMs, and anything that decorates instances. If a library misbehaves against a slotted class, this is usually why.

The easy version

@dataclass(slots=True)
class SlottedDC:
    x: int
    y: int
print('dataclass(slots=True):', SlottedDC(1,2), sys.getsizeof(SlottedDC(1,2)))

It prints:

dataclass(slots=True): SlottedDC(x=1, y=2) 48

slots=True arrived in Python 3.10 and is the version most people should use — the generated __init__ and __repr__, with the slots.

What to remember

  • __slots__ removes the per-instance dict. Real saving at hundreds of thousands of objects, invisible below that.

  • Do not measure it with getsizeof on one object — key sharing makes that misleading.

  • It rejects undeclared attributes, which is the better reason to use it.

  • Every class in an inheritance chain must declare it, or the saving is undone.

Not a default. Reach for it when a profiler points at memory, or when you want a mutable class with a NamedTuple’s resistance to typos.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.