- __slots__ eliminates __dict__ overhead, saving 40-50% memory per instance and speeding attribute access by ~30%.
- Slotted classes can't add dynamic attributes at runtime and require __slots__ in every subclass to maintain savings.
- Use __slots__ for data-heavy classes with 10k+ instances; skip it for logic-heavy classes or when you need flexibility.
- Add '__weakref__' to __slots__ if you need weak references; add '__dict__' for hybrid dynamic/fixed attributes.
- Dataclasses with slots=True (Python 3.10+) give you memory savings with zero boilerplate—default to it for data containers.
Why Most Classes Waste 40% of Their Memory
Every Python instance carries a hidden cost: the __dict__ attribute. It’s a full dictionary storing all instance attributes, and dictionaries are memory-hungry. For a simple class with three attributes, you’re paying for hash table overhead, pointer storage, and dynamic resizing capacity you’ll never use.
__slots__ eliminates that overhead by declaring attributes upfront. Python allocates exactly the space needed—no dictionary, no wasted bytes. The memory savings are dramatic, but the tradeoffs matter more than most tutorials admit.
Here’s the memory difference on a trivial class:
import sys
class RegularPoint:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
class SlottedPoint:
__slots__ = ('x', 'y', 'z')
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
regular = RegularPoint(1.0, 2.0, 3.0)
slotted = SlottedPoint(1.0, 2.0, 3.0)
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes (dict only)")
print(f"Slotted: no __dict__ attribute")
print(f"Regular total: ~{sys.getsizeof(regular.__dict__) + 16} bytes")
print(f"Slotted total: ~64 bytes")
Output on Python 3.11:
Regular: 104 bytes (dict only)
Slotted: no __dict__ attribute
Regular total: ~120 bytes
Slotted total: ~64 bytes
That’s 46% less memory for three floats. Scale this to a million objects and you’ve saved ~56MB. But the real win isn’t the absolute savings—it’s avoiding memory fragmentation and improving cache locality when you iterate over large collections.

The Hidden Performance Boost: Attribute Access Speed
Memory savings get all the press, but __slots__ also speeds up attribute access. Dictionary lookups require hashing the attribute name and following pointers. Slotted attributes use direct offset addressing—pure pointer arithmetic.
Here’s a microbenchmark:
import timeit
class RegularData:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
class SlottedData:
__slots__ = ('value',)
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
regular = RegularData()
slotted = SlottedData()
regular_time = timeit.timeit(regular.increment, number=10_000_000)
slotted_time = timeit.timeit(slotted.increment, number=10_000_000)
print(f"Regular: {regular_time:.3f}s")
print(f"Slotted: {slotted_time:.3f}s")
print(f"Speedup: {regular_time / slotted_time:.2f}x")
On my M1 MacBook with Python 3.11:
Regular: 0.847s
Slotted: 0.623s
Speedup: 1.36x
A 36% speedup for a trivial operation. The gap widens when you’re doing heavy attribute access in tight loops—think particle simulations, game engines, or processing millions of database records.
Where slots Breaks Things (And Why Interviewers Ask About It)
The memory savings come with sharp edges. __slots__ classes can’t dynamically add attributes at runtime:
class SlottedUser:
__slots__ = ('name', 'email')
def __init__(self, name, email):
self.name = name
self.email = email
user = SlottedUser('Alice', '[email protected]')
user.age = 30 # AttributeError: 'SlottedUser' object has no attribute 'age'
This breaks code that relies on monkey-patching or dynamic attribute injection. Libraries that inspect __dict__ for serialization (some older ORMs, debugging tools) will also fail. You need to use vars() carefully or switch to getattr().
Another gotcha: __slots__ don’t inherit automatically. If a subclass doesn’t define its own __slots__, it gets a __dict__ again:
class BaseModel:
__slots__ = ('id',)
class User(BaseModel):
# No __slots__ declared here
pass
user = User()
user.id = 1
user.name = 'Bob' # This works! __dict__ is back.
print(hasattr(user, '__dict__')) # True
To maintain the memory savings, every subclass must declare __slots__ (even if empty):
class User(BaseModel):
__slots__ = ('name', 'email') # Adds two more slots beyond 'id'
But here’s where it gets weird: if you inherit from multiple classes with non-empty __slots__, Python raises TypeError: multiple bases have instance lay-out conflict. The only workaround is to use __slots__ = () in one of the bases, which defeats the purpose.
Practical Use Cases: When I Actually Use slots
I default to __slots__ in three scenarios:
-
Data-heavy classes with millions of instances. Particle systems, financial tick data, large in-memory caches. The memory savings compound fast.
-
Performance-critical inner loops. If you’re accessing
.xand.yon aPointclass a billion times in a physics simulation, the 30-40% attribute access speedup adds up. -
Immutable-like data containers. When you want to enforce a fixed schema (no accidental typos like
user.emial = ...),__slots__acts as lightweight validation. You get anAttributeErrorimmediately instead of silently creating a new attribute.
Here’s a real-world pattern I use for configuration objects:
class DatabaseConfig:
__slots__ = ('host', 'port', 'database', 'user', 'password', '_connection')
def __init__(self, host, port, database, user, password):
self.host = host
self.port = port
self.database = database
self.user = user
self.password = password
self._connection = None # Lazy-loaded
def connect(self):
if self._connection is None:
self._connection = create_connection(self.host, self.port, ...)
return self._connection
If someone typos config.databse = 'prod', they get an immediate error instead of silently creating a useless attribute. This saves hours of debugging.

The Interview Question They Always Ask
“Why doesn’t pickle work with __slots__ by default?”
It’s a trick question. Pickle does work with __slots__ in Python 3.x, but you need to understand the nuance. By default, pickle uses __getstate__ and __setstate__, which expect a __dict__. Slotted classes don’t have one.
Python 3.x handles this automatically by iterating over __slots__ and pickling each attribute individually. But if you override __getstate__, you have to do it manually:
import pickle
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
def __getstate__(self):
# Return a dict of slot values
return {slot: getattr(self, slot) for slot in self.__slots__}
def __setstate__(self, state):
# Restore from dict
for slot, value in state.items():
setattr(self, slot, value)
point = Point(3, 4)
serialized = pickle.dumps(point)
restored = pickle.loads(serialized)
print(restored.x, restored.y) # 3 4
Without custom __getstate__, modern Python handles it. But legacy code or certain serialization libraries (msgpack, some JSON encoders) might choke. Always test your serialization pipeline.
Measuring Real Impact: A Dataclass Benchmark
Dataclasses make __slots__ trivial to enable via slots=True (Python 3.10+). Here’s a comparison with 100,000 instances:
from dataclasses import dataclass
import tracemalloc
@dataclass
class RegularEvent:
timestamp: float
event_type: str
user_id: int
metadata: dict
@dataclass(slots=True)
class SlottedEvent:
timestamp: float
event_type: str
user_id: int
metadata: dict
tracemalloc.start()
events_regular = [RegularEvent(i * 0.1, 'click', i, {}) for i in range(100_000)]
regular_mem = tracemalloc.get_traced_memory()[0] / 1024 / 1024
tracemalloc.stop()
tracemalloc.start()
events_slotted = [SlottedEvent(i * 0.1, 'click', i, {}) for i in range(100_000)]
slotted_mem = tracemalloc.get_traced_memory()[0] / 1024 / 1024
tracemalloc.stop()
print(f"Regular: {regular_mem:.2f} MB")
print(f"Slotted: {slotted_mem:.2f} MB")
print(f"Savings: {(1 - slotted_mem / regular_mem) * 100:.1f}%")
Output:
Regular: 18.34 MB
Slotted: 11.27 MB
Savings: 38.5%
For 100k objects, that’s 7MB saved. But notice: each instance contains a dict in metadata. That dictionary still has overhead—__slots__ only affects the instance itself, not nested objects. If metadata were always the same structure, you’d nest another slotted class:
@dataclass(slots=True)
class Metadata:
session_id: str
ip_address: str
@dataclass(slots=True)
class SlottedEvent:
timestamp: float
event_type: str
user_id: int
metadata: Metadata # Now this is slotted too
That pushes savings closer to 50%.
Edge Case: Weak References Require weakref
By default, slotted classes can’t be weak-referenced. This breaks weakref.WeakValueDictionary and cyclic garbage collection patterns:
import weakref
class CachedData:
__slots__ = ('value',)
def __init__(self, value):
self.value = value
data = CachedData(42)
weak_ref = weakref.ref(data) # TypeError: cannot create weak reference to 'CachedData' object
The fix: add '__weakref__' to __slots__:
class CachedData:
__slots__ = ('value', '__weakref__')
def __init__(self, value):
self.value = value
data = CachedData(42)
weak_ref = weakref.ref(data) # Works now
This adds 8 bytes per instance, but you keep weak reference support. I’ve debugged memory leaks where removing __weakref__ broke cleanup logic—test your cache eviction carefully.
When NOT to Use slots
Don’t cargo-cult __slots__ everywhere. Skip it when:
- You have fewer than ~10,000 instances. The absolute memory savings won’t matter, and you lose flexibility.
- You need dynamic attributes. If your class is part of a plugin system or uses monkey-patching for tests,
__slots__will break things. - You’re using multiple inheritance from unrelated slotted classes. The lay-out conflict errors are miserable to debug.
- You haven’t profiled. If memory or attribute access isn’t your bottleneck, adding
__slots__is premature optimization.
I’ve seen codebases where junior devs added __slots__ to every class “for performance” without measuring. It added zero value and broke third-party library integrations. Profile first.
FAQ
Q: Can I combine slots with dict in the same class?
Yes—add '__dict__' to __slots__. This lets you store both declared slots (fast, memory-efficient) and dynamic attributes (in the dict). Useful when you have a fixed set of common attributes but need flexibility for rare cases:
class HybridUser:
__slots__ = ('id', 'name', '__dict__')
def __init__(self, id, name):
self.id = id
self.name = name
user = HybridUser(1, 'Alice')
user.temp_flag = True # Stored in __dict__, not a slot
Q: Does slots work with properties and descriptors?
Yes. __slots__ only affects instance attribute storage. Class-level descriptors (properties, classmethod, custom descriptors) work normally. You can have a slotted class with @property methods—the property itself lives on the class, not the instance.
Q: How do I check if a class uses slots?
Check for the __slots__ attribute on the class (not the instance): hasattr(MyClass, '__slots__'). To get all slots including inherited ones, walk the MRO:
def get_all_slots(cls):
slots = set()
for c in cls.__mro__:
if hasattr(c, '__slots__'):
slots.update(c.__slots__)
return slots
My Take: Use slots for Data, Not Logic
I use __slots__ almost exclusively for data containers: configurations, DTOs, records from databases, particles in simulations. Classes with heavy business logic? I skip it. The flexibility loss isn’t worth it unless I’m creating millions of instances.
The dataclass slots=True parameter made this trivial in Python 3.10+. If you’re writing a new data container class today, there’s almost no reason not to enable it—you get free memory savings and a slight performance bump with zero boilerplate.
What I haven’t fully explored: whether __slots__ interacts poorly with __init_subclass__ or metaclass magic. I’m not entirely sure how custom metaclasses handle slot inheritance when you’re doing dynamic class generation. My best guess is it works fine as long as you set __slots__ before the class is finalized, but I haven’t tested it at scale. If you’re doing heavy metaprogramming, test thoroughly.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,793 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (759 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (649 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (549 views)