Operator Overloading in Python: Dunder Methods Like __add__ and __eq__
- Operators like
+,==, and<are shorthand for calls to dunder methods such as__add__,__eq__, and__lt__. - Defining these methods on your own class lets instances work with the built-in operators instead of requiring named methods for every operation.
__repr__is for developers and debugging;__str__is for readable output shown to users.- Returning
NotImplemented(not raising an error) from a dunder method lets Python fall back to the other operand's reflected method.
What dunder methods are
"Dunder" is short for "double underscore" — methods named like __init__ or __add__, with two underscores on each side. Python calls these methods automatically in response to syntax and built-in functions, rather than you calling them by name. When you write a + b, Python is really evaluating type(a).__add__(a, b). This is why + works differently for numbers, strings, and lists — each type provides its own __add__ that defines what addition means for it.
Defining these methods on your own classes is called operator overloading: giving existing operators new meaning for a new type.
Arithmetic operators: __add__ and friends
Suppose you have a small class representing a length measured in the same unit. Adding two lengths together should produce a new length:
class Length:
def __init__(self, meters):
self.meters = meters
def __add__(self, other):
return Length(self.meters + other.meters)
def __repr__(self):
return f"Length({self.meters}m)"
a = Length(3)
b = Length(4)
print(a + b) # Length(7m)
The same pattern applies to other operators: __sub__ for -, __mul__ for *, __truediv__ for /, and so on. You only implement the ones that make sense for your type — multiplying two Length objects together does not have an obvious meaning, so a Length class might reasonably leave __mul__ undefined.
Comparisons: __eq__ and __lt__
Without a custom __eq__, Python's default equality check compares identity — whether two variables point to the exact same object, not whether their contents match:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # False -- default __eq__ compares identity
Defining __eq__ fixes this by comparing the attributes that actually matter for equality:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
Ordering comparisons work the same way through __lt__, __le__, __gt__, and __ge__. Rather than implementing all four by hand, the functools.total_ordering decorator can fill in the rest once you supply __eq__ and one of the ordering methods.
__repr__ vs __str__
Both control how an object is turned into text, but for different audiences. __repr__ should produce an unambiguous, ideally code-like representation useful for debugging and logging; __str__ should produce something readable, aimed at an end user reading normal program output:
class Money:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f"Money(amount={self.amount})"
def __str__(self):
return f"${self.amount:.2f}"
m = Money(9.5)
print(str(m)) # $9.50
print(repr(m)) # Money(amount=9.5)
print(m) # $9.50 -- print() uses __str__
If you only define __repr__, Python falls back to it for str() as well, so it is common to implement __repr__ on every class and add __str__ only when a friendlier display format is genuinely useful.
Reflected methods and NotImplemented
When Python evaluates a + b and a's __add__ does not know how to handle b's type, it should return the special value NotImplemented rather than raising an exception. Python then tries b's reflected method, __radd__, giving the other operand a chance to handle the operation:
class Length:
def __init__(self, meters):
self.meters = meters
def __add__(self, other):
if not isinstance(other, Length):
return NotImplemented
return Length(self.meters + other.meters)
If both __add__ and the reflected __radd__ return NotImplemented, Python raises a clear TypeError on your behalf, rather than your code raising a confusing error partway through an operation it should never have attempted.
When operator overloading helps — and when it hurts
Operator overloading earns its keep for types that model mathematical or naturally comparable values — vectors, money, durations, custom numeric types. It becomes a liability when the operator's meaning is not obvious: overloading + on a class to mean something unrelated to combination or addition will confuse anyone reading the code later, including a future version of yourself. A useful test before implementing an operator: would a reader unfamiliar with your class correctly guess what the operator does just from ordinary Python conventions? If not, a plainly named method is the better choice.
It is also worth being deliberate about which dunder methods you implement together. Defining __eq__ without also giving thought to __hash__ can produce a class that compares as equal to another instance yet hashes differently — a combination that breaks the assumption dictionaries and sets rely on, since Python's data model states that objects which compare equal should also produce the same hash. By default, defining a custom __eq__ on a class sets its __hash__ to None, making instances unhashable, which is usually the safest default unless you have specifically thought through what hashing that class should mean.