When a class's __add__ returns NotImplemented, what does Python do next?
answer
- The return value is a signal, not a result
- The other operand gets a turn
- Same method name with an r prefix
- Operands arrive swapped in the reflected call
- Both decline, interpreter raises TypeError
basics
~10 sPython reads the return as a decline and calls the right operand's reflected method, radd, with the operands swapped. If that also returns NotImplemented, the interpreter raises TypeError: unsupported operand type(s) for +.
solid answer
~40 s`NotImplemented` is a decline, not a result. For `left + right`, Python calls `type(left).__add__(left, right)`; if that returns the sentinel it calls `type(right).__radd__(right, left)`, and if that also returns the sentinel it raises `TypeError: unsupported operand type(s) for +: 'Left' and 'Right'`. The sentinel never escapes into your code — the interpreter consumes it. Note that the reflected method receives swapped arguments: inside `__radd__`, `self` is the right operand, which matters for non-commutative operators like `__rsub__`. This is why a dunder should *return* `NotImplemented` for an unknown operand rather than raise `TypeError` itself: raising ends the expression, so a type that knew how to combine itself with yours never gets asked. Every arithmetic and bitwise operator follows the same pattern with its own `__r*__` partner.
code
python · 24 linesclass Depth:
def __init__(self, reads):
self.reads = reads
def __add__(self, other):
if not isinstance(other, Depth):
return NotImplemented
return Depth(self.reads + other.reads)
def __radd__(self, other):
if other == 0: # sum() starts from the integer 0
return self
return NotImplemented
def __repr__(self):
return f"Depth({self.reads})"
print(sum([Depth(120), Depth(83)])) # int declines, __radd__ picks it up
try:
Depth(120) + "chr1"
except TypeError as exc:
print(exc)go deeper
Recall the shape: returning NotImplemented does not end the operation, it gives the other operand a chance through its radd, and only a second decline produces a TypeError. Knowing the reflected name pattern (r prefix) is enough at this level.
This is your question. Walk the full sequence for left + right, name the reflected partner of at least one other operator, and explain that radd receives the operands swapped so a non-commutative operation must compute other - self, not self - other.
Demonstrate the interoperability judgment: type guards in dunders decline rather than raise, so types you do not control can still combine with yours. Be ready to diagnose an unsupported-operand TypeError as both sides declining, and to say which of the two types should have been taught about the other.
Own the library-design line. Decide whether your public types accept foreign operands at all, keep the operator surface symmetric so left and right placement behave alike, and treat a raised TypeError inside a dunder as an interoperability defect in review.
### The resolution sequence For `left + right`, CPython does not simply call `type(left).__add__(left, right)` and take whatever comes back. It runs a small protocol: 1. Call `type(left).__add__(left, right)` (the reflected method goes first in one special case — when `type(right)` is a proper subclass of `type(left)` that overrides `__radd__`). 2. If that returns `NotImplemented`, call `type(right).__radd__(right, left)`. 3. If that also returns `NotImplemented`, raise `TypeError: unsupported operand type(s) for +: 'Left' and 'Right'`. Every binary arithmetic and bitwise operator has this shape, with its own reflected partner: `__sub__`/`__rsub__`, `__mul__`/`__rmul__`, `__truediv__`/`__rtruediv__`, `__floordiv__`/ `__rfloordiv__`, `__mod__`/`__rmod__`, `__pow__`/`__rpow__`, `__and__`/`__rand__`, `__or__`/`__ror__`, `__xor__`/`__rxor__`, `__lshift__`/`__rlshift__`, `__rshift__`/`__rrshift__`, `__matmul__`/`__rmatmul__`. `NotImplemented` never escapes into your code. It is a signal consumed by the interpreter, which either finds a second route to a real result or converts the double refusal into the `TypeError` you would otherwise have had to write by hand — including the message that names both types and the operator. ### The reflected method sees swapped arguments This is the part people get wrong in a whiteboard implementation. In `__radd__(self, other)`, `self` is the **right-hand** operand and `other` is the left-hand one. Addition is commutative so nobody notices; subtraction is not: ```python def __rsub__(self, other): return Position(other - self.bp) # NOT self.bp - other ``` Get that backwards and `1000 - position` silently returns the negation of the right answer — a bug no type checker catches, because the signature is identical either way. ### Why declining beats raising The reason to return the sentinel instead of raising `TypeError` from inside your own method is interoperability. Your class was written before the other type existed; the other type may well know how to combine itself with yours. Returning `NotImplemented` gives it the chance. Raising ends the expression there, so a perfectly implementable operation becomes an error, and — worse — a caller who wanted to catch a genuine `TypeError` from a bug in your arithmetic cannot tell the two apart. The practical shape inside a dunder is therefore a type guard that *declines* rather than rejects: check `isinstance` (or whatever duck-typing test you prefer), return `NotImplemented` if it fails, and do the arithmetic otherwise. ### Where `__radd__` earns its keep Two everyday cases: * **The builtin sits on the left.** `2 * quantity` cannot work through `type(quantity).__mul__`, because the operand on the left is an `int`. `int.__mul__` returns `NotImplemented` for an unknown type, and `type(quantity).__rmul__` picks it up. * **`sum()` starts from 0.** `sum(values)` begins with the integer `0` and evaluates `0 + values[0]`. Without a `__radd__` that recognises a zero left operand, summing a list of your own type raises `TypeError` on the very first step — the single most common reason a class ends up needing a reflected method at all. ### Two edges that decide interview answers **Same type, no reflection.** If both operands are instances of the same class, CPython does not call the reflected method at all: `Frame() + Frame()` calls `Frame.__add__` once, and a `NotImplemented` return goes straight to `TypeError`. Trying `__radd__` there would only re-run the same code with the arguments swapped. Do not write a class that depends on the reflected call between two of its own instances. **Augmented assignment falls through the same chain.** `x += y` tries `type(x).__iadd__` first; if there is none, or it returns `NotImplemented`, Python falls back to the ordinary `__add__` / `__radd__` pair and rebinds the name to the result. Three chances, one expression. ### The protocol lives in the interpreter, not in the operator Nothing about this is special to the `+` token. `operator.add(a, b)` walks the identical sequence, because the sequence belongs to the abstract object layer that both the bytecode and the function call go through. That has two practical consequences. First, you cannot inspect the outcome yourself by calling `a.__add__(b)` directly and expecting an answer — a direct call returns the raw `NotImplemented` sentinel, since you have bypassed the machinery that would have tried the other side. Debugging sessions that do this and conclude "addition is broken" are common. Second, the comparison against the sentinel is an identity test against a unique singleton, which is why `NotImplemented` cannot collide with any legitimate result your method might return; a `None` or a `False` in that role would be ambiguous the moment some type wanted to return it for real. ### The failure to recognise in a traceback `TypeError: unsupported operand type(s) for +: 'Depth' and 'str'` is not "Python doesn't support that operator". It is the end of the protocol above: both sides were asked and both returned `NotImplemented`. That reframing is what the interviewer is listening for, because it tells you where the fix goes — into whichever of the two types should have known about the other.
- Why does a class that works fine with `+` still fail inside sum()?`sum()` starts from the integer `0`, so the first evaluation is `0 + values[0]`. `int.__add__` does not know your type and returns `NotImplemented`, so the only thing that can rescue the expression is your `__radd__`. Handle a zero left operand there by returning `self`, or pass a suitable `start` value to `sum()`. Without one of those, summing a list of your own type raises TypeError on the very first step.
- What is wrong with raising TypeError from __add__ when the operand type is unknown?It ends the expression immediately, so the right operand's reflected method is never tried and a type that could have handled the combination is silenced. It also makes a deliberate refusal indistinguishable from a genuine TypeError caused by a bug inside your arithmetic. Return `NotImplemented` instead and let the interpreter raise the TypeError, with a message naming both operand types and the operator.
- If both __add__ and __radd__ return NotImplemented, does the sentinel reach the caller?No. The interpreter consumes it and raises `TypeError: unsupported operand type(s) for +: 'Left' and 'Right'`. The sentinel only escapes when you return it from something the operator machinery is not driving — a plain method, for instance — in which case nothing converts it and it travels as an ordinary value.
It is a relay, not a verdict: the left operand can pass the baton to the right one, and only when both drop it does the interpreter call the race off with a TypeError.
saying these in an interview costs you the question
- Believes the expression evaluates to NotImplemented
- Raises TypeError from __add__ instead of returning the sentinel
- Writes __radd__ assuming self is the left operand
- Thinks __radd__ is tried between two instances of one class
- Expects an AttributeError when the right operand lacks __radd__
- Calls the unsupported-operand TypeError a missing-operator error