Operator Overloading0%

Operator Overloading

Beginner12 min readUpdated: Jul 11, 2026
Study Materials

Operator Overloading

Operator overloading allows user-defined classes to intercept and define custom semantics for Python's built-in operators—such as arithmetic symbols (+, -, *, /), matrix multiplication (@), augmented assignment (+=, -=), and rich comparison operators (==, <, >=).

Through operator overloading, your custom classes can behave as intuitive, first-class mathematical entities, domain models, or fluent query builders.


1. The Operator Dispatch Architecture & NotImplemented

When Python encounters a binary expression like a + b, it dispatches the operation through a multi-stage protocol:

Output
Expression: a + b
Try: type(a).__add__(a, b)
┌──────────────────┴──────────────────┐
▼ ▼
Returns a Valid Result Returns NotImplemented
(Operation finishes) │
Try: type(b).__radd__(b, a)
┌──────────────────┴──────────────────┐
▼ ▼
Returns a Valid Result Returns NotImplemented
(Operation finishes) │
Raise TypeError:
unsupported operand type(s) for +

The Difference: NotImplemented vs NotImplementedError

Critical Rule: In binary operator dunder methods (__add__, __eq__, __mul__), if your class does not support the type of the operand, you must return the singleton NotImplemented, NOT raise NotImplementedError.
  • Returning NotImplemented: Tells the Python runtime, "I don't know how to handle this operand. Please check the right-hand operand's reflected method (e.g., __radd__) or fallback comparison."
  • Raising NotImplementedError: Immediately terminates the expression with an uncaught exception, preventing the other operand from handling the operation.

2. Arithmetic and Reflected Operators

Arithmetic operators come in two primary flavors: normal (left-hand) and reflected/reverse (right-hand, prefixed with r).

OperatorLeft-hand MethodReflected MethodIn-place Method
+__add__(self, other)__radd__(self, other)__iadd__(self, other)
-__sub__(self, other)__rsub__(self, other)__isub__(self, other)
*__mul__(self, other)__rmul__(self, other)__imul__(self, other)
/__truediv__(self, other)__rtruediv__(self, other)__itruediv__(self, other)
//__floordiv__(self, other)__rfloordiv__(self, other)__ifloordiv__(self, other)
%__mod__(self, other)__rmod__(self, other)__imod__(self, other)
**__pow__(self, other)__rpow__(self, other)__ipow__(self, other)
@__matmul__(self, other)__rmatmul__(self, other)__imatmul__(self, other)

Implementing Arithmetic with Scalars and Objects

Visual Architecture & Process Flow

How data and code flow step-by-step

Flowchart
Step 1
def __init__
self, amount: float, currency: str = "USD"
Step 2
None:

3. In-Place Augmented Assignment (+=, -=, *=)

When you define methods like __iadd__, Python invokes them during a += b.

  • Mutable objects (like list): __iadd__ modifies self in-place and returns self.
  • Immutable objects (like int, str): If __iadd__ is omitted, Python falls back to a = a + b, allocating a new instance.
Python
class MutableVector:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
 
def __iadd__(self, other: "MutableVector") -> "MutableVector":
"""Mutates the existing vector in place."""
if not isinstance(other, MutableVector):
return NotImplemented
self.x += other.x
self.y += other.y
return self # CRITICAL: Always return self for in-place methods
 
def __repr__(self) -> str:
return f"MutableVector({self.x}, {self.y})"
 
vec = MutableVector(10, 20)
old_id = id(vec)
vec += MutableVector(5, 5)
 
print("Updated vector:", vec)
print(f"Preserved exact memory identity: {id(vec) == old_id}") # True

4. Rich Comparison Operators and total_ordering

Python defines 6 rich comparison dunder methods:

ComparisonMethod
==__eq__(self, other)
!=__ne__(self, other)
<__lt__(self, other)
<=__le__(self, other)
>__gt__(self, other)
>=__ge__(self, other)

Instead of manually implementing all 6 operators, the standard library provides @functools.total_ordering. You only need to define __eq__ and one ordering method (__lt__, __le__, __gt__, or __ge__), and Python automatically synthesizes the remaining 4 operators.

Visual Architecture & Process Flow

How data and code flow step-by-step

Flowchart
Step 1
def __init__
self, hostname: str, cpu_cores: int, ram_gb: int
Step 2
None:

5. Summary and Architectural Rules

  1. 1
    Always return NotImplemented on unknown operand types: This allows Python to query the other operand or produce clear, standardized error messages.
  2. 2
    In-place methods must return self: Failing to return self from __iadd__ will cause variables using += to silently become None.
  3. 3
    Use total_ordering cautiously in performance-critical paths: While @functools.total_ordering saves boilerplate, explicit implementations of comparison methods execute slightly faster by avoiding dynamic method wrapper lookups.

Multiple Choice Questions

1.

What should a binary operator dunder method like __add__(self, other) return when it encounters an unsupported type for other? A. raise TypeError B. raise NotImplementedError C. return NotImplemented D. return None

Answer: C
Explanation:Returning the singleton NotImplemented signals the Python runtime to attempt the operation using the right-hand operand's reflected method (__radd__). Raising an error immediately halts evaluation.

2.

Given the expression result = 10 + custom_obj, where 10 is an int that does not know how to add custom_obj, which method is invoked on custom_obj? A. custom_obj.__add__(10) B. custom_obj.__radd__(10) C. custom_obj.__iadd__(10) D. custom_obj.__call__(10)

Answer: B
Explanation:When the left operand fails or returns NotImplemented, Python attempts the reflected (reverse) operator method on the right operand, which is __radd__.

3.

What critical value must an in-place operator like __iadd__(self, other) return when mutating a mutable object? A. None B. True C. self D. The previous value of self

Answer: C
Explanation:In Python, the statement a += b assigns the return value of __iadd__ back to a. If __iadd__ returns None (or omits a return statement), a is reassigned to None.

4.

Which standard library decorator allows a class defining only __eq__ and __lt__ to automatically support all six rich comparison operators (<=, >, >=, !=)? A. @functools.lru_cache B. @functools.total_ordering C. @dataclasses.dataclass D. @operator.rich_comparison

Answer: B
Explanation:@functools.total_ordering takes a class that defines __eq__ and any one of __lt__, __le__, __gt__, or __ge__, and programmatically supplies the remaining comparison methods.

5.

Which operator corresponds to the special method __matmul__(self, other) introduced in Python 3.5? A. Regular matrix exponentiation (**) B. Matrix multiplication (@) C. Bitwise AND (&) D. Decorator application (@property)

Answer: B
Explanation:The @ binary operator was introduced in Python 3.5 (PEP 465) specifically for matrix multiplication and corresponds to the __matmul__ and __rmatmul__ dunder methods.

Next Lesson

Customizing Classes with Magic Methods

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext