Free demoJob-oriented courses at Debugshala, Old Palasia, Indore. Freshers, non-IT and year-gap welcome.Book a free demo +91-8982385539

Python Operator Overloading: Magic Methods __add__ and __eq__

Lesson 42 of 52 · 9 min · Updated 13 Nov 2026

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.

Python
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.

Python
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 operator overloading dispatch: a + b calls a.__add__(b); if it returns NotImplemented, Python calls b.__radd__(a); if that also fails, 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).

Python
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 politely

Quick check

What should __add__ return when the other operand is a type it does not support?

Quick check

Why does sum([Money(10), Money(20)]) need __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.

Python
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.

Python
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.

Python __str__ vs __repr__: print() and f-strings call __str__, which falls back to __repr__; the REPL, repr() and lists of objects always use __repr__

If you define only one, define __repr__: str() falls back to it, but not the other way round.

Python
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 TypeError instead of returning NotImplemented. It blocks Python from trying __radd__ on the other operand.
  • Changing self inside __add__. a + b must not modify a. 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.2 is not exactly 0.3. Store paise as integers or use decimal.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 NotImplemented for unsupported types so __radd__ can run.
  • @total_ordering builds 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.

Take the free Python Programming quiz10 quick questions with explanations. No sign-up needed, and you can come back to this lesson after.

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__?

__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__ 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

Python Polymorphism: Duck Typing, Method Overriding and ABCsPython Dataclasses: @dataclass, field(), frozen and __post_init__