Python Magic Methods

Magic methods in Python, also called dunder methods for “double underscore,” are special methods that have double underscores before and after their names, like __init__ and __str__. They let you define behaviors for built-in operations on your custom objects. Magic methods help you customize common operations such as adding, printing, or comparing objects. This makes your code cleaner and more intuitive.

Some of the most commonly used magic methods:

Initialization (__init__)

The __init__ method is the constructor in Python. It is called when you create an object. This allows you to initialize attributes.


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("John", 35)  # __init__ is called here
print(p.name)            # Output: John

String Representation (__str__ and __repr__)

__str__: This defines the informal string representation of an object. It is shown when using print or str().

__repr__: This defines the official string representation, which is meant for developers during debugging. It is called by repr() and in the interactive shell.


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"{self.name}, {self.age} years old"

    def __repr__(self):
        return f"Person('{self.name}', {self.age})"

p = Person("John", 35)
print(str(p))    # Output: John, 35 years old
print(repr(p))   # Output: Person('John', 35)

Arithmetic Operators (__add__, __sub__, etc.)

Magic methods enable objects to work with basic arithmetic operations.


class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __str__(self):
        return f"({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(4, 5)
print(v1 + v2)   # Output: (6, 8)

Comparison Operators (__eq__, __it__, __gt__, etc.)

These magic methods let you define custom behavior for comparison operations.


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        return self.age == other.age

    def __lt__(self, other):
        return self.age < other.age

p1 = Person("John", 30)
p2 = Person("Bob", 25)
print(p1 == p2)  # Output: False
print(p1 < p2)   # Output: False

Length (__len__)

This method defines the behavior for the len() function.


class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __len__(self):
        return self.pages

book = Book("My Python", 350)
print(len(book))  # Output: 350

Attribute Access (__getattr__, __setattr__, __delattr__)

These magic methods manage attributes dynamically.

__getattr__: This is called when you try to access an attribute that doesn’t exist.

__setattr__: This controls how attributes are set.

__delattr__: This controls how attributes are deleted.


class Person:
    def __init__(self, name):
        self.name = name

    def __getattr__(self, attr):
        return f"{attr} not found."

p = Person("John")
print(p.age)  # Output: age not found.

Callable (__call__)

This makes an instance of a class callable like a function.


class Greeter:
    def __init__(self, name):
        self.name = name

    def __call__(self, greeting):
        return f"{greeting}, {self.name}!"

greet = Greeter("John")
print(greet("Hello"))  # Output: Hello, John!

Context Management (__enter__ and __exit__)

These methods manage resources with with statements, such as file handling or database connections.


class FileHandler:
    def __init__(self, filename):
        self.filename = filename

    def __enter__(self):
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, traceback):
        self.file.close()

with FileHandler("test.txt") as file:
    file.write("Hello, World!")

Iterator Protocol (__iter__ and __next__)

These methods allow an object to be used as an iterator.


class Counter:
    def __init__(self, max):
        self.max = max
        self.current = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.max:
            self.current += 1
            return self.current
        raise StopIteration

counter = Counter(3)
for num in counter:
    print(num)  # Output: 1 2 3