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

📅 Published: April 20, 2025 ✏️ Updated: May 12, 2025 By Ojaswi Athghara
#python #magic #dunder #methods #oop

Python OOP Magic Methods: init, str, repr Complete Guide

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:

MethodPurposeAudienceCalled By
__str__Human-readableEnd usersprint(), str()
__repr__Developer-friendly, unambiguousDevelopersrepr(), 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

CategoryMethodOperator/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

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

© ojaswiat.com 2025-2027