Operator Overloading in Python
Operator overloading lets your custom objects work with Python’s built-in operators. When you use + between two objects, Python calls the first object’s add method. When you compare objects with ==, Python calls eq. By defining these special methods, you control how operators behave with your types.
How Operator Overloading Works
Python operators are syntactic sugar for method calls. When you write a + b, Python essentially runs a.add(b). If that method returns NotImplemented, Python tries the reflected operation on the right operand: b.radd(a).
This design lets you define meaningful behavior for operators without changing how the language works:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
Arithmetic Operators
Python defines methods for all arithmetic operators:
| Operator | Method | Description |
|---|---|---|
| + | add | Addition |
| - | sub | Subtraction |
| * | mul | Multiplication |
| / | truediv | Division |
| // | floordiv | Floor division |
| % | mod | Modulo |
| ** | pow | Exponentiation |
| @ | matmul | Matrix multiplication |
Here’s a number class that handles multiplication:
class Money:
def __init__(self, amount):
self.amount = amount
def __mul__(self, multiplier):
return Money(self.amount * multiplier)
def __rmul__(self, multiplier):
return Money(self.amount * multiplier)
def __repr__(self):
return f"${self.amount}"
price = Money(50)
print(price * 3) # $150
print(3 * price) # $150 — uses __rmul__
The reflected rmul method handles cases where the left operand doesn’t support the operation.
Comparison Operators
Comparison operators let your objects work with ==, !=, <, >, <=, >=:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __eq__(self, other):
return self.celsius == other.celsius
def __lt__(self, other):
return self.celsius < other.celsius
def __le__(self, other):
return self == other or self < other
def __gt__(self, other):
return not self <= other
def __ge__(self, other):
return not self < other
t1 = Temperature(20)
t2 = Temperature(25)
print(t1 < t2) # True
print(t1 == Temperature(20)) # True
Python automatically derives != from eq (and > from <=).
Boolean and Truthiness
Define how your object behaves in boolean contexts with bool:
class EmptyBuffer:
def __init__(self, data=None):
self.data = data or []
def __bool__(self):
return len(self.data) > 0
buffer = EmptyBuffer()
print(bool(buffer)) # False
buffer.data = [1, 2, 3]
print(bool(buffer)) # True
If you don’t define bool, Python falls back to len (truthy if non-zero).
Common Pitfalls
Returning the wrong type: Methods should return new objects, not modify existing ones:
# Wrong
def __add__(self, other):
self.x += other.x # Modifies self!
return self
# Correct
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
Forgetting NotImplemented: When you can’t handle an operation, return NotImplemented (not NotImplementedError):
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented # Try other.__radd__(self)
return Vector(self.x + other.x, self.y + other.y)
This lets Python try the reflected operation on the other object.
Bitwise Operators
You can also overload bitwise operators:
| Operator | Method | Description |
|---|---|---|
| & | and | Bitwise AND |
| or | ||
| ^ | xor | Bitwise XOR |
| ~ | invert | Bitwise NOT |
| << | lshift | Left shift |
| >> | rshift | Right shift |
class Permission:
def __init__(self, bits):
self.bits = bits
def __or__(self, other):
return Permission(self.bits | other.bits)
def __and__(self, other):
return Permission(self.bits & other.bits)
def __bool__(self):
return self.bits > 0
def __repr__(self):
perms = []
if self.bits & 1: perms.append("read")
if self.bits & 2: perms.append("write")
if self.bits & 4: perms.append("execute")
return f"Permission({', '.join(perms) or 'none'})"
read = Permission(1)
write = Permission(2)
execute = Permission(4)
print(read | write) # Permission(read, write)
print(read & write) # Permission(none)
When to Use Operator Overloading
Operator overloading shines when building domain-specific types that represent mathematical or logical concepts:
- Numeric types: Vector, Matrix, Complex, Fraction, Money
- Collections with ordering: Range, Interval, Version
- Set-like types: Bag, Multiset, BitSet
- Physical quantities: Distance, Temperature, Time
Avoid overloading operators when the meaning would be confusing.
Summary
Operator overloading makes your classes feel native to Python. Define special methods like add, eq, lt, and bool to control how operators behave with your objects. Return NotImplemented when you can’t handle an operation, and always return new objects rather than mutating existing ones.
See Also
- Python Classes and Objects Tutorial - Object-oriented programming fundamentals
- Python Dataclasses - simplified class definitions with automatic operator support
- Python Operator Module - operator module functions