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!