Python Operator Overloading: Magic Methods __add__ and __eq__
Learn Python operator overloading with magic methods: __add__, __radd__, __eq__, __lt__, __len__, __getitem__, __str__ and __repr__, using a Money class.
Picture the foreign exchange counter at Indore airport. A cashier adds ₹500 and ₹200 without thinking: ₹700. Adding $20 and $30 is just as easy: $50. But if you ask her to add ₹500 and $20, she stops. Those cannot be added until one is converted.
So "add" means something specific for money. It is not the same as adding plain numbers, and it is certainly not joining two pieces of text. The cashier knows the rules for her kind of thing.
Python works the same way. + adds integers, joins strings and concatenates lists, because each of those types defines what + means for itself. When you write a Money class, you can teach it the cashier's rules: same currency adds, different currencies refuse.
Operator overloading in Python is defining special methods, such as __add__, __eq__ or __len__, in your class so that built-in operators and functions work on its objects with a meaning you choose.
This lesson continues from Python polymorphism, where you saw that + and len() behave differently by type. Examples target Python 3.12+.
How does operator overloading work in Python?
Every operator is a call in disguise. When Python sees a + b, it runs type(a).__add__(a, b). Built-in types already have these methods, which is why + works on them. A class without __add__ gives a TypeError.
print((1450).__add__(980)) # what 1450 + 980 runs
print("Old ".__add__("Palasia")) # what "Old " + "Palasia" runs
print([1, 2].__add__([3]))
class Target:
def __init__(self, amount):
self.amount = amount
try:
Target(50000) + Target(35000)
except TypeError:
print("TypeError: Target does not define __add__")These double-underscore methods are called magic methods or dunder methods ("double underscore"). You define them; Python calls them. You almost never call obj.__add__() yourself.
Overloading + and - with __add__ and __sub__
Here is a small Money class that follows the forex cashier's rule. It stores amounts as integer paise to avoid floating-point rounding, which matters whenever you total real money.
class Money:
def __init__(self, rupees, currency="INR"):
self.paise = round(rupees * 100)
self.currency = currency
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if other.currency != self.currency:
raise ValueError(f"cannot add {self.currency} and {other.currency}")
return Money((self.paise + other.paise) / 100, self.currency)
def __sub__(self, other):
if not isinstance(other, Money) or other.currency != self.currency:
return NotImplemented
return Money((self.paise - other.paise) / 100, self.currency)
def __repr__(self):
return f"Money({self.paise / 100:.2f}, {self.currency!r})"
print(Money(1450.50) + Money(980.25))
print(Money(2000) - Money(450.75))
try:
Money(500) + Money(20, "USD")
except ValueError as err:
print("Refused:", err)Two details matter. First, __add__ returns a new object and leaves both operands unchanged, just as 2 + 3 does not change 2. Second, for a type it does not understand, it returns the special value NotImplemented instead of raising an error.
Why return NotImplemented? __radd__ and sum()
NotImplemented tells Python "I cannot do this; ask the other operand". Python then tries the reflected method on the right-hand object, such as __radd__. Only if both sides give up does it raise TypeError.
Python tries the left operand first, then the reflected method on the right, then gives up.
This is what makes sum() work. sum() starts from 0 and computes 0 + first_item. The integer 0 does not know about Money, so int.__add__ returns NotImplemented and Python calls Money.__radd__(0).
class Money:
def __init__(self, rupees):
self.paise = round(rupees * 100)
def __add__(self, other):
if isinstance(other, Money):
return Money((self.paise + other.paise) / 100)
return NotImplemented
def __radd__(self, other):
if other == 0: # sum() starts from 0
return self
return self.__add__(other)
def __repr__(self):
return f"Money({self.paise / 100:.2f})"
daily = [Money(12500), Money(8400.50), Money(15100.25)]
print(sum(daily))
print((5).__add__(Money(1))) # int gives up politelyQuick check
What should __add__ return when the other operand is a type it does not support?
Returning NotImplemented lets Python try the reflected method on the other operand, such as __radd__.
Quick check
Why does sum([Money(10), Money(20)]) need __radd__?
int.__add__ cannot handle Money, so Python asks the right operand through __radd__.
Comparison operators: __eq__, __lt__ and total_ordering
Without __eq__, two objects are equal only if they are the very same object. That is rarely what you want for values. Define __eq__ to compare contents, and __lt__ so that sorted() and min() work.
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, rupees):
self.paise = round(rupees * 100)
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.paise == other.paise
def __lt__(self, other):
if not isinstance(other, Money):
return NotImplemented
return self.paise < other.paise
def __hash__(self):
return hash(self.paise)
def __repr__(self):
return f"Money({self.paise / 100:.2f})"
bills = [Money(1450), Money(980.5), Money(2300), Money(980.5)]
print(Money(980.5) == Money(980.50), Money(1450) >= Money(980.5))
print(sorted(bills))
print(max(bills), len(set(bills)))@total_ordering fills in <=, > and >= from __eq__ and __lt__. Note the __hash__ method: defining __eq__ switches off the default hash, so objects could no longer go into a set or be dict keys.
If the object is immutable in practice, give it a hash based on the same fields. Sorting with keys instead of operators was covered in Python sorting.
Making an object behave like a container
Magic methods are not only for arithmetic. __len__, __getitem__, __contains__ and __bool__ let a class act like a list or dict, so len(), indexing, in and if work naturally.
class SalesLedger:
def __init__(self, entries):
self.entries = list(entries) # (city, amount) pairs
def __len__(self):
return len(self.entries)
def __getitem__(self, index):
return self.entries[index]
def __contains__(self, city):
return any(c == city for c, _ in self.entries)
ledger = SalesLedger([("Indore", 48200), ("Ujjain", 18900), ("Dewas", 7400)])
print(len(ledger), ledger[0], ledger[-1])
print("Ujjain" in ledger, "Bhopal" in ledger)
print(ledger[1:]) # slices pass through too
print(bool(SalesLedger([])), bool(ledger)) # falls back to __len__
for city, amount in ledger: # __getitem__ makes it iterable
print(f" {city:<7}{amount:>7}")The for loop works because Python falls back to calling __getitem__ with 0, 1, 2 and so on until IndexError. The modern way to make a class iterable is __iter__, covered in Python iterators.
__str__ vs __repr__ in Python
Both turn an object into text, for different readers. __repr__ is for developers: unambiguous, ideally looking like the code that would recreate the object. __str__ is for end users: readable, used by print() and f-strings.
If you define only one, define __repr__: str() falls back to it, but not the other way round.
class Money:
def __init__(self, rupees):
self.rupees = rupees
def __repr__(self):
return f"Money({self.rupees!r})"
def __str__(self):
return f"Rs {self.rupees:,.2f}"
m = Money(148250.5)
print(m) # __str__
print(f"Total: {m}") # __str__
print(repr(m)) # __repr__
print([m, Money(99)]) # lists show each item's __repr__Common magic methods at a glance
| You write | Python calls | Typical use |
|---|---|---|
a + b, a - b, a * b |
__add__, __sub__, __mul__ |
Money, vectors, durations |
5 + a |
__radd__ (after int gives up) |
Making sum() work |
a += b |
__iadd__, else __add__ |
In-place update |
a == b, a < b |
__eq__, __lt__ |
Comparing and sorting |
len(a), a[i], x in a |
__len__, __getitem__, __contains__ |
Container classes |
if a: |
__bool__, else __len__ |
Truth testing |
print(a), repr(a) |
__str__, __repr__ |
Readable output |
When should you overload an operator?
Overload an operator only when its meaning for your class would be obvious to a stranger. Adding two Money values, comparing two dates or checking "Indore" in ledger all pass that test. If you have to explain what * does for your class, write a normal method with a descriptive name instead, such as apply_discount().
The full list is in the Python data model reference.
Common mistakes with operator overloading
- Raising
TypeErrorinstead of returningNotImplemented. It blocks Python from trying__radd__on the other operand. - Changing
selfinside__add__.a + bmust not modifya. Return a new object. - Defining
__eq__and losing hashing. Add__hash__if objects belong in sets or dict keys, and only if they will not change. - Surprising meanings. Using
-to delete a row or*to send an email confuses every reader. Keep operators intuitive. - Using floats for money.
0.1 + 0.2is not exactly0.3. Store paise as integers or usedecimal.Decimal.
Key takeaways
- Operators call magic methods:
+is__add__,==is__eq__,len()is__len__. - Return a new object from arithmetic methods; never modify the operands.
- Return
NotImplementedfor unsupported types so__radd__can run. @total_orderingbuilds the remaining comparisons from__eq__and__lt__.__len__,__getitem__and__contains__make container-like classes.- Define
__repr__always, and__str__when users need friendlier text.
Most of this boilerplate can be generated for you, which is exactly what Python dataclasses do next. Building small domain classes like Money for finance datasets is the kind of exercise practised in the Python-for-finance-data sessions of Debugshala's analytics course.
FAQ
What is operator overloading in Python?
Operator overloading means giving operators like +, == or < a meaning for your own classes. You do it by defining special methods such as __add__ or __eq__. When Python evaluates a + b, it calls a.__add__(b), so your method decides what adding two of your objects means.
What are magic methods in Python?
Magic methods, also called dunder methods, are special methods whose names start and end with double underscores, such as __init__, __len__ and __repr__. Python calls them automatically in response to syntax or built-in functions. Defining them lets your objects work with operators, len(), print(), loops and comparisons.
What is the difference between __str__ and __repr__?
__str__ and __repr__?__repr__ returns an unambiguous text form for developers, ideally like the code that recreates the object, and is used by repr() and inside lists. __str__ returns a friendly form for users and is used by print() and f-strings. If __str__ is missing, Python falls back to __repr__.
What is __radd__ in Python?
__radd__ in Python?__radd__ is the reflected addition method. Python calls b.__radd__(a) for a + b when a.__add__(b) is missing or returns NotImplemented. It matters most for sum(), which starts from 0, so 0 + your_object needs __radd__ to succeed.
Where can I practise Python magic methods questions?
Interviewers like asking what __eq__ does to hashing or why NotImplemented exists. Re-type the Money class without looking, then check your dunder method knowledge on Debugshala Test. Debugshala's Indore campus in Old Palasia offers a free demo class before you join its data analytics course.
From the team behind this site
Go further with the Data Analytics Course at Debugshala
These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.
- Offline classes at Old Palasia, Indore
- Free demo class before you pay
- Non-IT, fresher and year-gap friendly
- Placement assistance
700+ students placed · 200+ hiring partners · 4.9★ Google rating