Python Magic methods

Magic methods in Python (also known as dunder methods, short for “double underscore”) are special methods with double underscores before and after their names, like __init__ and __str__. They allow you to define behaviors for built-in operations on your custom objects. Magic methods enable customization of common operations such as adding, printing, or comparing objects, making code cleaner and more intuitive.

some of the most commonly used magic methods:

Initialization (__init__)

The __init__ method is the constructor in Python. It’s called when an object is created, allowing 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__: Defines the informal string representation of an object, shown when using print or str().

__repr__: Defines the official string representation, aimed at developers for debugging. It’s 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 allow objects to interact 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__, __lt__, __gt__, etc.)

These magic methods allow you to 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__)

Defines 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__)

Magic methods for managing attributes dynamically.

__getattr__: Called when trying to access an attribute that doesn’t exist

__setattr__: Controls how attributes are set.

__delattr__: 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__)

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__)

Used for managing resources with with statements, like 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