Learn Python operator overloading with __add__, __sub__, __mul__, __eq__, and reverse operators. Build a Vector class that supports arithmetic, comparison, and string operations using magic methods.
“Simple things should be simple, complex things should be possible.”
Alan Kay, ACM Queue
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 11 minutes
Writing v1.add(v2) feels clunky when you could just write v1 + v2. Python operator overloading lets you decide what +, -, *, ==, <, and [] mean for your own objects. Under the hood, a + b is really a quiet method call: Python runs a.__add__(b) for you. These are the same magic methods from the magic methods tutorial, now wired up to operators instead of functions.
Think of operators as universal symbols, like the buttons on a calculator. Everyone already knows what the plus button does. Operator overloading just teaches Python what that same button should do when you press it on a Vector, a Money amount, or a shopping cart. That is why libraries like NumPy feel so natural: you write array1 + array2 instead of array1.add(array2), and the code reads like math instead of a pile of method calls. This post shows you how to add arithmetic, comparison, and container operators to your own classes, with every example tested and the rules for getting it right.
Table of Contents
Building a Vector Class: The Running Example
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Read this diagram as a translation table. Each operator on the left has a matching magic method on the right: + calls __add__, == calls __eq__, len() calls __len__, and the same pattern holds for the rest of the arithmetic, comparison, and reverse operations. That single mapping is the whole trick. Define the method, and your class works with Python’s built-in operators. The Vector class we build through this post implements several of these, so glance back here whenever a new magic method name shows up.
📄 vector.py: full operator overloading example
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# Arithmetic
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented
def __rmul__(self, scalar): # Handles: 3 * vector
return self.__mul__(scalar)
def __neg__(self):
return Vector(-self.x, -self.y)
def __abs__(self):
return math.sqrt(self.x ** 2 + self.y ** 2)
# Comparison
def __eq__(self, other):
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return NotImplemented
# Representation
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"3 * v1 = {3 * v1}") # Uses __rmul__
print(f"-v1 = {-v1}")
print(f"|v1| = {abs(v1):.2f}")
print(f"v1 == v2? {v1 == v2}")
print(f"v1 == Vector(3, 4)? {v1 == Vector(3, 4)}")
▶ Output
v1 + v2 = (4, 6) v1 - v2 = (2, 2) v1 * 3 = (9, 12) 3 * v1 = (9, 12) -v1 = (-3, -4) |v1| = 5.00 v1 == v2? False v1 == Vector(3, 4)? True
What happened here: v1 + v2 quietly becomes v1.__add__(v2). The interesting one is 3 * v1. Python first asks the left operand, the integer 3, by calling int.__mul__(3, v1). The integer has no idea how to multiply itself by a Vector, so it returns NotImplemented. Python then turns to the right operand and calls v1.__rmul__(3), which does know what to do. That is the whole job of the r-prefixed methods: they catch the case where your object sits on the right. One thing that trips people up: you return NotImplemented, you do not raise it. Returning it is how you politely tell Python to go try the other operand.
Python Operator Overloading: Operator-to-Method Reference
Think of this table as a phrasebook for travelers. You speak in symbols (+, -, *), and Python looks up the matching phrase in its own language (__add__, __sub__, __mul__). Each row also lists the reverse form for when your object sits on the right side, and the in-place form for the += style operators. Bookmark this section: you will come back to it every time you add a new operator to a class.
| Operator | Method | Reverse | In-place |
|---|---|---|---|
+ | __add__ | __radd__ | __iadd__ |
- | __sub__ | __rsub__ | __isub__ |
* | __mul__ | __rmul__ | __imul__ |
/ | __truediv__ | __rtruediv__ | __itruediv__ |
// | __floordiv__ | __rfloordiv__ | __ifloordiv__ |
% | __mod__ | __rmod__ | __imod__ |
** | __pow__ | __rpow__ | __ipow__ |
-obj (unary) | __neg__ | n/a | n/a |
abs() | __abs__ | n/a | n/a |
Real-World Example: Money Class
📄 money.py: currency-safe arithmetic
class Money:
def __init__(self, amount, currency="INR"):
self.amount = round(amount, 2)
self.currency = currency
def __add__(self, other):
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"Cannot add {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
return NotImplemented
def __sub__(self, other):
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"Cannot subtract {other.currency} from {self.currency}")
return Money(self.amount - other.amount, self.currency)
return NotImplemented
def __mul__(self, factor):
if isinstance(factor, (int, float)):
return Money(self.amount * factor, self.currency)
return NotImplemented
def __rmul__(self, factor):
return self.__mul__(factor)
def __eq__(self, other):
if isinstance(other, Money):
return self.amount == other.amount and self.currency == other.currency
return NotImplemented
def __lt__(self, other):
if isinstance(other, Money) and self.currency == other.currency:
return self.amount < other.amount
return NotImplemented
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
def __str__(self):
symbols = {"INR": "Rs.", "USD": "$", "EUR": "EUR"}
sym = symbols.get(self.currency, self.currency)
return f"{sym}{self.amount:,.2f}"
salary = Money(95000)
bonus = Money(15000)
tax = Money(22000)
take_home = salary + bonus - tax
print(f"Salary: {salary}")
print(f"Bonus: {bonus}")
print(f"Tax: {tax}")
print(f"Take home: {take_home}")
print(f"Double: {take_home * 2}")
try:
usd = Money(100, "USD")
result = salary + usd
except ValueError as e:
print(f"Error: {e}")
▶ Output
Salary: Rs.95,000.00 Bonus: Rs.15,000.00 Tax: Rs.22,000.00 Take home: Rs.88,000.00 Double: Rs.176,000.00 Error: Cannot add INR and USD
What happened here: The Money class refuses to mix currencies, the same way a real exchange counter will not let you staple a 100 rupee note to a 100 dollar bill and call it 200 of something. The check lives right inside __add__, so salary + usd raises a ValueError instead of silently giving you a nonsense total. __mul__ lets you scale an amount (Money * 2 for a double bonus), and __rmul__ covers the reverse order (2 * Money). The __str__ method is just for display: it picks the right symbol and adds thousands separators so the number reads like money, not like raw data.
In-Place Operators: +=, -=, *=
📄 inplace.py: augmented assignment
class Counter:
def __init__(self, value=0):
self.value = value
def __iadd__(self, other):
if isinstance(other, int):
self.value += other
return self # Must return self for in-place!
return NotImplemented
def __isub__(self, other):
if isinstance(other, int):
self.value -= other
return self
return NotImplemented
def __repr__(self):
return f"Counter({self.value})"
c = Counter(10)
print(c)
c += 5
print(c)
c -= 3
print(c)
▶ Output
Counter(10) Counter(15) Counter(12)
What happened here: __iadd__ is the method behind +=. Think of it like topping up the same prepaid SIM card instead of buying a brand new one. The Counter object stays put, you just change its balance, so you update self and then return self. That return self is easy to forget and important: skip it and your variable becomes None. For immutable objects like our Vector, do not bother writing __iadd__ at all. Python notices it is missing, quietly falls back to __add__, and rebinds the name to the fresh object for you.
Common Mistakes
Mistake 1: Forgetting reverse operators
Without __rmul__, 3 * vector blows up with a TypeError even though vector * 3 works perfectly. The order of the operands flipped, and your object was no longer the one being asked. So whenever your operator is meant to work with a different type on either side (a Vector times a plain number, for example), add the reverse method (__radd__, __rmul__) to cover both directions.
Mistake 2: Overloading operators with surprising behavior
Do not make + secretly do subtraction. Operators carry expectations the same way a red traffic light means stop everywhere you go. + should combine or add, * should scale or repeat, == should check equality. The moment an operator does something clever and unexpected, anyone reading your code has to stop and second-guess every line. Clever here is the enemy of clear.
Best Practices
- DO return
NotImplementedfor unsupported operand types - DO implement reverse operators when mixing types (vector * scalar)
- DO keep operator semantics intuitive, so
+always feels like addition - DON’T overload operators just because you can, only when it genuinely makes the Application Programming Interface (API) clearer
- DON’T forget
return selfin in-place operators for mutable objects
Practice Exercises
- Exercise 1: Extend the Vector class with
__truediv__sov1 / 2halves each component. ReturnNotImplementedfor anything that is not a number, and raiseZeroDivisionErrorwhen the divisor is 0. - Exercise 2: Build a
Temperatureclass that stores degrees Celsius. Add__add__,__sub__, and the comparison operators__lt__and__eq__so you can compare two readings, then print the warmer one. - Exercise 3: Give the Money class a
__rmul__test of your own: confirm that bothprice * 3and3 * pricereturn the same result, and thatprice * priceraises aTypeError(your__mul__returnsNotImplemented, and Python raises the error once both operands give up).
Conclusion
Python operator overloading lets your own objects speak the language’s native operator dialect. __add__ powers +, __mul__ powers *, and __iadd__ powers +=. The reverse methods like __radd__ step in when your object lands on the right side of the operator. The one rule to burn into memory: return NotImplemented (never raise NotImplementedError) when a particular combination does not make sense, so Python can hand the work to the other operand.
Python operator overloading controls how objects combine; property decorators control how attributes behave. In the property decorators tutorial, you will learn how @property lets you add validation, computation, and access control to attributes without changing the calling code. And if you are landing here mid-series or want to explore other topics, visit the Python + AI/ML tutorial series home for the full index.
Frequently Asked Questions
What is operator overloading in Python?
Operator overloading lets you define how operators like +, -, *, ==, and < work with your custom objects. Implement __add__ for +, __eq__ for ==, etc. Your objects then work with Python’s natural syntax instead of requiring method calls.
What are reverse operators like __radd__ in Python?
Reverse operators handle the case when the left operand doesn’t know how to handle the operation. 3 + obj first tries int.__add__(3, obj) which returns NotImplemented, then Python tries obj.__radd__(3). Implement __radd__ when your object can be on the right side.
What is the difference between __add__ and __iadd__?
__add__ handles a + b and returns a new object. __iadd__ handles a += b and can modify the object in-place (returning self). If __iadd__ is not defined, Python falls back to __add__ and rebinds the variable.
Should I use operator overloading for my classes?
Use it when operators make the API more natural: math classes (Vector, Matrix, Money), collections, and domain types where arithmetic makes sense. Skip it when a method name reads more clearly, since user.merge(other) beats user + other every time.
Why return NotImplemented instead of raising TypeError?
Returning NotImplemented tells Python: ‘I can’t handle this, try the other operand’s method.’ Raising TypeError immediately prevents Python from trying the fallback. Always return NotImplemented from operator methods for unsupported types.
Interview Questions on Operator Overloading
Scenario questions, not trivia: this is the form this topic takes in a real interview.
Q: You add __eq__ to a class, and suddenly its instances cannot be used as dictionary keys or stored in a set. What happened?
Defining __eq__ without __hash__ makes Python set __hash__ to None, so instances become unhashable and dicts and sets reject them. If the objects are logically immutable, define __hash__ yourself, typically return hash((self.x, self.y)) over the same fields that __eq__ compares. If the objects are mutable, staying unhashable is usually correct, because a key that changes its hash would get lost inside the dict.
Q: A function does data += extra on an argument. Callers who pass a list see their list change, but callers who pass a tuple do not. Why?
Lists implement __iadd__, so += mutates the list in place and every reference to it, including the caller’s, sees the change. Tuples are immutable and have no __iadd__, so Python falls back to __add__, builds a brand new tuple, and rebinds only the local name data. The caller’s tuple never changes. This is exactly the mutable versus immutable split you decide on when writing your own __iadd__.
Q: For a + b, in what order does Python try __add__ and __radd__, and when does it flip that order?
Normally Python tries a.__add__(b) first and only calls b.__radd__(a) if the first returns NotImplemented. There is one flip: if type(b) is a proper subclass of type(a) and overrides __radd__, Python asks the subclass first, so specialized child classes get priority. If both sides return NotImplemented, Python raises a TypeError for the unsupported operand types.
Q: In __eq__, why is returning NotImplemented for unknown types better than returning False?
Returning False ends the conversation: the other object never gets a chance to say the two are equal. Returning NotImplemented lets Python try other.__eq__(self), which matters when the other class knows how to compare itself to yours. And unlike arithmetic operators, == never raises when both sides give up: Python quietly falls back to identity comparison, so a == b becomes a is b.
Q: Your Money class stores float amounts, and a billing job that adds thousands of small charges is off by a few paise at the end of the month. What do you recommend?
Floats are binary fractions, so values like 0.1 cannot be stored exactly and tiny errors pile up over thousands of additions. For money, store amounts as decimal.Decimal or as integer paise (smallest currency unit) and only format to rupees for display. The operator overloading stays identical: __add__ and __mul__ just operate on Decimals or integers instead of floats.
Q: Can you overload and, or, not, or the assignment operator = in Python?
No. and and or short-circuit at the language level and only consult your object’s truthiness through __bool__, while not simply negates that result. Plain assignment = is name binding, not an operation on the object, so there is no magic method for it. The closest hooks are & and | via __and__ and __or__, which is how libraries like pandas build filter expressions.
Go deeper: when you outgrow this post, the official Python documentation is the next stop.
Related Posts
Previous: Python: Magic Methods (__str__, __repr__, __len__, __eq__)
Next: Python: Property Decorators, @property, getter, setter
Series Home: Python + AI/ML Tutorial Series

No comment