Menu

Python’s Magic Methods: How to Overload Operators Properly

Written by Abhay Palakkode | 10 min read

Operator overloading in Python is the ability to give custom behavior to built-in operators like +, -, *, and == when used with your own classes. Magic methods (also called dunder methods) are special methods that start and end with double underscores like __add__, __eq__, and __str__ that Python automatically calls when you use operators or built-in functions. This allows your custom objects to work seamlessly with Python’s operators and built-in functions.

Understanding how the + operator works seamlessly on both numbers and strings, or how len() functions across lists, dictionaries, and custom objects, is crucial for writing Pythonic code. This functionality is achieved through operator overloading.

Magic methods provide the mechanism for this operator overloading enabling your custom classes to integrate naturally with Python’s built-in operators and functions, creating intuitive interfaces that feel native to the language.

Magic methods have been part of Python since its early versions and are formalized in Python’s Data Model. The concept was established as part of Python’s object-oriented design philosophy, allowing objects to define their own behavior with respect to language operators.

1. Understanding Basic Magic Methods

Before we dive into operator overloading, let’s understand what magic methods are and how they work behind the scenes.

Magic methods are special methods that Python automatically calls when you use operators or built-in functions on your objects. They all start and end with double underscores (__), which is why they’re also called "dunder methods" (double underscore methods).

Let’s start with a simple example. We’ll create a Car class with basic attributes and see how magic methods work.

Our Car class will have an __init__ method for initialization, a __str__ method for string representation, and we’ll explore how these methods are called automatically.

python
class Car:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year
    
    def __str__(self):
        return f"{self.year} {self.brand} {self.model}"

# Create a car instance
car = Car("Toyota", "Camry", 2023)
print(car)  # This calls __str__ automatically
python
2023 Toyota Camry

When you run this code, you’ll see that print(car) automatically calls the __str__ method. Python knows to call __str__ when you use the print() function or str() function on an object.

Let’s see what happens without the __str__ method:

python
class CarWithoutStr:
    def __init__(self, brand, model, year):
        self.brand = brand
        self.model = model
        self.year = year

car_no_str = CarWithoutStr("Honda", "Civic", 2023)
print(car_no_str)  # This will show memory location
python
<__main__.CarWithoutStr object at 0x000002685C1855D0>

This shows the default behavior – Python displays the memory location of the object because there’s no custom __str__ method.

2. Arithmetic Operator Overloading

Let’s see how arithmetic operators work with custom classes. We’ll create a Vector class that represents a 2D vector and implement arithmetic operations.

Our Vector class will have an __init__ method for initialization, __str__ for display, and arithmetic magic methods (__add__, __sub__, __mul__) to enable mathematical operations on vector objects.

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

# Test arithmetic operations
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1: {v1}")           # Vector(3, 4)
print(f"v2: {v2}")           # Vector(1, 2)
print(f"v1 + v2: {v1 + v2}") # Vector(4, 6)
print(f"v1 - v2: {v1 - v2}") # Vector(2, 2)
print(f"v1 * 3: {v1 * 3}")   # Vector(9, 12)
python
v1: Vector(3, 4)
v2: Vector(1, 2)
v1 + v2: Vector(4, 6)
v1 - v2: Vector(2, 2)
v1 * 3: Vector(9, 12)

This demonstrates how __add__, __sub__, and __mul__ methods are automatically called when you use the +, -, and * operators. Python translates v1 + v2 into v1.__add__(v2) behind the scenes.

3. Comparison Operator Overloading

Let’s implement comparison operators for our objects. We’ll create a Product class that represents items in an inventory system.

Our Product class will have an __init__ method for initialization, __str__ for display, and comparison magic methods (__eq__, __lt__, __gt__, __le__, __ge__) to enable comparison operations between product objects based on their price.

python
class Product:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
    
    def __str__(self):
        return f"{self.name} - ${self.price} (qty: {self.quantity})"
    
    def __eq__(self, other):
        return self.price == other.price
    
    def __lt__(self, other):
        return self.price < other.price
    
    def __gt__(self, other):
        return self.price > other.price
    
    def __le__(self, other):
        return self.price <= other.price
    
    def __ge__(self, other):
        return self.price >= other.price

# Test comparison operations
laptop = Product("Laptop", 999.99, 5)
mouse = Product("Mouse", 29.99, 50)
keyboard = Product("Keyboard", 79.99, 25)

print(f"Laptop: {laptop}")
print(f"Mouse: {mouse}")
print(f"Keyboard: {keyboard}")

print(f"Laptop == Mouse: {laptop == mouse}")   # False
print(f"Mouse < Keyboard: {mouse < keyboard}") # True
print(f"Laptop > Keyboard: {laptop > keyboard}") # True

# We can now sort products by price
products = [laptop, mouse, keyboard]
sorted_products = sorted(products)
print("\nSorted by price:")
for product in sorted_products:
    print(product)
python
Laptop: Laptop - $999.99 (qty: 5)
Mouse: Mouse - $29.99 (qty: 50)
Keyboard: Keyboard - $79.99 (qty: 25)
Laptop == Mouse: False
Mouse < Keyboard: True
Laptop > Keyboard: True

Sorted by price:
Mouse - $29.99 (qty: 50)
Keyboard - $79.99 (qty: 25)
Laptop - $999.99 (qty: 5)

This shows how comparison operators enable your objects to be sorted and compared naturally. The sorted() function works because our Product objects now know how to compare themselves.

4. Container-Like Behavior

Let’s create a class that behaves like a container. We’ll build a ShoppingCart class that acts like a list but with custom behavior.

Our ShoppingCart class will have an __init__ method for initialization, container magic methods (__len__, __getitem__, __setitem__, __delitem__, __contains__) to make it behave like a list, plus business logic methods (add_item, remove_item, total_price) for cart operations.

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def remove_item(self, item):
        if item in self.items:
            self.items.remove(item)
    
    def total_price(self):
        return sum(item.price * item.quantity for item in self.items)
    
    def __len__(self):
        return len(self.items)
    
    def __getitem__(self, index):
        return self.items[index]
    
    def __setitem__(self, index, value):
        self.items[index] = value
    
    def __delitem__(self, index):
        del self.items[index]
    
    def __contains__(self, item):
        return item in self.items
    
    def __str__(self):
        if not self.items:
            return "Empty cart"
        return f"Cart with {len(self.items)} items (${self.total_price():.2f})"

# Test container behavior
cart = ShoppingCart()
cart.add_item(laptop)
cart.add_item(mouse)
cart.add_item(keyboard)

print(f"Cart: {cart}")
print(f"Cart length: {len(cart)}")        # Uses __len__
print(f"First item: {cart[0]}")           # Uses __getitem__
print(f"Laptop in cart: {laptop in cart}") # Uses __contains__

# We can iterate through the cart
print("\nItems in cart:")
for item in cart:  # Uses __getitem__ for iteration
    print(f"- {item}")
python
Cart: Cart with 3 items ($8499.20)
Cart length: 3
First item: Laptop - $999.99 (qty: 5)
Laptop in cart: True

Items in cart:
- Laptop - $999.99 (qty: 5)
- Mouse - $29.99 (qty: 50)
- Keyboard - $79.99 (qty: 25)

This demonstrates how container magic methods make your object behave like built-in containers. The len() function, indexing with [], and the in operator all work naturally.

5. Real-World Example: Bank Account System

Let’s build a comprehensive example that uses multiple magic methods. We’ll create a BankAccount class for a banking system.

Our BankAccount class will have an __init__ method for initialization, arithmetic magic methods (__add__, __sub__, __iadd__, __isub__) for deposit/withdrawal operations, comparison magic methods (__eq__, __lt__, __gt__) for account comparison, and string representation methods (__str__, __repr__) for display.

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def __str__(self):
        return f"Account {self.account_number}: {self.holder_name} - ${self.balance:.2f}"
    
    def __repr__(self):
        return f"BankAccount('{self.account_number}', '{self.holder_name}', {self.balance})"
    
    def __add__(self, amount):
        """Deposit money (returns new account with updated balance)"""
        return BankAccount(self.account_number, self.holder_name, self.balance + amount)
    
    def __sub__(self, amount):
        """Withdraw money (returns new account with updated balance)"""
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        return BankAccount(self.account_number, self.holder_name, self.balance - amount)
    
    def __iadd__(self, amount):
        """In-place deposit (modifies current account)"""
        self.balance += amount
        return self
    
    def __isub__(self, amount):
        """In-place withdrawal (modifies current account)"""
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        self.balance -= amount
        return self
    
    def __eq__(self, other):
        return self.balance == other.balance
    
    def __lt__(self, other):
        return self.balance < other.balance
    
    def __gt__(self, other):
        return self.balance > other.balance
    
    def __bool__(self):
        """Account is 'truthy' if it has positive balance"""
        return self.balance > 0

# Test the bank account system
alice_account = BankAccount("12345", "Alice Smith", 1000)
bob_account = BankAccount("67890", "Bob Johnson", 500)

print(f"Alice: {alice_account}")
print(f"Bob: {bob_account}")

# Test arithmetic operations
new_alice = alice_account + 200  # Deposit (creates new account)
print(f"After deposit: {new_alice}")
print(f"Original Alice: {alice_account}")  # Unchanged

# Test in-place operations
alice_account += 200  # In-place deposit
print(f"After in-place deposit: {alice_account}")

# Test comparisons
print(f"Alice > Bob: {alice_account > bob_account}")
print(f"Alice == Bob: {alice_account == bob_account}")

# Test boolean conversion
empty_account = BankAccount("00000", "Empty User", 0)
print(f"Alice account is truthy: {bool(alice_account)}")
print(f"Empty account is truthy: {bool(empty_account)}")

# We can sort accounts by balance
accounts = [alice_account, bob_account, empty_account]
sorted_accounts = sorted(accounts)
print("\nAccounts sorted by balance:")
for account in sorted_accounts:
    print(account)
python
Alice: Account 12345: Alice Smith - $1000.00
Bob: Account 67890: Bob Johnson - $500.00
After deposit: Account 12345: Alice Smith - $1200.00
Original Alice: Account 12345: Alice Smith - $1000.00
After in-place deposit: Account 12345: Alice Smith - $1200.00
Alice > Bob: True
Alice == Bob: False
Alice account is truthy: True
Empty account is truthy: False

Accounts sorted by balance:
Account 00000: Empty User - $0.00
Account 67890: Bob Johnson - $500.00
Account 12345: Alice Smith - $1200.00

This comprehensive example shows how multiple magic methods work together to create a natural, intuitive interface for your objects.

6. Common Magic Methods Reference

Let’s create a quick reference of the most common magic methods and when to use them.

Here’s a MagicMethodsDemo class that demonstrates various categories of magic methods with their __init__ method for initialization, and examples of lifecycle methods, string representation methods, arithmetic methods, comparison methods, and container methods.

python
class MagicMethodsDemo:
    def __init__(self, value):
        self.value = value
        print(f"Created object with value: {value}")
    
    # String representation
    def __str__(self):
        return f"Demo({self.value})"
    
    def __repr__(self):
        return f"MagicMethodsDemo({self.value!r})"
    
    # Arithmetic operators
    def __add__(self, other):
        return MagicMethodsDemo(self.value + other.value)
    
    def __mul__(self, scalar):
        return MagicMethodsDemo(self.value * scalar)
    
    # Comparison operators
    def __eq__(self, other):
        return self.value == other.value
    
    def __lt__(self, other):
        return self.value < other.value
    
    # Container-like behavior
    def __len__(self):
        return len(str(self.value))
    
    def __bool__(self):
        return bool(self.value)
    
    # Make it callable
    def __call__(self, *args):
        return f"Called with {args}, value is {self.value}"

# Test various magic methods
demo1 = MagicMethodsDemo(42)
demo2 = MagicMethodsDemo(58)

print(f"demo1: {demo1}")                    # __str__
print(f"repr(demo1): {repr(demo1)}")        # __repr__
print(f"demo1 + demo2: {demo1 + demo2}")    # __add__
print(f"demo1 * 2: {demo1 * 2}")            # __mul__
print(f"demo1 == demo2: {demo1 == demo2}")  # __eq__
print(f"demo1 < demo2: {demo1 < demo2}")    # __lt__
print(f"len(demo1): {len(demo1)}")          # __len__
print(f"bool(demo1): {bool(demo1)}")        # __bool__
print(f"demo1('hello'): {demo1('hello')}")  # __call__
python
Created object with value: 42
Created object with value: 58
demo1: Demo(42)
repr(demo1): MagicMethodsDemo(42)
Created object with value: 100
demo1 + demo2: Demo(100)
Created object with value: 84
demo1 * 2: Demo(84)
demo1 == demo2: False
demo1 < demo2: True
len(demo1): 2
bool(demo1): True
demo1('hello'): Called with ('hello',), value is 42

This reference shows how different magic methods serve different purposes and can be combined to create powerful, intuitive interfaces.

8. Best Practices

Here are some important best practices when implementing magic methods:

Always Return New Objects for Arithmetic Operations

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        # Good: Return new object
        return Point(self.x + other.x, self.y + other.y)
        
    def __iadd__(self, other):
        # Good: Modify self for in-place operations
        self.x += other.x
        self.y += other.y
        return self

Implement __repr__ for Debugging

python
class Customer:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    def __repr__(self):
        return f"Customer({self.name!r}, {self.email!r})"
    
    def __str__(self):
        return f"{self.name} ({self.email})"

Handle Edge Cases Gracefully

python
class SafeVector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        if not isinstance(other, SafeVector):
            return NotImplemented
        return SafeVector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        if not isinstance(other, SafeVector):
            return NotImplemented
        return self.x == other.x and self.y == other.y
Free Course
Master Core Python — Your First Step into AI/ML

Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.

Start Free Course
Trusted by 50,000+ learners
Related Course
Master Python — Hands-On
Join 5,000+ students at edu.machinelearningplus.com
Explore Course
Get the full course,
completely free.
Join 57,000+ students learning Python, SQL & ML. One year of access, all resources included.
📚 10 Courses
🐍 Python & ML
🗄️ SQL
📦 Downloads
📅 1 Year Access
No thanks
🎓
Free AI/ML Starter Kit
Python · SQL · ML · 10 Courses · 57,000+ students
🎉   You're in! Check your inbox (or Promotions/Spam) for the access link.
⚡ Before you go

Python.
SQL. NumPy.
All free.

Get the exact 10-course programming foundation that Data Science professionals use.

🐍
Core Python — from first line to expert level
📈
NumPy & Pandas — the #1 libraries every DS job needs
🗃️
SQL Levels I–III — basics to Window Functions
📄
Real industry data — Jupyter notebooks included
R A M S K
57,000+ students
★★★★★ Rated 4.9/5
⚡ Before you go
Python. SQL.
All Free.
R A M S K
57,000+ students  ★★★★★ 4.9/5
Get Free Access Now
10 courses. Real projects. Zero cost. No credit card.
New learners enrolling right now
🔒 100% free ☕ No spam, ever ✓ Instant access
🚀
You're in!
Check your inbox for your access link.
(Check Promotions or Spam if you don't see it)
Or start your first course right now:
Start Free Course →
Scroll to Top
Scroll to Top
Course Preview

Machine Learning A-Z™: Hands-On Python & R In Data Science

Free Sample Videos:

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science

Machine Learning A-Z™: Hands-On Python & R In Data Science