By default, every Python object stores its attributes in a dictionary (__dict__). That dictionary is flexible — we can add any attribute at any time — but it costs memory. __slots__ replaces that dictionary with a fixed set of attribute slots, saving memory and making attribute access faster.
How __dict__ Works (The Default)
Normally, each instance carries its own __dict__:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.__dict__) # {'x': 1, 'y': 2}
p.z = 3 # we can add attributes on the fly
print(p.__dict__) # {'x': 1, 'y': 2, 'z': 3}
That flexibility is great, but each __dict__ is a hash table. For a class with just two attributes, we’re paying the overhead of an entire dictionary per instance.
Enter __slots__
__slots__ tells Python “these are the ONLY attributes this class will ever have.” Python then uses a more compact internal structure instead of a dictionary.
class Point:
__slots__ = ('x', 'y') # fixed attribute set
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x) # 1 — works normally
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
# p.__dict__ # AttributeError: 'Point' object has no attribute '__dict__'
We lose the ability to add random attributes, but we gain memory savings and speed.
Memory Savings — Concrete Numbers
The savings are real and measurable. Let’s compare creating a million instances:
import sys
class RegularPoint:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
r = RegularPoint(1, 2)
s = SlottedPoint(1, 2)
print(sys.getsizeof(r) + sys.getsizeof(r.__dict__)) # ~152 bytes
print(sys.getsizeof(s)) # ~48 bytes
That’s roughly 3x less memory per instance. With a million objects, that’s the difference between ~150 MB and ~48 MB. The savings come from eliminating the per-instance hash table.
Faster Attribute Access
Because Python knows exactly where each slot is in memory (it’s a fixed offset, not a hash lookup), reading and writing slotted attributes is faster — roughly 10-20% faster in microbenchmarks.
This matters when we’re doing millions of attribute accesses in tight loops.
__slots__ with Inheritance
This is where it gets tricky. If a parent class has __slots__ and a child class doesn’t define __slots__, the child gets __dict__ back.
class Base:
__slots__ = ('x',)
class Child(Base): # no __slots__ defined
pass
c = Child()
c.x = 1 # uses slot from Base
c.y = 2 # works — Child has __dict__ again
For slots to work through the whole chain, every class in the hierarchy needs __slots__.
class Base:
__slots__ = ('x',)
class Child(Base):
__slots__ = ('y',) # only NEW attributes
c = Child()
c.x = 1 # slot from Base
c.y = 2 # slot from Child
# c.z = 3 # AttributeError — no __dict__
Important: don’t repeat parent slots in the child. Only list new attributes in the child’s __slots__.
Combining __slots__ and __dict__
If we want slots for common attributes but still want the flexibility to add extras, we can include '__dict__' in __slots__:
class Flexible:
__slots__ = ('x', 'y', '__dict__')
def __init__(self, x, y):
self.x = x # stored in slot (fast, compact)
self.y = y # stored in slot
f = Flexible(1, 2)
f.z = 3 # stored in __dict__ (flexible)
We get slot-speed for the common attributes and dict-flexibility for extras. But we still pay the __dict__ overhead when we use it.
__slots__ in Dataclasses
Since Python 3.10, dataclasses have a slots=True parameter that handles everything for us:
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
# p.z = 3 # AttributeError — slots in effect
This is the easiest way to get slotted classes. No manual __slots__ definition needed.
When to Use __slots__
Use it when:
- We’re creating many instances of the same class (thousands or millions)
- The class has a fixed set of attributes that won’t change
- Memory or attribute access speed matters (data processing, game objects, ORM rows)
Avoid it when:
- We need to add attributes dynamically (plugins, monkey-patching)
- The class has very few instances (savings don’t matter)
- We’re using multiple inheritance with non-slotted classes (gets messy)
Quick Gotchas
- No
__dict__means novars(obj)— we can’t introspect attributes the usual way. - No
__weakref__by default — add it to__slots__if we need weak references. - Can’t use
__slots__with variable-length built-in types (like inheriting fromstrorlist).
In simple language, __slots__ trades flexibility for efficiency. Instead of a per-instance dictionary, Python stores attributes in fixed-size slots — using less memory and accessing them faster. We should reach for it when creating lots of instances with a known set of attributes, and @dataclass(slots=True) makes it painless.