Have you ever created a beautiful, complex custom class in Python, instantiated an object, and tried to print() it, only to be greeted by an ugly, cryptic message like <__main__.Car object at 0x000001A2B3C4D5E6>?
By default, Python doesn’t know how to display your custom objects to a user. It doesn’t know how to add two of your objects together using the + sign, and it doesn’t know how to calculate the len() of your object.
To teach Python how your custom classes should interact with its built-in functions and operators, we use Dunder Methods. Also known as “Magic Methods,” these special functions are the secret sauce that makes your custom objects feel completely natural, professional, and “Pythonic.”
What is Dunder?
In Python, Dunder Methods (short for “Double Underscore” methods) are special pre-defined methods whose names begin and end with two underscores. They are not meant to be called directly by you (the programmer). Instead, they are invoked automatically by Python under the hood when certain operations are performed on your object.
For example, when you use the + operator, Python secretly calls the __add__() dunder method. When you call len(), Python secretly calls __len__(). By overriding these methods in your custom classes, you gain absolute control over how your objects behave.
Syntax & Basic Usage
The most common dunder method you already know is __init__(), which is called automatically when an object is created. To implement other dunder methods, you define them inside your class exactly like standard instance methods, ensuring they accept self as the first parameter.
Let’s look at the problem of printing a raw object, and the basic solution using __str__.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
# Defining the __str__ dunder method
def __str__(self):
# This string will be returned whenever print() is called on the object
return f"'{self.title}' by {self.author}"
# Create an instance of the Book
favorite_book = Book("1984", "George Orwell")
# Because we defined __str__, print() now shows our beautifully formatted string!
print(favorite_book)
# Expected Output:
# '1984' by George Orwell
Python Dunder Methods and Function Arguments
Let’s deeply explore the most essential dunder methods every senior Python developer relies on to build robust, interactive objects.
1. The Human-Readable String: __str__(self)
The __str__ method dictates what happens when you pass your object into the print() or str() functions. Its primary goal is to return a highly readable, user-friendly string representation of the object.
class BankAccount:
def __init__(self, account_holder, balance):
self.holder = account_holder
self.balance = balance
def __str__(self):
# Returning a clean, formatted string for the end-user
return f"Account Holder: {self.holder} | Balance: ${self.balance:.2f}"
user_account = BankAccount("Alice", 1500.50)
print(user_account)
# Expected Output:
# Account Holder: Alice | Balance: $1500.50
2. The Developer String: __repr__(self)
While __str__ is for users, __repr__ (short for representation) is for developers. It is triggered by the repr() function and when an object is inspected in an interactive Python terminal.
The strict industry standard for __repr__ is that it should return a string that looks exactly like valid Python code—code that could be copied and pasted to recreate the object. If a class has a __repr__ but no __str__, Python will use __repr__ as a fallback for print().
class BankAccount:
def __init__(self, account_holder, balance):
self.holder = account_holder
self.balance = balance
def __repr__(self):
# Returning a string that perfectly mimics the code used to create the object
return f"BankAccount('{self.holder}', {self.balance})"
user_account = BankAccount("Alice", 1500.50)
# Simulating how a developer inspecting logs or the terminal would see it
developer_view = repr(user_account)
print(f"Developer Log: {developer_view}")
# Expected Output:
# Developer Log: BankAccount('Alice', 1500.5)
3. Custom Lengths: __len__(self)
If you build a class that acts like a container (like a custom playlist, inventory, or cart), you will want the built-in len() function to work on it. Overriding __len__ requires you to return an integer.
class MusicPlaylist:
def __init__(self, playlist_name):
self.name = playlist_name
self.songs = []
def add_song(self, song_title):
self.songs.append(song_title)
def __len__(self):
# We define the "length" of the playlist as the number of songs it holds
return len(self.songs)
my_playlist = MusicPlaylist("Workout Mix")
my_playlist.add_song("Eye of the Tiger")
my_playlist.add_song("Stronger")
# Python seamlessly uses our __len__ method!
print(f"There are {len(my_playlist)} songs in the playlist.")
# Expected Output:
# There are 2 songs in the playlist.
4. Operator Overloading: __add__(self, other)
“Operator Overloading” is a fancy term for redefining how math symbols work with your custom objects. If you try to add two Book objects together with +, Python crashes. But if you define __add__, you tell Python exactly what + means in the context of your class.
The __add__ method takes two arguments: self (the object on the left side of the +) and other (the object on the right side).
class Wallet:
def __init__(self, amount):
self.amount = amount
def __add__(self, other_wallet):
# We define adding two wallets together as combining their amounts
# Best practice is to return a BRAND NEW instance of the object!
combined_amount = self.amount + other_wallet.amount
return Wallet(combined_amount)
def __str__(self):
return f"${self.amount}"
wallet_one = Wallet(50)
wallet_two = Wallet(75)
# Python secretly calls wallet_one.__add__(wallet_two)
combined_wallet = wallet_one + wallet_two
print(f"Wallet 1: {wallet_one}")
print(f"Wallet 2: {wallet_two}")
print(f"Combined: {combined_wallet}")
# Expected Output:
# Wallet 1: $50
# Wallet 2: $75
# Combined: $125
5. Checking Equality: __eq__(self, other)
By default, if you check object_a == object_b, Python only checks if they occupy the exact same spot in your computer’s memory. Even if they have identical data, == will return False. By defining __eq__, you tell Python exactly which attributes must match for two objects to be considered “equal.”
class Product:
def __init__(self, item_id, name):
self.item_id = item_id
self.name = name
def __eq__(self, other_product):
# We define equality strictly by the item_id matching
return self.item_id == other_product.item_id
# These are two different objects in memory...
item_one = Product(101, "Wireless Mouse")
item_two = Product(101, "Wireless Mouse")
item_three = Product(999, "Keyboard")
# ...but our __eq__ method makes them logically equal!
print(f"Is item_one identical to item_two? {item_one == item_two}")
print(f"Is item_one identical to item_three? {item_one == item_three}")
# Expected Output:
# Is item_one identical to item_two? True
# Is item_one identical to item_three? False
Real-World Practical Examples
Scenario 1: A Complete Shopping Cart
Let’s build a highly Pythonic ShoppingCart that utilizes multiple magic methods to feel exactly like a built-in Python data structure.
class ShoppingCart:
def __init__(self, customer_name):
self.customer = customer_name
self.items = []
def add_item(self, item_name):
self.items.append(item_name)
# Allow len() to work on the cart
def __len__(self):
return len(self.items)
# Provide a beautiful receipt when print() is called
def __str__(self):
cart_contents = ", ".join(self.items)
return f"{self.customer}'s Cart ({len(self)} items): [{cart_contents}]"
# Using our highly interactive class
active_cart = ShoppingCart("David")
active_cart.add_item("Laptop")
active_cart.add_item("Headphones")
active_cart.add_item("Mousepad")
print(active_cart)
print(f"Total items calculated via len(): {len(active_cart)}")
# Expected Output:
# David's Cart (3 items): [Laptop, Headphones, Mousepad]
# Total items calculated via len(): 3
Scenario 2: 2D Physics Vectors
In game development and physics simulations, developers rely heavily on operator overloading to handle complex math effortlessly. Here is a Vector2D class that supports addition (+) and subtraction (- using __sub__).
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
# Define how to add two vectors (x1+x2, y1+y2)
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
# Define how to subtract two vectors
def __sub__(self, other):
return Vector2D(self.x - other.x, self.y - other.y)
# Define how to print the vector
def __str__(self):
return f"Vector({self.x}, {self.y})"
# Create two forces
wind_force = Vector2D(5, 2)
engine_force = Vector2D(10, 8)
# The magic methods make complex math look incredibly simple!
total_force = wind_force + engine_force
braking_force = engine_force - wind_force
print(f"Total combined force: {total_force}")
print(f"Remaining force after braking: {braking_force}")
# Expected Output:
# Total combined force: Vector(15, 10)
# Remaining force after braking: Vector(5, 6)
Best Practices & Common Pitfalls
- Never Return Integers from
__str__or__repr__: These methods must return a String data type. If you return an integer or a list, Python will crash with aTypeError. If you need to print a number, cast it to a string first (e.g.,return str(self.amount)). - Never Return Strings from
__len__: Conversely, the__len__method must return an Integer. Returning a string here will crash your program. - Understand
__str__vs__repr__: A simple rule of thumb:__str__is for your application’s end-users (it should be pretty).__repr__is for you and your fellow developers (it should be highly technical and unambiguous). If you only have time to implement one, implement__repr__, as Python uses it as a fallback. - Return New Instances in Math Dunders: When you implement
__add__, do not alter the originalselfobject. Instead, create and return a brand new instance of the class containing the summed data. This prevents accidental data corruption across your application.
Summary
- Dunder (Magic) Methods are special methods surrounded by double underscores that Python calls automatically during built-in operations.
__str__is triggered byprint()and returns a clean, human-readable string.__repr__is triggered byrepr()and should return a string representing valid Python code to recreate the object.__len__allows your custom objects to be measured using the built-inlen()function.- Operator Overloading is achieved by defining methods like
__add__(for+),__sub__(for-), and__eq__(for==), allowing mathematical operators to work seamlessly with custom objects.
