Why should the __lt__ you pair with functools.total_ordering return NotImplemented?
answer
- It is a return value, not an exception
- Gives the other operand a turn
- Ends in a clear TypeError, not a lie
- The generated operators pass it straight through
- A False answer makes the derived > lie
basics
~10 sReturning NotImplemented for an operand type you do not handle lets Python try the other operand's reflected method and then raise a clear TypeError. Returning False instead answers a question you cannot answer, silently.
solid answer
~50 s`NotImplemented` is the binary-operator protocol's way of saying *"I do not know how to compare with this operand"*. When `__lt__` returns it, the interpreter tries the right operand's reflected `__gt__`, and if that also declines, raises `TypeError: '<' not supported between instances of ...`. Returning `False` short-circuits all of that, and under `functools.total_ordering` it is worse than a merely wrong `<`: the generated `__gt__` means *not less and not equal*, so a `__lt__` answering `False` for an unrelated operand makes the object claim it is **greater** than a value it cannot compare with, and `>=` answer `True` too. The generated operators exist to pass `NotImplemented` through; feed them a boolean and every one of them inherits the lie, silently and far from the cause. Note `NotImplemented` is a singleton value returned, not `NotImplementedError`, which is an exception you raise.
code
python · 26 linesfrom functools import total_ordering
@total_ordering
class Meters:
def __init__(self, v):
self.v = v
def __eq__(self, other):
return self.v == other.v if isinstance(other, Meters) else NotImplemented
def __lt__(self, other):
return self.v < other.v if isinstance(other, Meters) else NotImplemented
try:
Meters(1) < 5
except TypeError as exc:
print("TypeError:", exc)
print(Meters(1) == 5)
try:
Meters(1) >= 5
except TypeError as exc:
print("the derived >= declines too:", exc)go deeper
Recall that NotImplemented is returned, never raised, and that it is what you give back when the other operand is a type your comparison does not handle. Knowing the guard-clause shape is enough at this level.
Explain the two-stage dispatch: your method declines, the reflected method on the other operand is tried, and only then does TypeError appear. Be able to say why a False-returning lt makes the derived > answer True.
Show why this is a production concern. A comparison that lies never raises; it corrupts ordering and threshold logic far from the cause, so argue for declining loudly and for reviewing comparison guards as carefully as any other input validation.
Own the API-design angle: what type pairs your value objects agree to compare with, whether coercion belongs in the operator or at the call site, and how subclasses are allowed to widen those rules without the base class knowing about them.
## The binary-operator protocol Every binary operator in Python is a two-stage negotiation. For `x < y` the interpreter calls `type(x).__lt__(x, y)`. If that returns the singleton `NotImplemented`, the interpreter does **not** give up: it tries the reflected operation on the right-hand operand, `type(y).__gt__(y, x)`. Only if that also returns `NotImplemented` does it raise `TypeError: '<' not supported between instances of 'A' and 'B'`. (One wrinkle: if `type(y)` is a proper subclass of `type(x)` and overrides the reflected method, Python tries the subclass first, so a subclass can extend the comparison rules of its base.) `NotImplemented` is therefore not an error and not a failure — it is a **return value that means "pass"**. Your method declines, and the language's dispatch machinery gets a chance to find another implementation before anyone concludes the operation is impossible. That is what makes mixed-type arithmetic and comparison extensible without every class having to know about every other class. Two confusions are worth naming. `NotImplemented` is a built-in singleton you `return`; `NotImplementedError` is an exception you `raise`, and it means "this abstract method has no body yet". Returning the exception class, or raising the singleton, are both broken. And `NotImplemented` is truthy, so `if x.__lt__(y):` on a declined comparison takes the true branch — which is exactly why you should never call a dunder directly and test the result. ## Why False is the wrong answer The tempting alternative is a guard clause that answers `False` for anything unfamiliar: ```python def __lt__(self, other): if not isinstance(other, Meters): return False # wrong return self.v < other.v ``` Now `Meters(1) < 5` is `False`. A concrete boolean ends the dispatch immediately: the reflected operation is never tried, and the `TypeError` that should have fired never does. Under `functools.total_ordering` it gets actively dangerous, because the generated `__gt__` means *not less and not equal* and the generated `__ge__` means *not less* — so `Meters(1) > 5` and `Meters(1) >= 5` both come back `True`. The class has not merely failed to compare; it has asserted that a length is greater than the number five. No exception is raised, nothing is logged, and the wrong branch is taken somewhere downstream. The comparison that *should* have blown up at the point of the type confusion instead corrupts a sort or a threshold check, and the traceback you eventually get points nowhere near the cause. The same reasoning applies to `__eq__`. Returning `NotImplemented` from `__eq__` for an unknown type is safe precisely because equality has a defined last resort: when both sides decline, Python falls back to identity, so `Meters(1) == 5` is `False` — correct, and reached honestly. Ordering has no such fallback, which is why declining an ordering comparison ends in `TypeError`. That asymmetry is the point: `==` between unrelated types is a meaningful question with the answer "no"; `<` between unrelated types is a meaningless question, and Python is right to refuse it. ## The interaction with total_ordering `functools.total_ordering` builds its three generated operators on top of the one you wrote, and since Python 3.4 each generated method checks the result first: ```python def __ge__(self, other): result = type(self).__lt__(self, other) if result is NotImplemented: return result # propagate, don't coerce return not result ``` So a well-behaved `__lt__` gives you four well-behaved operators: pass an unrelated type to any of them and the whole chain declines, the reflected operation gets its turn, and you get a `TypeError` naming both types. A `__lt__` that returns `False` instead poisons all four at once, and the derived `__ge__` — which is just `not result` — flips it into a confident `True`, claiming your object is greater than or equal to a value it cannot compare with at all. ## Getting it right The shape is always the same: narrow the operand, decline anything else. ```python def __lt__(self, other): if not isinstance(other, Meters): return NotImplemented return self.v < other.v ``` Use `isinstance` rather than an exact `type(other) is Meters` check so subclasses keep working, and prefer declining to coercing: if a caller genuinely wants to compare a `Meters` with a plain number, make them say so at the call site instead of guessing on their behalf. When you want the comparison to be truly type-agnostic, use duck typing on the attribute you need and let the resulting `AttributeError` be caught and turned into `NotImplemented` — but never turn an unanswerable question into a boolean.
- Why is returning NotImplemented from __eq__ safe when returning it from __lt__ ends in a TypeError?Equality has a last resort and ordering does not. When both operands decline `==`, Python falls back to identity comparison, so the result is `False` — a meaningful answer. For `<` there is no sensible default, so once both sides decline, the interpreter raises `TypeError` naming both types. That asymmetry is deliberate: `==` across unrelated types is answerable; `<` is not.
- What is the difference between NotImplemented and NotImplementedError?`NotImplemented` is a built-in singleton value you *return* from a binary special method to say "try someone else"; `NotImplementedError` is an exception you *raise* to say "this method has no implementation yet", typically in an abstract base class. Returning the exception, or raising the singleton, breaks the operator protocol — and because `NotImplemented` is truthy, mistaking it for a boolean silently takes the true branch.
- How does Python decide whether to call the left operand's method or the right operand's reflected method first?Normally the left operand goes first. The exception is when the right operand's type is a proper subclass of the left operand's type *and* overrides the reflected method — then Python tries the subclass first, so a subclass can refine comparisons against its own base without the base knowing about it.
A witness who says "I don't know" lets the court call the next witness; a witness who guesses "no" closes the question with an answer nobody can challenge.
saying these in an interview costs you the question
- Returning False for operand types you do not handle
- Raising NotImplementedError instead of returning NotImplemented
- Thinking NotImplemented is falsy
- Believing the interpreter stops at the first NotImplemented
- Using type(other) is C instead of isinstance, breaking subclasses
- Thinking a False-returning __lt__ only makes < wrong