💡 Python QuickBits — 📦 Memory Saver with __slots__
Posted On: September 10, 2025
Description:
When you create many instances of a class in Python, each one carries an internal dict to store attributes. This makes objects flexible — but also memory-heavy.
Python offers an optimization: slots. By declaring fixed attributes, you remove the per-object dict. The result? Smaller memory footprint and often faster attribute access.
Normal Class vs slots
A regular class stores attributes in a dict. With millions of instances, this overhead adds up.
class UserNormal:
def __init__(self, uid, name, active):
self.uid = uid
self.name = name
self.active = active
class UserSlots:
__slots__ = ("uid", "name", "active")
def __init__(self, uid, name, active):
self.uid = uid
self.name = name
self.active = active
With UserSlots, Python knows exactly what attributes exist. No dict means less memory per object.
Attribute Restrictions
The trade-off: slots forbids adding new attributes not listed in the slots.
u = UserSlots(1, "alice", True)
u.email = "alice@example.com" # ❌ raises AttributeError
This can actually be a feature — it prevents typos and keeps object structure predictable.
Attribute Access Speed
Because slots bypass the dictionary lookup, attribute access is often slightly faster.
u_norm = UserNormal(1, "alice", True)
u_slots = UserSlots(1, "alice", True)
def read_norm(): return u_norm.uid
def read_slots(): return u_slots.uid
import timeit
print(timeit.timeit(read_norm, number=2_000_000))
print(timeit.timeit(read_slots, number=2_000_000))
Expect slots to be a bit faster, but the real gain is memory savings.
Key Points
- Use slots in object-heavy apps to cut memory use.
- Slight speedup in attribute access.
- You cannot add arbitrary attributes.
- If needed, include "weakref" in slots for weak references.
- Python 3.10+: @dataclass(slots=True) auto-generates slots.
Code Snippet:
import tracemalloc # measure memory usage by taking snapshots
import timeit # quick micro-benchmarks for attribute access
# Define two equivalent classes: one normal, one with __slots__
class UserNormal:
# regular class has a per-instance __dict__ (flexible but heavier)
def __init__(self, uid, name, active):
self.uid = uid # set attribute 'uid'
self.name = name # set attribute 'name'
self.active = active # set attribute 'active'
class UserSlots:
__slots__ = ("uid", "name", "active") # declare fixed attributes; no __dict__ by default
def __init__(self, uid, name, active):
self.uid = uid # set attribute 'uid'
self.name = name # set attribute 'name'
self.active = active # set attribute 'active'
def make_users(cls, n=100_000):
# helper to create n instances with small strings/ints
return [cls(i, f"user{i}", (i % 2 == 0)) for i in range(n)]
# Measure memory for normal class instances
tracemalloc.start() # begin tracking allocations
_ = make_users(UserNormal, n=100_000) # create many normal instances
snap_normal = tracemalloc.take_snapshot() # snapshot memory after creation
tracemalloc.stop() # stop tracking
# Measure memory for slots class instances
tracemalloc.start() # restart tracking for a clean measurement
_ = make_users(UserSlots, n=100_000) # create many slots instances
snap_slots = tracemalloc.take_snapshot() # snapshot memory after creation
tracemalloc.stop() # stop tracking
# Compute total allocated size for each snapshot (sum all traces)
total_normal = sum(stat.size for stat in snap_normal.statistics('filename'))
total_slots = sum(stat.size for stat in snap_slots.statistics('filename'))
print(f"Total allocated (normal): {total_normal/1024/1024:.2f} MiB")
print(f"Total allocated (slots) : {total_slots/1024/1024:.2f} MiB")
print(f"Memory reduction (~): {(1 - (total_slots/total_normal)) * 100:.1f}%")
u = UserSlots(1, "alice", True) # create a slots-based instance
try:
u.email = "alice@example.com" # ❌ adding new attribute not in __slots__ will fail
except AttributeError as e:
print("Expected AttributeError:", e)
# If you need weak references, include "__weakref__" in __slots__
class UserSlotsWeak:
__slots__ = ("uid", "name", "active", "__weakref__") # allows weakref support if needed
def __init__(self, uid, name, active):
self.uid = uid
self.name = name
self.active = active
# Prepare one instance of each for fair comparison
u_norm = UserNormal(1, "alice", True)
u_slots = UserSlots(1, "alice", True)
def read_norm():
# read a couple of attributes; return something so it's not optimized away
return u_norm.uid + (1 if u_norm.active else 0)
def read_slots():
# same workload for the slots-based instance
return u_slots.uid + (1 if u_slots.active else 0)
# timeit returns total seconds for the given number of loops; lower is better
t_norm = timeit.timeit(read_norm, number=2_000_000) # run many iterations
t_slots = timeit.timeit(read_slots, number=2_000_000)
print(f"Attribute read (normal): {t_norm:.3f}s")
print(f"Attribute read (slots) : {t_slots:.3f}s")
speedup = (t_norm / t_slots) if t_slots else float('inf')
print(f"Speedup (≈): {speedup:.2f}x")
No comments yet. Be the first to comment!