Skip to content

__slots__ - Dramatic memory reduction

By default, every Python instance stores its attributes in a __dict__, a regular Python dictionary that lives on the heap alongside the object. This dictionary is flexible and allows you to add any attribute at any time, but it comes with a significant hidden memory cost, a dictionary has substantial overhead even when it only holds a few keys.

__slots__ is a class-level declaration that tells Python exactly which attributes an instance will ever have. Instead of creating a __dict__ for each instance, Python allocates a fixed, compact block of memory with one slot per attribute, like a C struct rather than a dictionary.

When Python creates an instance, it actually allocates two separate objects on the heap, not one:

  • Object 1 — the instance itself This is the actual object, it holds the type information, the reference count, and a pointer to the __dict__. It does not hold the attribute values directly.

  • Object 2 — the __dict__ This is a separate, standalone Python dictionary that lives next to the instance on the heap. This is where the actual attribute values are stored.

rp = RegularPoint(1.0, 2.0, 3.0)
Heap
────
Object 1 Object 2
──────── ────────
[ RegularPoint instance ] [ __dict__ ]
type: RegularPoint ─────────────────────────────────► "x" 1.0
refcount: 1 "y" 2.0
pointer to __dict__ ──────┘ "z" 3.0
~48 bytes ~232 bytes
(the instance) (the dictionary)

You can verify both objects exist independently:

rp = RegularPoint(1.0, 2.0, 3.0)
# Object 1 — the instance
print(sys.getsizeof(rp)) # ~48 bytes
print(type(rp)) # <class 'RegularPoint'>
print(id(rp)) # memory address of Object 1
# Object 2 — the __dict__
print(sys.getsizeof(rp.__dict__)) # ~232 bytes
print(type(rp.__dict__)) # <class 'dict'>
print(id(rp.__dict__)) # different memory address — Object 2

So when you access rp.x, Python has to do two steps:

rp.x
Step 1 find the instance on the heap
──────
rp ──────────────────────────► [ RegularPoint instance ]
pointer to __dict__ ──┐
Step 2 follow the pointer to __dict__, look up "x"
──────
◄───────────────────────┘
[ __dict__ ]
"x" 1.0 found it
"y" 2.0
"z" 3.0

With __slots__ there is only one object, the instance itself holds the attribute values directly, no dictionary, no second object, no two-step lookup:

sp = SlottedPoint(1.0, 2.0, 3.0)
Heap
────
Object 1 only
─────────────
[ SlottedPoint instance ]
type: SlottedPoint
refcount: 1
slot x 1.0 stored directly in the instance
slot y 2.0 no pointer, no second object
slot z 3.0
~72 bytes total one object, one allocation

So the “two memory costs” means literally two heap allocations: one for the instance object and one for its __dict__ dictionary, where __slots__ reduces this to a single allocation.

dict implies 2 objects
rp = RegularPoint(1.0, 2.0, 3.0)
Heap
────
[ RegularPoint instance ] ~48 bytes
└──► [ __dict__ ] ~232 bytes the hidden cost
├── "x" ──► [ float: 1.0 ]
├── "y" ──► [ float: 2.0 ]
└── "z" ──► [ float: 3.0 ]
total per instance: ~280 bytes

With __slots__, the dictionary disappears entirely:

__slots__ implies 1 object
sp = SlottedPoint(1.0, 2.0, 3.0)
Heap
────
[ SlottedPoint instance ] ~72 bytes
├── slot x ──► [ float: 1.0 ]
├── slot y ──► [ float: 2.0 ]
└── slot z ──► [ float: 3.0 ]
total per instance: ~72 bytes
no __dict__ slots are part of the object itself
class RegularPoint:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# Python automatically creates __dict__ for each instance
class SlottedPoint:
__slots__ = ["x", "y", "z"] # pre-declare all attributes
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
# no __dict__ created — attributes stored in fixed slots

The difference per instance is modest, but across hundreds of thousands of instances it becomes significant:

import tracemalloc
tracemalloc.start()
# 100,000 regular instances
regular = [RegularPoint(i, i, i) for i in range(100_000)]
snapshot1 = tracemalloc.take_snapshot()
regular_stats = snapshot1.statistics("lineno")
regular_mem = sum(stat.size for stat in regular_stats)
# 100,000 slotted instances
slotted = [SlottedPoint(i, i, i) for i in range(100_000)]
snapshot2 = tracemalloc.take_snapshot()
slotted_stats = snapshot2.statistics("lineno")
slotted_mem = sum(stat.size for stat in slotted_stats)
print(f"regular: {regular_mem / 1024 / 1024:.1f} MB")
print(f"slotted: {slotted_mem / 1024 / 1024:.1f} MB")
100,000 instances
RegularPoint SlottedPoint
──────────── ────────────
100,000 × ~280 bytes 100,000 × ~72 bytes
= ~28 MB = ~7 MB
~75% less memory at scale
Regular class__slots__ class
Memory per instance~280 bytes~72 bytes
Dynamic attributes✅ Yes❌ No
__dict__ present✅ Yes❌ No
Best forGeneral purpose objectsLarge collections of simple objects
Typical use caseBusiness logic, modelsPoints, vectors, records, data classes