skip to content

Why does @dataclass(slots=True) replace the class instead of modifying it?

level: seniorimportance: nice to knowfreq 26%

answer

  1. The decorator runs too late to help
  2. Layout is decided when the type is built
  3. You get back a different object entirely
  4. Captured references point at the discarded class
  5. weakref needs its own slot flag

basics

~20 s

slots has to be present when the type object is created, so a decorator cannot add it afterwards. The decorator builds a brand-new class from the same body plus slots and returns that, leaving anything that already captured the original class pointing at a discarded object.

solid answer

~40 s

A decorator runs *after* the class statement has already produced a type, and by then the instance layout is fixed — `__slots__` is only honoured while the type is being created. So `@dataclass(slots=True)` copies the class body into a fresh class with a `__slots__` entry for every field and returns that new object. The consequence people hit is identity: any decorator stacked **below** `@dataclass(slots=True)`, or any registry, cache or default-value lookup that captured the class during class creation, holds the original class, and `isinstance(new_instance, captured_class)` is `False`. Instances also lose `__dict__`, so ad-hoc attributes fail and `weakref.ref()` raises `TypeError` unless you also pass `weakref_slot=True`. `slots=True` arrived in Python 3.10; `weakref_slot` in 3.11.

code

python · 15 lines
python
from dataclasses import dataclass

registry = []

def register(cls):
    registry.append(cls)
    return cls

@dataclass(slots=True)
@register
class Point:
    x: int

print(registry[0] is Point)
print(isinstance(Point(1), registry[0]))

go deeper

for a junior

Know that slots=True means instances have no dict, so you cannot invent new attributes on them, and that the flag exists only from Python 3.10 onwards.

for a middle

Explain why a decorator cannot add slots after the fact and what the rebuild implies: a new class object, no instance dict, and field defaults no longer readable as class attributes.

for a senior

Diagnose the identity failures — decorator stacking order, registries populated during class creation, weakref TypeErrors — and know weakref_slot as the fix for the last one.

for a principal

Own where the flag belongs: cheap and obviously right on high-volume leaf record types, a commitment on framework-facing base classes that other code may want to extend, attach state to, or capture at import time.

## Why a decorator cannot bolt slots on When a `class` statement executes, Python builds the namespace, calls the metaclass, and the resulting type's instance layout is fixed at that moment. `__slots__` is one of the inputs to that construction: it tells the type machinery to allocate descriptor-backed storage per name and to omit the per-instance `__dict__`. Assigning `Cls.__slots__ = (...)` afterwards sets a class attribute that nothing reads — the layout has already been decided, and the instances still carry a dict. A decorator, by definition, runs after that. So `@dataclass(slots=True)` cannot mutate its way to slots. What it does instead is take the class's namespace, drop the field names' class-level defaults, add a `__slots__` tuple naming every field, and create a **new class** from that namespace with the same name, bases and metaclass. That new object is what the decorator returns and what the module-level name ends up bound to. The class the `class` statement produced is discarded. ## What that costs you **Identity.** Anything that captured the original class object still refers to it, and that object is not the class your instances belong to. The clearest case is decorator stacking, because decorators apply bottom-up: ```python registry = [] def register(cls): registry.append(cls) return cls @dataclass(slots=True) @register class Point: x: int registry[0] is Point # False isinstance(Point(1), registry[0]) # False ``` The `register` decorator ran first, on the original class; `@dataclass` then replaced it. Put the registering decorator **on top** and it captures the final class. The same hazard applies to a metaclass or `__init_subclass__` that stores subclasses during creation, to `super().__init_subclass__` bookkeeping, and to any module-level structure built from the class at definition time. **Class-level defaults are gone.** With slots, each field name on the class is now a slot descriptor, so a default that used to be readable as a class attribute is not: `Point.x` prints something like `<member 'x' of 'Point' objects>` rather than the default value. Code that introspects defaults must use `dataclasses.fields()` and read each `Field`'s default, which is the correct way regardless. **No ad-hoc attributes.** Instances have no `__dict__`, so `obj.temp = 1` raises `AttributeError`. That is often the point — it catches typos in attribute names — but it breaks patching-style code and some serialization helpers. **No weak references by default.** `weakref.ref(obj)` raises `TypeError: cannot create weak reference to 'Point' object`, because the weakref slot is part of the same layout decision. Pass `weakref_slot=True` (Python 3.11+) alongside `slots=True` when the objects need to live in a `WeakSet`, a weak-keyed cache, or an observer registry. **Conflicts are refused.** If the class body already defines `__slots__`, the decorator raises `TypeError: Point already specifies __slots__` rather than trying to merge. ## One thing that is *not* a problem on 3.14 Zero-argument `super()` relies on a hidden `__class__` closure cell that the compiler fills in with the class the method was compiled in — which, after the rebuild, is the discarded class. On Python 3.13 calling `super().__repr__()` from a method of a `slots=True` dataclass raised `TypeError: super(type, obj): obj (instance of Point) is not an instance or subtype of type (Point)`, one of the most confusing error messages in the language. On Python 3.14 the decorator updates that cell to point at the new class, and zero-argument `super()` works normally. If you are supporting older interpreters, the workaround is the explicit two-argument form, or leaving `slots=True` off that class. ## When to use it The reasons to pass `slots=True` — smaller instances and faster attribute access — are properties of the slot layout itself and apply to any class. What is specific to the dataclass flag is convenience: you get the slot names generated from the annotations, kept in sync automatically, and combinable with `frozen=True`. The judgement call is whether the class is a leaf value type (good candidate: many instances, fixed shape, no dynamic attributes, no mixin gymnastics) or a framework-facing class where something else may want to attach state, weak-reference it, or capture it at definition time. For a record type produced in bulk by a scraper or parser it is nearly free; for a base class in an extensible hierarchy it is a commitment worth stating explicitly.

  • How do you keep a class registry working with @dataclass(slots=True)?
    Put the registering decorator **above** `@dataclass(slots=True)` so it runs last and captures the class the decorator returns. If the capture happens in a metaclass or `__init_subclass__` — which necessarily run during class creation, before the decorator — you cannot reorder it; either drop `slots=True` for that hierarchy or re-register the final class explicitly after decoration.
  • Can instances of a slots=True dataclass be weak-referenced?
    Not by default: without a `__dict__` there is also no weakref slot, so `weakref.ref(obj)` raises `TypeError`. Pass `weakref_slot=True` alongside `slots=True` (Python 3.11+) and the decorator adds the slot, at the cost of one pointer per instance. You need it for `WeakSet` membership, weak-keyed caches and observer registries.
  • What happens if the class body already defines __slots__ and you pass slots=True?
    The decorator raises `TypeError` saying the class already specifies `__slots__`. It will not merge your tuple with the generated one, because the generated one is derived from the annotated fields and the two could disagree. Pick one: hand-write `__slots__` and leave the flag off, or let the decorator derive the slots from the field annotations.

You cannot re-plumb a house by putting a note on the front door; the decorator builds an identical house with the pipes in place and hands you the new keys, while anyone holding the old address still goes to the old building.

saying these in an interview costs you the question

  • Says the decorator adds __slots__ to the existing class
  • Expects isinstance against a class captured before decoration
  • Sets ad-hoc attributes on a slots=True dataclass instance
  • Assumes weakref.ref works on any dataclass instance
  • Reads a field default off the class attribute under slots
  • Believes slots=True exists in every Python 3 release

context