Why does `self._data = value` inside `__setattr__` recurse forever, and what is the fix?
answer
- The hook is not exempt from itself
- Dot syntax on self re-enters the hook
- Use the unbound default implementation
- object.__setattr__ or super().__setattr__
- Seed backing state before any access
basics
~20 sBecause an assignment inside setattr is itself an attribute assignment, so it calls setattr again and never terminates, ending in RecursionError. Break the cycle by delegating to object.setattr or super().setattr, or by writing straight into the instance dictionary.
solid answer
~40 sThe attribute hooks are re-entrant: every `self.x` read goes through `__getattribute__`, every `self.x = v` through `__setattr__`, and every `del self.x` through `__delattr__`. So a hook that uses ordinary attribute syntax on `self` calls itself, and the interpreter stops it with `RecursionError`. The fix is to use the unbound default implementation for internal state — `object.__setattr__(self, name, value)`, `object.__getattribute__(self, name)`, `object.__delattr__(self, name)` — or, inside a class hierarchy, `super().__setattr__(name, value)`, which still runs cooperative bases in the MRO. `self.__dict__[name] = value` also works but is blunter: it bypasses data descriptors such as a `property`, and fails outright when the class uses `__slots__`. The same trap has a third shape: a `__getattr__` that reads a backing attribute which `__init__` has not yet set will re-enter itself on the miss.
code
python · 27 linesclass Broken:
writes = 0
def __setattr__(self, name, value):
self.writes = self.writes + 1 # re-enters __setattr__
object.__setattr__(self, name, value)
try:
Broken().qty = 5
except RecursionError:
print("RecursionError")
class Fixed:
def __init__(self):
super().__setattr__("writes", 0)
def __setattr__(self, name, value):
super().__setattr__("writes", self.writes + 1)
super().__setattr__(name, value)
f = Fixed()
f.qty = 5
f.sku = "A-1"
print(f.qty, f.sku, f.writes)go deeper
Remember the shape of the bug: assigning to self inside setattr, or reading self inside getattribute, calls the hook again and ends in RecursionError. The escape is object.setattr or super().
Explain why the hooks are re-entrant, show all three shapes including a getattr that reads state init has not set yet, and justify choosing super() over object. for the delegating call.
Show the defensive habits: seed backing state before any access is possible, read internal state through object.getattribute, and handle construction paths that skip init such as unpickling and copying.
Weigh whether a class needs these hooks at all. Each one adds a re-entrancy hazard that surfaces in debuggers and serialisation, so set a team norm for when dynamic attribute interception is worth its review cost.
## The mechanism Every attribute operation on an instance is routed through a hook on its type. Reading `self.x` calls `type(self).__getattribute__`; assigning `self.x = v` calls `type(self).__setattr__`; deleting calls `type(self).__delattr__`. Those routes do not care that the code doing the access is *inside* one of the hooks. Writing an assignment inside `__setattr__` therefore invokes `__setattr__` again, with no base case, until CPython's recursion limit trips and raises `RecursionError`. The traceback is distinctive: hundreds of identical frames pointing at one line of your own hook. Recognising that shape is half the debugging. ## The three shapes of the trap **1. Writing inside `__setattr__`.** The direct case: `self._count = self._count + 1` inside the hook re-enters on the write (and, if the read side is also hooked, on the read). **2. Reading inside `__getattribute__`.** Because that hook runs on *every* access, any `self.anything` in its body — including a helper flag or a cached dict — re-enters immediately. **3. A `__getattr__` that reads unseeded state.** This is the subtlest one and the most common in real code. A record wrapper does `return self._data[name]` inside `__getattr__`. That is safe *once `_data` exists*, because the ordinary lookup finds it and the hook is not entered. But if `_data` has not been set — during `__init__` before the assignment, after unpickling, or when a subclass forgets to call the parent constructor — the lookup misses, `__getattr__` runs to resolve `_data`, which reads `self._data`, which misses again. Infinite recursion on a class that works perfectly in the happy path. ## The fixes, in order of preference **Delegate to the default implementation.** `object.__setattr__(self, name, value)` performs the normal store without consulting your hook; `object.__getattribute__(self, name)` performs the normal read; `object.__delattr__(self, name)` the normal delete. These are the canonical escape hatches and they respect data descriptors and `__slots__`. **Prefer `super()` inside a hierarchy.** `super().__setattr__(name, value)` starts the search after your class in the MRO, so a cooperative base that also customises attribute access still runs. `object.__setattr__` jumps straight to the root and silently skips those bases. Use `object.` only when you deliberately want the raw default — commonly for reading one private attribute of your own. **Write to `self.__dict__` only when you mean it.** `self.__dict__[name] = value` stores the value directly. It is a bigger hammer: it bypasses data descriptors, so a `property` with a validating setter is silently defeated, and it raises `AttributeError` when the class defines `__slots__` and has no instance dictionary. **Seed backing state before the hooks can be exercised.** In `__init__`, install the backing container with `object.__setattr__(self, "_data", {})` *first*. That guarantees `_data` is present for every later lookup, so the fallback can read it normally. A defensive `__getattr__` can also read its own backing state through `object.__getattribute__(self, "_data")`, which cannot re-enter. ## Related traps worth naming * **`hasattr` or `getattr` inside a hook** is an attribute access like any other and re-enters the same hook. Test membership against the backing container instead. * **Name-prefix guards** — `if name.startswith("_"): return super().__setattr__(name, value)` — are a common shortcut, and they work, but they couple correctness to a naming convention that a subclass can break. * **Unpickling and copying** construct an instance *without* calling `__init__` and then restore state. A `__getattr__` that assumes `__init__` ran will recurse the moment the restore machinery probes the half-built object. Guard it by reading the backing attribute through `object.__getattribute__` inside a `try`, and raising `AttributeError` when it is absent. * **Debuggers and reprs** touch attributes to render an object, so a recursive hook can hang the debugger rather than the program, which makes the bug look like a tool problem. ## The habit to internalise Inside an attribute hook, treat plain `self.x` syntax as forbidden for anything the hook itself is responsible for. Use the explicit unbound form for internal state, delegate for everything you do not handle, and make sure the backing state exists before any access can happen. Those three rules eliminate essentially every recursion bug in this area. ## Testing for it The bug is trivially testable once you know the shapes, and the tests are worth writing because the failure mode is a hang-like stack explosion rather than a clean assertion failure. Cover: reading a name the object does not have, writing a brand-new name, deleting one, constructing the object and immediately touching an attribute inside `__init__`, and round-tripping the object through `copy.deepcopy`. The last one is the highest-value case, because it exercises the construction path that skips `__init__`, which is exactly where a hook that assumes seeded state falls over in production but never in a unit test that only builds objects the normal way.
- When would you write `self.__dict__[name] = value` instead of `object.__setattr__(self, name, value)`?Almost never by preference. Writing to the instance dictionary bypasses data descriptors, so a `property` with a validating setter is silently defeated, and it fails with AttributeError under `__slots__` because there is no instance dictionary. Choose it only when you deliberately want to shadow nothing and store raw state, and say so in a comment; `object.__setattr__` is the honest default.
- Why can unpickling trigger this recursion in a class that works normally?The pickling machinery creates the instance without calling `__init__` and then restores its state, probing the half-built object for optional hooks along the way. Any probe for a name that is missing runs `__getattr__`, which reads a backing attribute that has not been restored yet, and the hook re-enters itself. Read the backing attribute through `object.__getattribute__` in a try block and raise AttributeError when it is absent.
- How do you tell this bug apart from ordinary deep recursion in a traceback?The traceback repeats one or two frames from your own hook, alternating with nothing else, and the failing line is an attribute access on `self` inside `__setattr__`, `__getattribute__` or `__getattr__`. Ordinary runaway recursion shows the recursive call site instead. If a debugger hangs while merely displaying the object, suspect an attribute hook, because rendering an object reads its attributes.
saying these in an interview costs you the question
- Raises the recursion limit instead of fixing the hook
- Thinks the hook is exempt from its own interception
- Uses hasattr inside the hook to guard it
- Writes to __dict__ under __slots__ and expects it to work
- Blames the recursion on a circular import or reference cycle