π¬ Python Code Lab β Does __slots__ Really Save Memory?
Posted on: October 7, 2026
Description:
Python objects are flexible by design. Attributes can usually be added or modified dynamically, making classes easy to work with.
But that flexibility has a memory cost.
Python's __slots__ is often suggested as a way to reduce the memory consumed by class instances. Instead of simply accepting that claim, let's measure what happens when thousands of regular and slotted objects are created.
What Are We Testing?
We'll compare two classes containing exactly the same data:
RegularUserβ a normal Python classSlottedUserβ a class defining its attributes with__slots__
We'll then create increasing numbers of instances and measure their memory usage using Python's tracemalloc module.
The goal is to answer a simple question:
Does __slots__ actually reduce memory usage, and does the difference become more significant as the number of objects grows?
Creating the Classes
First, we create two equivalent classes.
class RegularUser:
def __init__(self, user_id, name, email):
self.user_id = user_id
self.name = name
self.email = email
class SlottedUser:
__slots__ = ("user_id", "name", "email")
def __init__(self, user_id, name, email):
self.user_id = user_id
self.name = name
self.email = email
Both store the same three values.
The important difference is that SlottedUser explicitly declares which instance attributes are allowed.
Measuring Memory Usage
For the experiment, we use tracemalloc to track memory allocated while the objects are being created.
def measure_memory(user_class, count):
gc.collect()
tracemalloc.start()
users = [
user_class(i, f"User {i}", f"user{i}@example.com")
for i in range(count)
]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current, peak
Garbage collection runs before each measurement to reduce interference from objects created during previous tests.
The objects remain alive when the measurement is taken, allowing us to compare the memory associated with each collection.
Scaling the Experiment
Testing only a few objects would make the difference difficult to appreciate.
Instead, we repeat the measurement at several scales:
object_counts = [1_000, 10_000, 50_000, 100_000]
For every object count, the program creates both regular and slotted objects and calculates the memory difference.
saving = regular_current - slotted_current
saving_percent = (saving / regular_current) * 100
This lets us observe not only whether __slots__ saves memory, but also how that saving behaves as more objects are created.
The exact measurements will vary between Python versions, operating systems, and runtime environments, so the experiment focuses on the relative difference rather than expecting one universal number.
Why Does __slots__ Use Less Memory?
Regular Python class instances normally maintain their attributes through an instance dictionary called __dict__.
That provides useful flexibility, including the ability to add new attributes dynamically.
We can verify its presence directly:
print("RegularUser has __dict__:", hasattr(regular_user, "__dict__"))
print("SlottedUser has __dict__:", hasattr(slotted_user, "__dict__"))
For the simple classes used in this experiment, the regular instance has a __dict__, while the slotted instance does not.
By declaring the expected attributes in advance, __slots__ can avoid that per-instance dictionary overhead.
When only a few objects exist, this difference may not matter much. When tens or hundreds of thousands of similar objects exist simultaneously, those per-object savings can accumulate.
There's a Trade-Off
The reduced memory overhead does not come completely free.
A normal object allows us to do this:
regular_user.role = "admin"
But role was not declared in SlottedUser.__slots__.
Attempting the same operation therefore raises an AttributeError:
slotted_user.role = "admin"
This illustrates the main trade-off: __slots__ provides a more fixed instance structure in exchange for some of the dynamic attribute flexibility available with regular Python objects.
Key Findings
__slots__can reduce the per-instance memory overhead of Python objects.- The benefit becomes easier to observe when many instances of the same class exist.
- Regular instances normally use
__dict__for instance attributes, while simple slotted classes can avoid it. __slots__restricts arbitrary new instance attributes unless they are explicitly supported.- Actual memory savings depend on the Python implementation, version, platform, class structure, and measurement method.
Conclusion
__slots__ really can save memory, particularly when an application maintains large numbers of objects with the same fixed set of attributes.
It is not automatically the right choice for every class. For a small number of objects, the memory reduction may not justify giving up some of Python's dynamic flexibility.
The useful question is therefore not simply "Does __slots__ save memory?", but "Does it save enough memory to matter for this workload?"
And that is exactly the kind of question worth measuring.
Code Snippet:
import gc
import tracemalloc
class RegularUser:
def __init__(self, user_id, name, email):
self.user_id = user_id
self.name = name
self.email = email
class SlottedUser:
__slots__ = ("user_id", "name", "email")
def __init__(self, user_id, name, email):
self.user_id = user_id
self.name = name
self.email = email
def measure_memory(user_class, count):
gc.collect()
tracemalloc.start()
users = [
user_class(i, f"User {i}", f"user{i}@example.com")
for i in range(count)
]
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return current, peak
object_counts = [1_000, 10_000, 50_000, 100_000]
results = []
for count in object_counts:
regular_current, regular_peak = measure_memory(RegularUser, count)
slotted_current, slotted_peak = measure_memory(SlottedUser, count)
saving = regular_current - slotted_current
saving_percent = (saving / regular_current) * 100
results.append(
(
count,
regular_current,
slotted_current,
saving,
saving_percent,
)
)
print(f"{'Objects':>10} {'Regular (MB)':>15} {'Slots (MB)':>15} {'Saved (MB)':>15} {'Saved (%)':>12}")
print("-" * 72)
for count, regular, slotted, saving, saving_percent in results:
print(f"{count:>10,} {regular / 1024**2:>15.2f} {slotted / 1024**2:>15.2f} {saving / 1024**2:>15.2f} {saving_percent:>11.2f}%")
regular_user = RegularUser(1, "Alice", "alice@example.com")
slotted_user = SlottedUser(1, "Alice", "alice@example.com")
print("\nRegularUser has __dict__:", hasattr(regular_user, "__dict__"))
print("SlottedUser has __dict__:", hasattr(slotted_user, "__dict__"))
regular_user.role = "admin"
print("\nRegularUser role:", regular_user.role)
try:
slotted_user.role = "admin"
except AttributeError as error:
print("SlottedUser:", error)
No comments yet. Be the first to comment!