A fundamental rule of Python operator overloading: Infix operators like + (__add__) and * (__mul__) must never mutate their operands. They must always read from the operands and return a new object.
When Python evaluates a + b, it follows this exact sequence:
- Call
a.__add__(b). - If
__add__raises an exception, crash. - If
__add__returnsNotImplemented, or ifahas no__add__method, Python callsb.__radd__(a). - If
__radd__returnsNotImplemented, or ifbhas no__radd__method, Python raises aTypeError: unsupported operand type(s) for +.
This mechanism is what allows (1, 2) + MyVector([3, 4]) to work, even though the built-in tuple class has no idea what MyVector is.
NotImplementedis a singleton value (likeNone). You return it from dunder operator methods to tell Python's execution engine to try the reverse fallback.NotImplementedErroris an exception. You raise it inside abstract base classes to force subclasses to override a method. If you raise it inside an operator method, it breaks the dispatch chain and crashes the program.
Augmented assignment operators like += look for the in-place method __iadd__.
- If
__iadd__exists, Python evaluatesa = a.__iadd__(b). The method should mutateain place, and it mustreturn self. - If
__iadd__does not exist, Python falls back toa = a.__add__(b). This evaluates the expression and binds the variableato the brand new object returned by__add__.