Python __slots__ — Why Your Subclass Still Has __dict__
MemoryError processing 3.2M points: expected 220MB, got 1.2GB.
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- __slots__ replaces instance __dict__ with fixed C-level slot descriptors
- Memory savings: ~67% less per instance for 3-attribute objects (56 MB vs 18 MB for 100k instances)
- Attribute access uses direct offset instead of hash lookup — ~15% faster reads in CPython
- You lose dynamic attribute assignment: trying to set an undeclared attribute raises AttributeError
- Biggest mistake: expecting __slots__ to work across inheritance without defining it in every child class
Think of a Python object like a backpack. Normally, each backpack has a separate duffel bag (__dict__) where you can toss any item whenever you want. __slots__ replaces that duffel bag with fixed compartments sewn into the backpack. You can only put the items you planned for, but the backpack is lighter and you find things faster because you know exactly where they are.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Most Python objects carry a __dict__ — a hash map storing all instance attributes. For a small number of large objects this is fine. For millions of small objects (coordinate points, events, records), the dict overhead becomes significant.
__slots__ is the mechanism for trading flexibility for efficiency. Once you define __slots__, your class no longer has a __dict__ per instance, and attributes are stored as fixed C-level offsets instead.
What __slots__ Actually Does — and Doesn't
__slots__ is a class-level attribute that tells Python to reserve a fixed-size array for instance attributes instead of the per-instance __dict__. This eliminates the hash table overhead — each slot becomes a descriptor that stores the value at a known offset in the instance's internal struct. The result: each instance saves ~64 bytes (the __dict__ overhead) and attribute access becomes a direct array lookup instead of a hash-table probe, giving a measurable speedup in tight loops.
Crucially, __slots__ only applies to the class that defines it. Subclasses that don't redeclare __slots__ will still get a __dict__ — and if they do declare __slots__, they inherit the parent's slots but also get their own __dict__ unless they explicitly set __slots__ = () to suppress it. This is the single most common source of confusion: developers assume __slots__ is inherited like a method, but it's not — it's a per-class declaration that controls the layout of that class's instances.
Use __slots__ when you have many instances (thousands or more) of a simple data-holder class — for example, ORM models, game entities, or configuration objects. The memory savings are linear: 100,000 instances save ~6 MB of dict overhead. But never use __slots__ on classes that need dynamic attribute assignment, weak references (unless you add __weakref__ to slots), or inheritance chains where subclasses add attributes — the complexity quickly outweighs the benefit.
Basic __slots__ Usage
To use __slots__, declare a class-level attribute __slots__ containing a tuple or list of attribute names. That's it. CPython then allocates fixed-size descriptors for these names instead of a per-instance __dict__.
You can still assign values normally in __init__. The difference is you can't add new attributes after __init__. Trying to do so raises AttributeError.
This is the simplest way to get the memory win — but watch out for inheritance gotchas (see later section).
- A Python class without __slots__ is like an open dictionary in memory.
- __slots__ freezes the attribute names at class definition time.
- Access becomes a C pointer offset instead of a hash table probe.
- You trade flexibility for speed and memory — exactly like choosing a struct over a dict in C.
Memory Savings at Scale
The memory win is real when you handle tens of thousands of objects. Each Python object without __slots__ carries a __dict__ overhead of about 232 bytes (for a typical dict) plus the object header. With __slots__, you only have the object header and the slot values — typically 40-80 bytes total.
Here's a benchmark comparing 100,000 event objects with and without __slots__:
sys.getsizeof() on a single instance and multiply by N.Inheritance and __slots__
Here's the trap most engineers hit: __slots__ in a parent class does NOT carry over to child classes. Each subclass must define its own __slots__, otherwise the subclass instances will still have a __dict__ — and you lose the memory benefit.
If a subclass defines __slots__, it can only include the new attributes it adds, not the parent's. Python merges them at the C level automatically.
What happens if a parent class does NOT use __slots__? Then any subclass that uses __slots__ will STILL have a __dict__ because the parent provides one. The only way to avoid that is to include '__dict__' in the parent's __slots__ (defeating the purpose) or to refactor the hierarchy.
Performance: Attribute Access Speed
Removing the dict hash lookup gives you a small but measurable speed boost for reading and writing attributes. In microbenchmarks, __slots__ attribute access is about 10-20% faster than dict-backed access. For most applications the difference is negligible, but in tight loops (e.g., game physics, data processing pipelines) it can add up.
Note that the speed gain comes from avoiding the hash computation and dict resize overhead, not from eliminating the attribute itself. Writing to a slot is still a Python attribute set operation, but it bypasses the dict insertion path.
Use Cases and Trade-offs
__slots__ shines where you have many small, simple objects. Classic use cases: - Data transfer objects (DTOs) representing rows, API responses, or log entries - Game entities (player positions, bullets, particles) - Large collections of immutable value objects (coordinates, timestamps) - Objects that are serialized/deserialized frequently (less memory pressure reduces GC pauses)
Trade-offs you must accept: 1. No dynamic attributes — every attribute must be declared at class definition. 2. Breaks some libraries: Django models, SQLAlchemy's ORM, and many patches that rely on __dict__. You can't use __slots__ with those out of the box. 3. Inheritance complexity as discussed. 4. Weak references: classes with __slots__ can't be weakly referenced unless you add '__weakref__' to __slots__. 5. Default values: You can't set default values in __slots__ directly; you need to handle them in __init__.
Alternatives and Best Practices
- namedtuple / SimpleNamespace: for immutable, lightweight objects without __slots__ hassle
- dataclass(slots=True) (Python 3.10+): automatic __slots__ generation with less boilerplate
- Manual dict usage: if you need many attributes but can use a single dict field
- __dict__ with __slots__: include '__dict__' in __slots__ to allow dynamic attributes while still getting some memory benefit (but you lose most of the savings)
Best practices: 1. Measure before and after: never rely on intuition. Use sys.getsizeof() and tracemalloc. 2. Keep __slots__ at the leaf classes of your hierarchy; avoid putting it on abstract base classes. 3. Document the trade-off explicitly in the class docstring. 4. If you inherit from a C extension type (e.g., tuple, list), __slots__ may not work; check the type's tp_dictoffset.
Why __slots__ Breaks Pickling (and What to Do)
You just decorated a high-volume data class with __slots__ to cut memory. Great. Now your serialization pipeline throws AttributeError: 'MyRecord' object has no attribute '__dict__'. This is the first thing that burns juniors in production.
Pickle and copy.deepcopy rely on __dict__ by default. When you kill the dict, you kill naive serialization. The fix isn't to remove slots — it's to implement __getstate__ and __setstate__. Or switch to __reduce__ if you need pickle protocol 2+.
For JSON serialization, you already have a method. Add __json__ or use dataclasses with slots=True which handles this for you. Never assume your downstream consumers can handle slot-based objects without explicit support.
If you use multiprocessing, check your pickling path before rollout. I've seen a 200-node cluster silently fail because a slot-based config object couldn't be serialized across workers. Test this during system testing, not after deployment.
How __slots__ Breaks Weak References (Fixed With __weakref__)
Your caching layer uses weakref.WeakValueDictionary to hold slot objects. Good idea — until objects get garbage collected immediately because you forgot one line. When you define __slots__, you also remove the __weakref__ attribute that Python's garbage collector uses for weak references.
The symptom: Every object you add to the weak dictionary disappears instantly. Your cache evicts entries on insertion. This is another silent failure that only shows up under load.
Fix: Add '__weakref__' to your __slots__ tuple. It's a reserved slot name that restores weak reference support without bringing back the full __dict__. Do this whenever your objects participate in any caching, observer pattern, or event system.
If you're using weakref.finalize for cleanup, same problem applies. Python 3.4+ has __weakref__ as a standard slot, but you must declare it explicitly when overriding __slots__. Otherwise your destructors never run.
Rule of thumb: If your class is used in a cache, callbacks, or async context, add '__weakref__' to slots. It costs ~8 bytes per object and saves hours of hair-pulling.
__slots__ with Inheritance: Constraints and Patterns
When using __slots__ in a class hierarchy, there are important constraints and patterns to follow. If a parent class defines __slots__, the child class must also define __slots__ to avoid creating a __dict__. If the child class does not define __slots__, it will automatically have a __dict__ and the parent's __slots__ will still work, but the child instances will have both slots and a __dict__. To enforce no __dict__ in subclasses, each subclass must explicitly define __slots__ (even if empty). Additionally, a subclass can only define slots that are not already defined in its parent classes; attempting to redefine a parent slot will raise a TypeError. A common pattern is to define __slots__ in each subclass, including the parent's slots via concatenation or by using a tuple that extends the parent's slots. For example:
```python class Parent: __slots__ = ('x', 'y')
class Child(Parent): __slots__ = ('z',) # Child instances have x, y, z slots, no __dict__ ```
If you want to allow arbitrary attributes in a subclass, you can omit __slots__ in the child, but then instances will have a __dict__ alongside inherited slots. This is often undesirable for memory optimization. Another pattern is to use __slots__ in mixins or abstract base classes to enforce a consistent interface. Remember that __slots__ are inherited, but each class must explicitly list its own slots to avoid __dict__ creation. Also, nonempty __slots__ in a subclass prevent the creation of a __dict__ even if the parent has none. For diamond inheritance, the same rules apply: each class in the MRO must have __slots__ defined to avoid __dict__. In practice, it's best to design your class hierarchy with __slots__ from the base class down, ensuring all subclasses explicitly define their slots.
__slots__ vs dataclasses(slots=True)
Python 3.10 introduced slots=True in the @dataclass decorator, which automatically generates a __slots__ attribute for the class. This combines the convenience of dataclasses with the memory and performance benefits of __slots__. However, there are differences between manually defining __slots__ and using dataclasses(slots=True). With dataclasses(slots=True), the decorator generates __slots__ based on the fields, and also creates appropriate __init__, __repr__, __eq__, and other methods. It also handles inheritance: if a parent dataclass uses slots=True, the child must also use slots=True to avoid __dict__ creation, similar to manual __slots__. One advantage of dataclasses(slots=True) is that it automatically adds __weakref__ if needed, whereas manual __slots__ requires explicit __weakref__ to support weak references. Additionally, dataclasses with slots=True still support default values, field types, and other dataclass features. However, manual __slots__ gives you more control: you can define slots for non-field attributes, mix with properties, or use slots in non-dataclass classes. Performance-wise, both are similar because they both use __slots__ internally. The choice depends on whether you need the full dataclass functionality. If you just need a simple data container with __slots__, @dataclass(slots=True) is the easiest. For more complex scenarios or when you need to customize behavior, manual __slots__ might be better.
Example: ```python from dataclasses import dataclass
@dataclass(slots=True) class Point: x: float y: float
p = Point(1.0, 2.0) print(p.x) # 1.0 # p.z = 3.0 # AttributeError: 'Point' object has no attribute 'z' ```
This is equivalent to manually defining __slots__ and writing the dataclass methods, but with less boilerplate.
@dataclass(slots=True) over manual __slots__ for data containers, as it reduces code and integrates with type checkers and IDEs.dataclasses(slots=True) provides the benefits of __slots__ with less boilerplate, but manual __slots__ offers more flexibility for non-dataclass classes.Performance Benchmarks: __slots__ vs __dict__
To quantify the performance benefits of __slots__, we can run benchmarks comparing attribute access speed and memory usage between a class with __slots__ and a regular class with __dict__. The following benchmark uses Python's timeit module to measure attribute access time and sys.getsizeof for memory (though getsizeof is shallow; for deep memory, use pympler or tracemalloc). Typically, __slots__ classes are about 10-20% faster for attribute access and use significantly less memory (around 40-60% less per instance for simple classes). The exact savings depend on the number of attributes and the class hierarchy.
Example benchmark: ```python import timeit import sys
class WithDict: def __init__(self, x, y): self.x = x self.y = y
class WithSlots: __slots__ = ('x', 'y') def __init__(self, x, y): self.x = x self.y = y
# Memory comparison d = WithDict(1, 2) s = WithSlots(1, 2) print(f'WithDict size: {sys.getsizeof(d)} bytes') print(f'WithSlots size: {sys.getsizeof(s)} bytes')
# Speed comparison setup = 'from __main__ import WithDict, WithSlots; d=WithDict(1,2); s=WithSlots(1,2)' time_dict = timeit.timeit('d.x; d.y', setup=setup, number=10_000_000) time_slots = timeit.timeit('s.x; s.y', setup=setup, number=10_000_000) print(f'WithDict access time: {time_dict:.3f} sec') print(f'WithSlots access time: {time_slots:.3f} sec') print(f'Speedup: {(time_dict/time_slots - 1)*100:.1f}%') ```
Results (may vary): WithDict ~56 bytes, WithSlots ~40 bytes (for two attributes). Access time: WithDict ~0.45 sec, WithSlots ~0.38 sec for 10 million accesses, about 18% faster. Memory savings are more pronounced with many instances. For 1 million instances, __slots__ can save tens of megabytes. Note that __slots__ also reduces memory fragmentation because the slots are stored in a compact array rather than a dictionary.
The 4-Million-Point Memory Blowup
- __slots__ is not inherited. Every subclass must define its own __slots__ to avoid the __dict__ penalty.
- Always verify memory consumption with
sys.getsizeof()on instances of every subclass in the hierarchy. - If you need both slots and dynamic attributes, include '__dict__' in __slots__ — but you lose the memory benefit.
sys.getsizeof() on an instance and look for __dict__ attribute. If present, verify inheritance chain.print(hasattr(instance, '__dict__'))print(getattr(cls, '__slots__', 'No __slots__'))| File | Command / Code | Purpose |
|---|---|---|
| slots_inheritance.py | class Base: | Inheritance and __slots__ |
| speed_comparison.py | class DictPoint: | Performance |
| weakref_example.py | class WithSlots: | Use Cases and Trade-offs |
| dataclass_slots.py | from dataclasses import dataclass | Alternatives and Best Practices |
| slots_pickle_fix.py | class Record: | Why __slots__ Breaks Pickling (and What to Do) |
| slots_weakref.py | class CacheNode: | How __slots__ Breaks Weak References (Fixed With __weakref__ |
| slots_inheritance.py | class Parent: | __slots__ with Inheritance |
| slots_benchmark.py | class WithDict: | Performance Benchmarks |
Key takeaways
Interview Questions on This Topic
What is the purpose of __slots__ in Python and when would you use it?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
That's Advanced Python. Mark it forged?
7 min read · try the examples if you haven't