Skip to main content

Operator Overloading

Source: Fluent Python, 2nd Edition — Chapter 16 by Luciano Ramalho

Gosling left overloading out of Java after watching C++ abuse it. Python keeps it, with fences: you cannot change int + int, cannot invent ***, cannot overload is / and / or / not (bitwise & | ~ are fine). Used well, pathlib and Scapy read like English.

Chapter 1’s toy __add__ / __mul__ and Chapter 11’s Vector2d == [3, 4] were too loose. This chapter is how to refuse an operand (NotImplemented, not TypeError), mixed types, rich comparisons, and +=.

Unary operators​

__neg__, __pos__, __invert__, and abs (__abs__). One argument: self. Always return a new object. -v is a new vector of negated components; +v is usually a copy. abs is a scalar. Skip ~ unless bits or an ORM WHERE clause.

Almost always x == +x. Exceptions: decimal.Decimal after context precision drops (+x rebuilds with the new precision); +Counter drops non-positive counts.

Infix +: pad, reverse, NotImplemented​

Vector addition is pairwise; unequal length pads with 0 via itertools.zip_longest. Return a new vector. Sequences in the data model use + for concat; here + is math on purpose.

a + b: try a.__add__(b); if missing or NotImplemented, try b.__radd__(a); else TypeError. The r is reversed/right. Commutative + can delegate __radd__ to self + other. Concatenation is not commutative.

Catch TypeError from zip_longest or float + str and return NotImplemented so the other type gets a turn. Raising TypeError aborts dispatch. NotImplemented is a sentinel; NotImplementedError is an exception for ABC stubs.

Scalar * and @​

v * 10 is elementwise. float(scalar) accepts int, bool, Fraction; complex fails → NotImplemented. Avoid numbers.Real if you also want static checkers.

@ is the dot/matrix operator (PEP 465): __matmul__. If both operands are abc.Sized and abc.Iterable and equal length, sum(a * b for a, b in zip(...)). Else ValueError or NotImplemented. Goose typing: a list works; you need not subclass those ABCs. __rmatmul__ can be self @ other.

Infix names follow the same pattern: __sub__ / __rsub__ / __isub__, /, //, %, **, &, |, ^, shifts.

Rich comparisons​

Same methods run forward and reverse, arguments swapped: a > b tries a.__gt__(b) then b.__lt__(a). == / != never raise: last resort is id. Python 3 != is not (a == b) unless __eq__ returned NotImplemented. Ordering of mixed nonsense types raises TypeError (Python 2 invented garbage).

[1, 2] == (1, 2) is False. A Vector that equals any numeric iterable is too cute. If other is a Vector (or subclass), compare lengths and zip; else NotImplemented so Vector2d.__eq__ (tuple both sides) can still say True, while tuple.__eq__ falls through to identity vs a Vector.

You often omit __ne__.

Augmented assignment​

Without __iadd__, a += b is a = a + b: new object, fine for immutables. Never implement in-place methods on an immutable Vector.

On a mutable cage, __add__ builds a new cage from two Tombola-like pools (strict: same type). __iadd__ loads self from another cage or any iterable (like list += x vs list + x), then return self. No __radd__ if forward + only accepts the same type.

+ XYZ treats XYZ as three characters via list(right.strip()). += M N splits on spaces.

Duck typing is flexible; goose typing (isinstance vs ABCs) keeps operator errors from turning into nonsense. pathlib uses / for paths; that is the good kind of overload.