Python’s Magic Methods: How to Overload Operators Properly
Discover how operator overloading is implemented using magic methods, allowing custom classes to integrate with Python's core operations.
By Abhay Palakkode · July 2025
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.
PyInteractive Python
classCar:def__init__(self, brand, model, year):self.brand = brandself.model = modelself.year = yeardef__str__(self):returnf"{self.year}{self.brand}{self.model}"# Create a car instancecar = Car("Toyota", "Camry", 2023)print(car) # This calls __str__ automatically#> 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:
PyInteractive Python
classCarWithoutStr:def__init__(self, brand, model, year):self.brand = brandself.model = modelself.year = yearcar_no_str = CarWithoutStr("Honda", "Civic", 2023)print(car_no_str) # This will show memory location#> <__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.
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.
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.
PyInteractive Python
classShoppingCart:def__init__(self):self.items = []defadd_item(self, item):self.items.append(item)defremove_item(self, item):if item inself.items:self.items.remove(item)deftotal_price(self):returnsum(item.price * item.quantity for item inself.items)def__len__(self):returnlen(self.items)def__getitem__(self, index):returnself.items[index]def__setitem__(self, index, value):self.items[index] = valuedef__delitem__(self, index):delself.items[index]def__contains__(self, item):return item inself.itemsdef__str__(self):ifnotself.items:return"Empty cart"returnf"Cart with {len(self.items)} items (${self.total_price():.2f})"# Test container behaviorcart = 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 cartprint("\nItems in cart:")for item in cart: # Uses __getitem__ for iterationprint(f"- {item}")#> 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.
PyInteractive Python
classBankAccount:def__init__(self, account_number, holder_name, balance=0):self.account_number = account_numberself.holder_name = holder_nameself.balance = balancedef__str__(self):returnf"Account {self.account_number}: {self.holder_name} - ${self.balance:.2f}"def__repr__(self):returnf"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:raiseValueError("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 += amountreturnselfdef__isub__(self, amount):"""In-place withdrawal (modifies current account)"""if amount >self.balance:raiseValueError("Insufficient funds")self.balance -= amountreturnselfdef__eq__(self, other):returnself.balance == other.balancedef__lt__(self, other):returnself.balance < other.balancedef__gt__(self, other):returnself.balance > other.balancedef__bool__(self):"""Account is 'truthy' if it has positive balance"""returnself.balance >0# Test the bank account systemalice_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 operationsnew_alice = alice_account +200# Deposit (creates new account)print(f"After deposit: {new_alice}")print(f"Original Alice: {alice_account}") # Unchanged# Test in-place operationsalice_account +=200# In-place depositprint(f"After in-place deposit: {alice_account}")# Test comparisonsprint(f"Alice > Bob: {alice_account > bob_account}")print(f"Alice == Bob: {alice_account == bob_account}")# Test boolean conversionempty_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 balanceaccounts = [alice_account, bob_account, empty_account]sorted_accounts =sorted(accounts)print("\nAccounts sorted by balance:")for account in sorted_accounts:print(account)#> 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.
PyInteractive Python
classMagicMethodsDemo:def__init__(self, value):self.value = valueprint(f"Created object with value: {value}")# String representationdef__str__(self):returnf"Demo({self.value})"def__repr__(self):returnf"MagicMethodsDemo({self.value!r})"# Arithmetic operatorsdef__add__(self, other):return MagicMethodsDemo(self.value + other.value)def__mul__(self, scalar):return MagicMethodsDemo(self.value * scalar)# Comparison operatorsdef__eq__(self, other):returnself.value == other.valuedef__lt__(self, other):returnself.value < other.value# Container-like behaviordef__len__(self):returnlen(str(self.value))def__bool__(self):returnbool(self.value)# Make it callabledef__call__(self, *args):returnf"Called with {args}, value is {self.value}"# Test various magic methodsdemo1 = 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__#> 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