Operator Overloading
Operator overloading defines what Python's operators mean for your classes: + becomes vector addition, * becomes matrix scaling, < becomes a domain ordering. The mechanism is the same dunder protocol as the previous chapter - a + b calls type(a).__add__(a, b), and if that returns NotImplemented, Python tries the reflected type(b).__radd__(b, a). The craft is choosing the right semantics (value types return new objects, the conventions of __iadd__ and friends), handling the reflected forms, and knowing when an operator is the right interface versus a plain method.
Arithmetic: __add__ and Friends
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x - other.x, self.y - other.y)
def __neg__(self):
return Vector(-self.x, -self.y)
def __mul__(self, k):
if not isinstance(k, (int, float)):
return NotImplemented
return Vector(self.x * k, self.y * k)
def __rmul__(self, k):
# makes 2 * v work: int has no __mul__ for Vector, so the
# reflected form on Vector is tried
return self.__mul__(k)
def __repr__(self):
return f"Vector({self.x!r}, {self.y!r})"
a = Vector(1, 2)
b = Vector(3, 4)
print(a + b) # Vector(4, 6)
print(a - b) # Vector(-2, -2)
print(-a) # Vector(-1, -2)
print(a * 3) # Vector(3, 6)
print(2 * a) # Vector(2, 4) - via __rmul__
The pattern for every binary operator: check the operand's type, return NotImplemented (not False, not an exception) when you do not handle it, and return a new object for value types. The reflected methods (__radd__, __rmul__, ...) exist for operands whose type does not know about yours - 2 * v works because int.__mul__ returns NotImplemented for a Vector and Python falls back to Vector.__rmul__. For commutative operators (+, *), __rmul__ can just delegate to __mul__; for non-commutative ones (-, /, **) the reflected method must actually reverse the operands.
In-Place: __iadd__ and the Assignment Trap
class Stack:
def __init__(self):
self.items = []
def __iadd__(self, item):
# += on a Stack pushes - the in-place operator has domain meaning
self.items.append(item)
return self
def __repr__(self):
return f"Stack({self.items!r})"
s = Stack()
s += 1
s += 2
print(s) # Stack([1, 2])
print(s is s) # True - __iadd__ returned self, no rebind needed
x += y tries __iadd__ first. If your class defines it, you choose what "in-place" means - for a Stack, pushing is the natural reading. If it does not, Python falls back to x = x + y, which rebinds the name. Two traps: (1) defining __iadd__ to return None (forgetting the return self) makes s += 1 silently set s to None; (2) for immutable value types, defining __iadd__ at all is a smell - += will build a new object anyway, and the in-place name promises a mutation that did not happen. The convention: mutable stateful objects may define in-place operators with genuine in-place semantics; value types leave them out.
Comparison and Ordering
from functools import total_ordering
@total_ordering
class Card:
"""Playing cards ordered by rank then suit."""
SUITS = {"c": 0, "d": 1, "h": 2, "s": 3}
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def _key(self):
return (self.rank, self.SUITS[self.suit])
def __eq__(self, other):
if not isinstance(other, Card):
return NotImplemented
return self._key() == other._key()
def __lt__(self, other):
if not isinstance(other, Card):
return NotImplemented
return self._key() < other._key()
a = Card(10, "h")
b = Card(10, "s")
c = Card(9, "s")
print(a == b) # False - same rank, different suit
print(a > b) # True - hearts above spades in our order
print(a > c) # True
print(sorted([b, a, c]))
# [Card(9,s), Card(10,h), Card(10,s)] - sort works via __lt__
Comparison dunders: __eq__, __lt__, __le__, __gt__, __ge__. You do not need all five - functools.total_ordering fills in the missing ones from __eq__ plus one ordering method (__lt__ is the usual choice), which is the standard pattern for ordered value types. The _key method that reduces the object to a comparable tuple is the trick that keeps the comparison logic in one place. As with __eq__, return NotImplemented for foreign types. Note the interaction: defining __eq__ without __hash__ makes the class unhashable (the default), which is correct for mutable orderable objects and wrong for value types you want in sets - define __hash__ when the ordering fields are stable.
The Operator Map
| Operator | Forward | Reflected |
|---|---|---|
a + b | __add__ | __radd__ |
a - b | __sub__ | __rsub__ |
a * b | __mul__ | __rmul__ |
a / b | __truediv__ | __rtruediv__ |
a // b | __floordiv__ | __rfloordiv__ |
a % b | __mod__ | __rmod__ |
a ** b | __pow__ | __rpow__ |
-a | __neg__ | - |
abs(a) | __abs__ | - |
a < b | __lt__ | __gt__ |
a == b | __eq__ | __eq__ (reflected) |
The direction matters for the semantics: a - b is not b - a, so __rsub__ must compute other - self. The NotImplemented return is the handshake that makes mixed-type arithmetic work: each side gets one chance, and if both decline, Python raises TypeError - the exact error you want when someone writes vector + "text".
When an Operator Is the Right Interface
- Use an operator when the meaning is genuinely mathematical or set-like and users will expect
+/*/<to work: vectors, money (carefully), intervals, bitsets, filters. - Use a method when the operation is domain-specific and an operator would be a mnemonic stretch:
account.transfer(to, amt)beatsaccount -> to;query.filter(...)beats inventing an operator for filtering. - Never overload
+to mean "and then" - the abuse that turns code into a private language. If you cannot explain the operator's meaning in one sentence of standard math, it is a method.
Summary
Overload operators by defining the dunders: __add__/__mul__ and the rest of the table, returning NotImplemented for operands you do not handle so the reflected form and the eventual TypeError work as designed. Implement __rmul__-style reflected methods for commutative cases (delegate) and true operand reversal for non-commutative ones. Reserve __iadd__ for genuinely mutable objects with real in-place semantics, and build ordered value types with __eq__ plus __lt__ under @total_ordering. The design test is semantic honesty: operators for things that are mathematically that thing, methods for everything else - the moment an operator becomes a mnemonic, the code has stopped being readable.