Why can super(type(self), self) cause a RecursionError once a class is subclassed?
answer
- One argument should never be computed
- Correct until somebody subclasses the class
- The search starts one step too late
- The next class found is the method itself
- self.__class__ has the same defect
basics
~20 sThe first argument of a two-argument super must be the class the code is written in, not the runtime type. With a subclass, type(self) starts the search one step too late, so it re-enters the same method.
solid answer
~40 s`super(C, obj)` means "start searching after `C` in `type(obj)`'s order", so `C` has to be the *defining* class — a fixed, compile-time fact. `type(self)` is the *receiver's* class, which is the same thing only while nobody subclasses. Write `super(type(self), self).run()` inside `B.run` and it behaves correctly for a `B` instance; construct a `C(B)` and `type(self)` is now `C`, so the search starts after `C`, finds `B.run` again, and recurses until `RecursionError`. The same trap appears as `super(self.__class__, self)`. The fix is zero-argument `super()`, which reads the defining class from the compiler-inserted `__class__` cell and cannot be fooled, or an explicit `super(B, self)` if you must spell it out.
code
python · 18 linesimport sys
class A:
def tag(self): return "A"
class B(A):
def tag(self): return "B" + super(type(self), self).tag()
class C(B):
pass
print(B().tag())
sys.setrecursionlimit(200)
try:
C().tag()
except RecursionError:
print("RecursionError from super(type(self), self)")go deeper
You are unlikely to be asked this, but take away the habit it teaches: write super() with no arguments and never compute a class to hand it. That single rule avoids the whole family of mistakes.
Be able to trace it: for a subclass instance the search starts after the most-derived class and lands on the very method doing the call. Say why the class's own tests still pass and why zero-argument super() cannot express the bug.
Recognise it from a production traceback — a repeating single-method cycle with an unchanged receiver — and explain why a latent defect that only fires in someone else's subclass is worse than one that fails at import.
Treat it as a lint and review matter rather than a lesson: computed first arguments to super can be flagged mechanically, and standardising on the zero-argument form removes an entire class of latent, cross-team failures from a shared library.
### What the first argument actually means The two-argument form `super(C, obj)` is defined as: take `type(obj).__mro__`, find `C` in it, and search the classes *after* that position. `C` answers "where am I standing", not "where do I want to go". The only correct value is the class whose body contains the code doing the call. `type(self)` answers a different question. It is the class of the object that happened to be passed in, which is the most-derived class in the hierarchy, not the class the method was written in. The two coincide exactly when the receiver is an instance of the defining class itself — which is precisely the case every unit test constructs. ### The recursion, step by step ```python class A: def tag(self): return "A" class B(A): def tag(self): return "B" + super(type(self), self).tag() class C(B): pass print(B().tag()) # 'BA' — looks fine C().tag() # RecursionError ``` For `B()`, the order is `(B, A, object)`; `type(self)` is `B`; the search starts after `B` and finds `A.tag`. Correct, by coincidence. For `C()`, the order is `(C, B, A, object)`. The call still executes inside `B.tag`, but `type(self)` is now `C`, so the search starts after `C` — and the very next class is `B`. It finds `B.tag`, calls it, and that method evaluates `super(type(self), self)` again with an unchanged `self`. Nothing advances. Each iteration pushes another frame until the interpreter raises `RecursionError`. The identical bug is written as `super(self.__class__, self)`, which is the same expression by another spelling and fails in exactly the same way. ### Why this survives review and testing Three properties make it durable. It is *correct for the class that defines it*, so the module's own tests pass. The failure is *not local*: it appears in a subclass, possibly written months later by another team, and the traceback shows a wall of identical frames in the base class rather than in the code that caused it. And it *reads plausibly* — `type(self)` looks like the dynamic, future-proof choice, when it is the opposite: dynamic is exactly what the first argument must not be. When you see the traceback, the tell is a repeating two-frame cycle through one method, with the receiver unchanged. Look for a `super` call whose first argument is computed rather than named. ### What it looks like in production The recursion limit is reached quickly, but the traceback is unhelpful at first glance: CPython collapses the identical frames into a `[Previous line repeated N more times]` marker, so the visible stack is a short sandwich around one repeating line. Read the repeated line, not the top frame. If the recursive method logs or allocates on entry, the incident often arrives as a burst of log lines or a memory spike and gets reported as something other than a recursion bug, which delays the diagnosis further. ### The fix and the general rule Zero-argument `super()` is the answer in almost every case. It takes the defining class from the `__class__` cell the compiler stores in the method, which is fixed at class-creation time and cannot drift with the receiver, and it takes the receiver from the frame. It is impossible to write this bug with it. If you genuinely need the explicit form — code outside a class body, a nested helper with no first argument — name the class literally: `super(B, self)`. The rule to carry: **the first argument is a constant, the second is a variable.** Anything computed from the receiver in that first slot is a defect. ### A related distinction worth keeping straight This is not the same failure as passing the wrong *kind* of object as the second argument. If the second argument is neither an instance of the first nor a subclass of it, Python rejects the pair immediately with a `TypeError` — a loud, cheap error at the call site. The `type(self)` bug passes that check every time, because `self` really is an instance of `type(self)`. It is well-formed and wrong, which is why it costs a production incident rather than a failed import.
- How would you recognise this bug from a traceback alone?You see a very deep stack made of one repeating cycle: the same method in the same class, over and over, with the same receiver, ending in RecursionError. That signature says the search is not advancing through the order. Scan the repeated frame for a super call whose first argument is computed — type(self), self.__class__, or anything derived from the receiver — rather than a literal class name.
- Does the same mistake break classmethods, and how is it spelled there?Yes. The classmethod form is `super(C, cls)`, and writing `super(cls, cls)` reproduces the bug: `cls` is the most-derived class for a subclass call, so the search starts after it and re-enters the same classmethod. The first argument must again be the literal defining class, or better, use zero-argument `super()`, which works in a classmethod and binds `cls` from the frame automatically.
- Is there any legitimate reason to compute the first argument of super?Essentially none in normal code. The first argument fixes a position in the order and that position is a property of where the source lives, so it should be a literal or come from the __class__ cell. The rare exception is tooling that walks a hierarchy deliberately — a debugger or a migration script iterating classes — and there the class is chosen from an explicit iteration, not from the receiver's type.
It is like giving directions that begin "from where you are now" instead of "from the corner shop": the instruction works while you happen to be standing at the shop, and sends the next person round in circles forever.
saying these in an interview costs you the question
- Thinks type(self) is the safe, dynamic choice
- Believes super's first argument is the target class
- Says the code is fine because its own tests pass
- Confuses this with the TypeError for a mismatched pair
- Uses self.__class__ as a different, safer spelling
- Blames the recursion limit rather than the argument