Python Operator Overloading
What is Operator Overloading?
Operator overloading lets you define how built-in Python operations (+, -, *, /, len(), ==, etc.) behave on your own custom classes. This is done by implementing special dunder methods (double underscore methods, also called magic methods).
Example:
1 + 1works because theintclass implements__add__. You can do the same for your own classes.
Dunder / Magic Methods
All dunder methods follow this pattern: __method_name__
They are triggered automatically by Python operations — you don't call them directly (though you can).
Arithmetic Operations
| Operation | Dunder Method | Example |
|---|---|---|
| Addition | __add__ |
a + b |
| Subtraction | __sub__ |
a - b |
| Multiplication | __mul__ |
a * b |
| Division | __truediv__ |
a / b |
| Integer division | __floordiv__ |
a // b |
Example — __add__ on a Page class
class Page:
def __init__(self, words, page_number):
self.words = words
self.page_number = page_number
def __add__(self, other):
new_words = self.words + " " + other.words
new_page_number = max(self.page_number, other.page_number) + 1
return Page(new_words, new_page_number)
page1 = Page("Hello world", 1)
page2 = Page("This is page two", 2)
page3 = page1 + page2
print(page3.words) # Hello world This is page two
print(page3.page_number) # 3
Example — __sub__ and __mul__ on a StoreItem class
class StoreItem:
tax = 0.13
def __init__(self, name, price):
self.name = name
self.price = price
self.after_tax_price = 0
self.set_after_tax_price()
def set_after_tax_price(self):
self.after_tax_price = self.price * (1 + self.tax)
def __sub__(self, discount): # flat dollar discount
return StoreItem(self.name, self.price - discount)
def __mul__(self, value): # percentage discount
return StoreItem(self.name, self.price * value)
bread = StoreItem("Bread", 7)
discounted = bread - 2 # $2 off
print(round(discounted.after_tax_price, 2)) # 5.65
sale = bread * 0.8 # 20% off
print(round(sale.after_tax_price, 2)) # 6.33
Example — __truediv__ and __floordiv__ on a Line class
import math
class Line:
def __init__(self, point1, point2):
self.point1 = point1
self.point2 = point2
def __truediv__(self, factor): # /
new_p1 = (self.point1[0] / factor, self.point1[1] / factor)
new_p2 = (self.point2[0] / factor, self.point2[1] / factor)
return Line(new_p1, new_p2)
def __floordiv__(self, factor): # //
new_p1 = (self.point1[0] // factor, self.point1[1] // factor)
new_p2 = (self.point2[0] // factor, self.point2[1] // factor)
return Line(new_p1, new_p2)
line1 = Line((10, 5), (20, 10))
line2 = line1 / 2
print(line2.point1) # (5.0, 2.5)
line3 = line1 // 2
print(line3.point1) # (5, 2)
__len__
Returns the length of an object when len() is called on it.
⚠️ Must return an integer — not a float.
class Line:
def __len__(self):
dist_x = (self.point1[0] - self.point2[0]) ** 2
dist_y = (self.point1[1] - self.point2[1]) ** 2
return round(math.sqrt(dist_x + dist_y))
print(len(line1)) # 11
Comparison Operations
| Operation | Dunder Method |
|---|---|
== |
__eq__ |
!= |
__ne__ |
> |
__gt__ |
>= |
__ge__ |
< |
__lt__ |
<= |
__le__ |
Default behaviour without __eq__
Without implementing __eq__, Python checks if two objects are the exact same object in memory (like using is). Two separate instances with identical values will return False.
line1 = Line((10, 5), (20, 10))
line2 = Line((10, 5), (20, 10)) # same values, different object
print(line1 == line2) # False — without __eq__
Implementing comparison methods
class Line:
def __eq__(self, other):
if not isinstance(other, Line): # always check type first
return False
return self.point1 == other.point1 and self.point2 == other.point2
def __ne__(self, other):
return not self.__eq__(other) # reuse __eq__ to avoid duplication
def __gt__(self, other):
return len(self) > len(other)
def __ge__(self, other):
return len(self) >= len(other)
def __lt__(self, other):
return len(self) < len(other)
def __le__(self, other):
return len(self) <= len(other)
Tip: Always check
isinstance(other, ClassName)inside__eq__before accessing attributes — otherwise comparing to a different type will crash.
String Representation Methods
__str__ — Human-readable output
Called when you use print() or str() on an object. Should return a friendly, readable string.
class Page:
def __str__(self):
return f"Page(text={self.text}, page_number={self.page_number})"
print(page1) # Page(text=Hello world, page_number=1)
__repr__ — Developer / debug representation
Called when you use repr(). Should return the internal/debug representation — typically more terse, focused on identity.
class Book:
def __repr__(self):
return f"Book(id_number={self.id_number})"
def __str__(self):
output = f"Book {self.title} {self.author} {self.id_number}"
for page in self.pages:
output += "\n" + str(page)
return output
print(book) # triggers __str__ — full readable output
print(repr(book)) # triggers __repr__ — Book(id_number=1)
| Method | Triggered by | Purpose |
|---|---|---|
__str__ |
print(), str() |
Human-readable display |
__repr__ |
repr(), debugger |
Internal/debug representation |
Key Takeaways & Recap
| Concept | Summary |
|---|---|
| Dunder methods | Special __method__ methods triggered by Python operators |
| Operator overloading | Define custom behaviour for +, -, *, /, //, len(), comparisons, etc. |
__add__ / __sub__ / __mul__ |
Arithmetic on custom objects |
__truediv__ / __floordiv__ |
/ and // division |
__len__ |
Must return an integer |
__eq__ default |
Checks same object in memory — override to compare values |
Always check isinstance |
In __eq__ (and comparisons), verify type before accessing attributes |
__ne__ shortcut |
Return not self.__eq__(other) to avoid rewriting logic |
__str__ vs __repr__ |
__str__ = readable; __repr__ = debug/internal |