__slots__
__slots__ lets a class declare a fixed set of attribute names up front, trading away dynamic per-instance attributes for lower memory use and faster attribute access.
By default, every instance of a Python class carries its own __dict__ — a per-instance dictionary that stores whatever attributes get assigned to it, at any time, in any order. __slots__ is a class-level declaration that replaces this flexible-but-costly dictionary with a fixed, pre-allocated layout, similar in spirit to a field list in a struct.
Default Attribute Storage: The Instance __dict__
Without __slots__, CPython gives every instance a __dict__ that holds its attributes as key-value pairs. This is what makes instances so flexible — you can add a new attribute to any object at any time — but a dict has real memory overhead: hash table buckets, resizing headroom, and per-entry bookkeeping, all on top of the attribute values themselves.
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 # freely add a new attribute — no error
print(p.__dict__) # {'x': 1, 'y': 2, 'z': 3}Declaring __slots__
Setting __slots__ to a sequence of attribute-name strings tells Python: this class's instances will only ever have these attributes, so don't bother giving each instance a __dict__ — instead reserve a fixed, dense slot for each name.
__slots__ is usually written as a tuple of strings (or a single string for one slot), but any iterable of strings works. Using a tuple is conventional since the slot list is fixed and should not be mutated.
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 1 2
p.z = 3 # AttributeError: 'Point' object has no attribute 'z'How Slots Actually Store Attributes
Behind the scenes, declaring __slots__ makes the class generate a descriptor for each named slot — a small object that knows how to get/set a fixed-position storage slot on the instance, similar to how @property works but implemented at the C level for compactness. No per-instance __dict__ is created (unless you explicitly re-add '__dict__' to the slots list), which is where both the memory savings and the restrictions come from.
print(type(Point.x)) # <class 'member_descriptor'>
print(Point.__dict__["x"]) # the descriptor object itselfMemory Implications
CPython implementation detail: the exact byte savings depend on CPython's internal object layout and vary by version, but the general shape is consistent — skipping the per-instance dict avoids both the dict object's own overhead and its hash-table headroom, which matters most when you create very many instances of a small class.
Treat any specific byte counts as illustrative, not guaranteed — they are a CPython implementation detail that can shift between versions. The qualitative claim ('slots use meaningfully less memory per instance when you have many instances') is the safe, durable statement; a specific 'N bytes saved' figure is not.
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
a, b = WithDict(1, 2), WithSlots(1, 2)
print(sys.getsizeof(a) + sys.getsizeof(a.__dict__)) # instance + its dict
print(sys.getsizeof(b)) # no separate dict at allRestrictions Imposed by __slots__
Slots trade flexibility for compactness, and several familiar patterns stop working (or need extra opt-in) once a class uses them.
Restriction Why
-------------------------------------------- ----------------------------------------
No new attributes beyond the declared slots there is no dict to hold arbitrary keys
No class-level default value sharing a name a slot descriptor and a class attribute
with a slot (without care) of the same name collide
No __dict__ unless you add "__dict__" to slots exist specifically to omit it
__slots__ yourself (defeats much of the point)
No __weakref__ support unless you add the weakref machinery needs a dedicated
"__weakref__" to __slots__ slot too
Multiple inheritance from two classes that each needs its own layout; Python cannot
both define non-empty __slots__ often fails merge two incompatible instance layoutsclass Base1:
__slots__ = ("a",)
class Base2:
__slots__ = ("b",)
class Combined(Base1, Base2): # TypeError: multiple bases have instance lay-out conflict
__slots__ = ()Slots and Weak References
Ordinary instances support weak references (see the Weak References topic) because CPython reserves a __weakref__ slot for them automatically. A slotted class does not get this for free — if you need weakref.ref(obj) to work on your slotted instances, add '__weakref__' explicitly to __slots__.
import weakref
class NoWeakref:
__slots__ = ("value",)
class Weakrefable:
__slots__ = ("value", "__weakref__")
n = NoWeakref(); n.value = 1
# weakref.ref(n) # TypeError: cannot create weak reference
w = Weakrefable(); w.value = 1
ref = weakref.ref(w) # works fine
print(ref())Slots and Inheritance
Slots are additive down an inheritance chain: a subclass's own __slots__ only needs to list the attributes it adds, not the ones its parent already declared. But if a subclass omits __slots__ entirely, it silently regains a __dict__ (inherited from object's default behavior), which quietly cancels the memory benefit for that subclass — one of the most common slots pitfalls.
If a subclass genuinely adds no new attributes, declare __slots__ = () explicitly to preserve the dict-free layout all the way down the hierarchy.
class Base:
__slots__ = ("x",)
class GoodChild(Base):
__slots__ = ("y",) # adds y, still no per-instance dict
class LeakyChild(Base):
pass # forgot __slots__ = () -> regains a __dict__!
g = GoodChild(); g.x, g.y = 1, 2
l = LeakyChild(); l.x = 1
l.anything = "surprise" # works — LeakyChild has a __dict__ after all
print(hasattr(g, "__dict__"), hasattr(l, "__dict__")) # False TrueWhen Slots Are (and Aren't) Worth It
Slots pay off clearly for classes you instantiate in large numbers with a small, fixed, known-in-advance set of attributes — data-holder classes, nodes in a graph or tree, records read from a file. They are usually not worth the restrictions for classes with few instances, classes that genuinely need dynamic attributes, or classes deep in a multiple-inheritance hierarchy where layout conflicts bite.
For the common 'small, fixed-attribute data record' case specifically, a dataclass (optionally combined with __slots__ via @dataclass(slots=True) since Python 3.10) or NamedTuple often gives you the same memory benefit with less boilerplate — see the related Dataclasses topic.
from dataclasses import dataclass
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
# p.z = 3 # AttributeError — slots are auto-generated from the field listPerformance Claims Must Be Qualified
It is common to see 'slots make attribute access faster' stated as a flat fact. Be precise: slot access can be faster than dict-based attribute access because it skips a hash-table lookup in favor of a fixed-offset read, but the difference is typically small for individual accesses and only becomes meaningful in tight loops over many objects or extremely large object counts. The reliable, durable benefit is memory footprint per instance, not a guaranteed large speedup — always measure with your actual workload (e.g. with timeit) rather than assuming a specific number.
Quick Reference
class C:
__slots__ = ("a", "b") # only a, b allowed as instance attrs
__slots__ = ("a", "__dict__") # keep slots, add back a dict
__slots__ = ("a", "__weakref__") # allow weakref.ref(instance)
__slots__ = () # subclass adds no new attrs, stays dict-free
Trade-offs: less memory per instance, no arbitrary new attributes,
weakref/dict need explicit opt-in, multiple-inheritance layout
conflicts possible, speed gains are workload-dependent — measure.Key Takeaways
__slots__ replaces a per-instance __dict__ with fixed, named storage slots, declared as a class attribute.
Instances of a slotted class cannot get new attributes beyond the declared names unless __dict__ is added back to the slots list.
Slots must be re-declared (or left empty) on every subclass — an inherited class without its own __slots__ silently regains a __dict__.
Weak references need '__weakref__' explicitly added to __slots__; it is not automatic like it is for ordinary classes.
The dependable benefit is lower memory per instance; treat specific byte or speed numbers as CPython-version-dependent, and measure before relying on a performance claim.
For simple data-record classes, @dataclass(slots=True) (3.10+) or NamedTuple often achieve the same goal with less manual bookkeeping.
class Base: __slots__ = ("x",) class Child(Base): pass # forgot __slots__ c = Child() c.anything = 1 # "works", but silently defeats the point of slots class Base: __slots__ = ("x",) class Child(Base): __slots__ = () # explicitly stay dict-free if adding no new attrs
A subclass that omits __slots__ regains a full instance __dict__ by default, silently cancelling the memory savings for that subclass and all further subclasses of it.
class Point: __slots__ = ("x", "y") p = Point() p.x = 1 weakref.ref(p) # TypeError: cannot create weak reference to 'Point' object class Point: __slots__ = ("x", "y", "__weakref__") p = Point() p.x = 1 weakref.ref(p) # works
Weak-reference support is not automatic on a slotted class; '__weakref__' must be listed explicitly among the slots if you need weakref.ref() to work on instances.
class Point: __slots__ = ("x", "y") x = 0 # class attribute named the same as a slot class Point: __slots__ = ("x", "y") def __init__(self, x=0, y=0): self.x = x self.y = y
Defining a class-level attribute with the same name as a slot conflicts with the slot descriptor Python generates for that name and typically raises ValueError at class-creation time; give instances default values in __init__ instead.
What does __slots__ replace, and what does a class lose by using it?
It replaces the per-instance __dict__ with a fixed set of named storage slots declared on the class; instances lose the ability to be given attributes beyond that declared set (and lose automatic __dict__/__weakref__ support unless those names are explicitly added back to __slots__).
If Base defines __slots__ = ('x',) and Child(Base) is defined with no __slots__ at all, does Child still save memory the way Base does? Why or why not?
No — a subclass that does not declare its own __slots__ regains a normal per-instance __dict__ by default, so Child instances get both Base's x slot and a full dict, silently losing the memory benefit; Child should declare __slots__ = () if it adds no new attributes.
Why can't weakref.ref() be used on an instance of a slotted class by default, and how do you fix it?
Ordinary classes get a hidden __weakref__ slot automatically, but slotted classes do not create it unless asked; add the string '__weakref__' to the class's __slots__ tuple to opt back in.
Why does multiple inheritance sometimes fail with 'multiple bases have instance lay-out conflict' when slots are involved?
Each class with non-empty __slots__ defines its own fixed instance memory layout; when two unrelated base classes each define their own non-empty layout, CPython cannot merge them into one consistent layout for the combined subclass, so class creation raises TypeError.
Is it accurate to say 'slots always make attribute access significantly faster'? What is the more precise claim?
No — slot access can be faster than dict-based lookup because it avoids a hash-table lookup, but the difference is usually small per access and workload-dependent; the reliable, durable benefit of __slots__ is reduced memory per instance, not a guaranteed large speed increase, so any speed claim should be measured, not assumed.
What modern, lower-boilerplate alternative achieves a similar memory benefit to hand-written __slots__ for simple data classes, and since what version?
@dataclass(slots=True), available since Python 3.10, auto-generates __slots__ from the dataclass's declared fields.
Write a slotted class Vector3 with x, y, z float attributes, an __init__ that sets them, and a method magnitude() that returns their Euclidean length. Confirm that assigning an undeclared attribute raises AttributeError.
import math class Vector3: __slots__ = ("x", "y", "z") def __init__(self, x: float, y: float, z: float): self.x = x self.y = y self.z = z def magnitude(self) -> float: return math.sqrt(self.x2 + self.y2 + self.z**2) v = Vector3(1.0, 2.0, 2.0) print(v.magnitude()) # 3.0 try: v.w = 4.0 except AttributeError as e: print("blocked:", e)
Declaring __slots__ = ('x', 'y', 'z') means only those three names get storage; assigning v.w raises AttributeError because there is no slot and no __dict__ to fall back on.
What does this print, and what happens on the final line?
It prints '1 2'. The final line raises AttributeError: 'B' object has no attribute 'c' — B's __slots__ only adds 'b' on top of A's 'a'; since neither class re-adds '__dict__', no new attribute name beyond a and b is allowed on B instances.
Write two classes, PointDict (no __slots__) and PointSlots (__slots__ = ('x', 'y')), each storing x and y, and write a small script using sys.getsizeof to compare the size of a single instance of each (noting that PointDict's true cost also includes its __dict__).
import sys class PointDict: def __init__(self, x, y): self.x = x self.y = y class PointSlots: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y d = PointDict(1, 2) s = PointSlots(1, 2) dict_total = sys.getsizeof(d) + sys.getsizeof(d.__dict__) slots_total = sys.getsizeof(s) print("dict-based total:", dict_total) print("slots-based total:", slots_total)
PointDict's real per-instance cost is sys.getsizeof(instance) plus sys.getsizeof(instance.__dict__), since the attributes actually live in that separate dict object; PointSlots has no separate dict, so sys.getsizeof(instance) alone reflects its footprint. The slots-based total is smaller, though the exact byte counts are a CPython implementation detail that can vary by version.
class A:
__slots__ = ("a",)
class B(A):
__slots__ = ("b",)
obj = B()
obj.a = 1
obj.b = 2
print(obj.a, obj.b)
obj.c = 3