Python OOP Magic Methods: __init__, __str__, __repr__ Complete Guide
Master Python magic methods (dunder methods) with practical examples. Learn __init__, __str__, __repr__, __len__, __eq__, __add__ and more to write Pythonic, object-oriented code

The Day Magic Methods Clicked for Me
I was writing a Vector class and got frustrated:
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# This worked
result = add_vectors(v1, v2)
# But I wanted this to work
result = v1 + v2 # TypeError!
"How do built-in types support + but my class doesn't?"
My mentor showed me one line that changed everything:
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
Suddenly: v1 + v2 worked! Magic.
Today, I'll demystify Python's magic methods (also called dunder methods) and show you how to make your classes behave like built-in types.
What Are Magic Methods?
Magic methods (or dunder methods - double underscore) are special methods that define how objects behave with Python's built-in operations.
Format: __method_name__
Examples:
__init__- Object initialization__str__- String representation (for humans)__repr__- String representation (for developers)__len__- Length (len())__add__- Addition (+)__eq__- Equality (==)
Why "magic"? They're automatically called by Python when you use operators or built-in functions.
1. Object Lifecycle Methods
__init__ - Constructor (Initialization)
Called when object is created. Most commonly used magic method.
class Person:
def __init__(self, name, age):
"""Initialize Person object"""
self.name = name
self.age = age
print(f"✅ Created Person: {name}")
# __init__ is called automatically
person = Person("Alice", 25)
# Output: ✅ Created Person: Alice
print(person.name) # Alice
print(person.age) # 25
With default values:
class Config:
def __init__(self, host="localhost", port=8080, debug=False):
self.host = host
self.port = port
self.debug = debug
# Different ways to initialize
config1 = Config() # All defaults
config2 = Config("example.com") # Custom host
config3 = Config("example.com", 3000, True) # All custom
__new__ - Constructor (Object Creation)
Called before __init__. Rarely used, but powerful for singletons.
class Singleton:
_instance = None
def __new__(cls):
"""Create instance only once"""
if cls._instance is None:
print("Creating new instance")
cls._instance = super().__new__(cls)
else:
print("Returning existing instance")
return cls._instance
def __init__(self):
print("Initializing instance")
# First call
s1 = Singleton()
# Output:
# Creating new instance
# Initializing instance
# Second call
s2 = Singleton()
# Output:
# Returning existing instance
# Initializing instance
print(s1 is s2) # True (same object!)
__del__ - Destructor (Cleanup)
Called when object is about to be destroyed. Use with caution!
class FileHandler:
def __init__(self, filename):
self.filename = filename
self.file = open(filename, 'w')
print(f"📂 Opened {filename}")
def __del__(self):
"""Cleanup when object is destroyed"""
self.file.close()
print(f"🔒 Closed {filename}")
# Object created
handler = FileHandler("test.txt")
# Object destroyed (file automatically closed)
del handler
# Output: 🔒 Closed test.txt
2. String Representation Methods
__str__ vs __repr__
Two ways to represent objects as strings:
| Method | Purpose | Audience | Called By |
|---|---|---|---|
__str__ | Human-readable | End users | print(), str() |
__repr__ | Developer-friendly, unambiguous | Developers | repr(), interactive shell |
Rule of thumb: repr() should be valid Python code to recreate the object.
class Book:
def __init__(self, title, author, year):
self.title = title
self.author = author
self.year = year
def __str__(self):
"""Human-readable string (for print())"""
return f"'{self.title}' by {self.author} ({self.year})"
def __repr__(self):
"""Developer-friendly representation (for debugging)"""
return f"Book(title='{self.title}', author='{self.author}', year={self.year})"
book = Book("1984", "George Orwell", 1949)
# __str__ is used
print(str(book)) # '1984' by George Orwell (1949)
print(book) # '1984' by George Orwell (1949)
# __repr__ is used
print(repr(book)) # Book(title='1984', author='George Orwell', year=1949)
# In interactive shell
>>> book
Book(title='1984', author='George Orwell', year=1949) # Uses __repr__
Best practice: Always implement both!
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name} - ${self.price:.2f}"
def __repr__(self):
return f"Product(name='{self.name}', price={self.price})"
products = [
Product("Laptop", 999.99),
Product("Mouse", 29.99)
]
# __str__ for each item
for product in products:
print(product)
# Output:
# Laptop - $999.99
# Mouse - $29.99
# __repr__ for list
print(products)
# Output:
# [Product(name='Laptop', price=999.99), Product(name='Mouse', price=29.99)]
3. Comparison Methods
Make objects comparable with ==, <, >, etc.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
"""Equality: =="""
return self.grade == other.grade
def __lt__(self, other):
"""Less than: <"""
return self.grade < other.grade
def __le__(self, other):
"""Less than or equal: <="""
return self.grade <= other.grade
def __gt__(self, other):
"""Greater than: >"""
return self.grade > other.grade
def __ge__(self, other):
"""Greater than or equal: >="""
return self.grade >= other.grade
def __ne__(self, other):
"""Not equal: !="""
return self.grade != other.grade
def __repr__(self):
return f"Student('{self.name}', {self.grade})"
alice = Student("Alice", 95)
bob = Student("Bob", 87)
charlie = Student("Charlie", 95)
# Comparison operators work!
print(alice == charlie) # True (same grade)
print(alice > bob) # True (95 > 87)
print(bob < alice) # True (87 < 95)
# Sorting works too!
students = [alice, bob, charlie]
students.sort() # Uses __lt__ for sorting
print(students)
# [Student('Bob', 87), Student('Alice', 95), Student('Charlie', 95)]
Shortcut: Use @functools.total_ordering to implement only __eq__ and one comparison:
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
return self.grade == other.grade
def __lt__(self, other):
return self.grade < other.grade
# __le__, __gt__, __ge__, __ne__ automatically generated!
4. Arithmetic Methods
Make objects support math operators.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Addition: +"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Subtraction: -"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Multiplication: *"""
return Vector(self.x * scalar, self.y * scalar)
def __truediv__(self, scalar):
"""Division: /"""
return Vector(self.x / scalar, self.y / scalar)
def __abs__(self):
"""Absolute value: abs()"""
return (self.x ** 2 + self.y ** 2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
# Arithmetic operations work!
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 2) # Vector(6, 8)
print(v1 / 2) # Vector(1.5, 2.0)
print(abs(v1)) # 5.0 (magnitude)
More arithmetic operators:
class Counter:
def __init__(self, value=0):
self.value = value
def __iadd__(self, other):
"""In-place addition: +="""
self.value += other
return self
def __isub__(self, other):
"""In-place subtraction: -="""
self.value -= other
return self
def __repr__(self):
return f"Counter({self.value})"
counter = Counter(10)
counter += 5 # Uses __iadd__
print(counter) # Counter(15)
counter -= 3 # Uses __isub__
print(counter) # Counter(12)
5. Container Methods
Make objects behave like lists, dicts, or other containers.
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def __len__(self):
"""Length: len()"""
return len(self.songs)
def __getitem__(self, index):
"""Indexing: playlist[0]"""
return self.songs[index]
def __setitem__(self, index, value):
"""Assignment: playlist[0] = 'song'"""
self.songs[index] = value
def __delitem__(self, index):
"""Deletion: del playlist[0]"""
del self.songs[index]
def __contains__(self, song):
"""Membership: 'song' in playlist"""
return song in self.songs
def __iter__(self):
"""Iteration: for song in playlist"""
return iter(self.songs)
def __repr__(self):
return f"Playlist('{self.name}', {len(self.songs)} songs)"
playlist = Playlist("My Favorites")
playlist.songs = ["Song A", "Song B", "Song C"]
# Container operations work!
print(len(playlist)) # 3
print(playlist[0]) # "Song A"
print("Song B" in playlist) # True
# Iteration works
for song in playlist:
print(song)
# Output:
# Song A
# Song B
# Song C
# Slicing works too!
print(playlist[1:]) # ['Song B', 'Song C']
6. Callable Objects
Make objects callable like functions using __call__.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
"""Make object callable: multiplier(5)"""
return x * self.factor
# Create multiplier objects
double = Multiplier(2)
triple = Multiplier(3)
# Call like functions!
print(double(5)) # 10
print(triple(5)) # 15
# Check if callable
print(callable(double)) # True
Real-world example: Decorator class
class Logger:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(f"📝 Calling {self.func.__name__}")
result = self.func(*args, **kwargs)
print(f"✅ {self.func.__name__} returned {result}")
return result
@Logger
def add(a, b):
return a + b
result = add(3, 5)
# Output:
# 📝 Calling add
# ✅ add returned 8
7. Context Managers
Implement __enter__ and __exit__ for use with with statement.
class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None
def __enter__(self):
"""Called when entering 'with' block"""
print(f"🔗 Connecting to {self.connection_string}")
self.connection = f"Connection to {self.connection_string}"
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
"""Called when exiting 'with' block"""
print("🔒 Closing connection")
self.connection = None
return False # Don't suppress exceptions
# Use with 'with' statement
with DatabaseConnection("mysql://localhost:3306") as conn:
print(f"💾 Using {conn}")
# Output:
# 🔗 Connecting to mysql://localhost:3306
# 💾 Using Connection to mysql://localhost:3306
# 🔒 Closing connection (automatically!)
Complete Example: Custom List Class
Putting it all together:
class CustomList:
"""A list-like class demonstrating magic methods"""
def __init__(self, items=None):
self.items = items if items else []
# String representation
def __str__(self):
return f"CustomList({self.items})"
def __repr__(self):
return f"CustomList({self.items!r})"
# Container methods
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 __iter__(self):
return iter(self.items)
# Arithmetic
def __add__(self, other):
return CustomList(self.items + other.items)
def __iadd__(self, other):
self.items.extend(other.items)
return self
# Comparison
def __eq__(self, other):
return self.items == other.items
# Callable
def __call__(self, index):
"""Alternative way to get item"""
return self.items[index]
# Test all features
lst = CustomList([1, 2, 3])
print(lst) # CustomList([1, 2, 3])
print(len(lst)) # 3
print(lst[0]) # 1
print(2 in lst) # True
# Iteration
for item in lst:
print(item)
# Addition
lst2 = CustomList([4, 5])
lst3 = lst + lst2
print(lst3) # CustomList([1, 2, 3, 4, 5])
# Callable
print(lst(1)) # 2 (alternative to lst[1])
Magic Methods Cheat Sheet
| Category | Method | Operator/Function |
|---|---|---|
| Initialization | __init__ | Constructor |
__new__ | Object creation | |
__del__ | Destructor | |
| String | __str__ | str(), print() |
__repr__ | repr(), shell | |
| Comparison | __eq__ | == |
__ne__ | != | |
__lt__ | < | |
__le__ | <= | |
__gt__ | > | |
__ge__ | >= | |
| Arithmetic | __add__ | + |
__sub__ | - | |
__mul__ | * | |
__truediv__ | / | |
__floordiv__ | // | |
__mod__ | % | |
| Container | __len__ | len() |
__getitem__ | [] (get) | |
__setitem__ | [] (set) | |
__delitem__ | del [] | |
__contains__ | in | |
__iter__ | for ... in | |
| Callable | __call__ | () |
| Context | __enter__ | with (enter) |
__exit__ | with (exit) |
Best Practices
1. Always Implement __repr__
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name='{self.name}')"
2. Make __str__ Human-Friendly
def __str__(self):
return f"User: {self.name}"
3. Return New Objects (Don't Modify Self)
# ✅ GOOD
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
# ❌ BAD
def __add__(self, other):
self.x += other.x
return self
4. Check Types
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
Conclusion: Write Pythonic Classes
Magic methods let you:
- ✅ Make custom classes behave like built-ins
- ✅ Write intuitive, readable code
- ✅ Integrate seamlessly with Python's syntax
- ✅ Create professional, Pythonic APIs
Remember:
__init__- Initialize objects__str__- Human-readable string__repr__- Developer-friendly representation__eq__,__lt__, etc. - Comparisons__add__,__sub__, etc. - Arithmetic__len__,__getitem__, etc. - Containers
Start using magic methods in your Python classes today!
Building Pythonic applications with magic methods? I'd love to hear about your experience! Connect with me on Twitter or LinkedIn!
Support My Work
If this guide helped you with this topic, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for Python developers.
☕ Buy me a coffee - Every contribution, big or small, means the world to me and keeps me motivated to create more content!
Cover image by Cristian Escobar on Unsplash