Python Magic Methods

Magic methods in Python are also called “dunder” methods, which is short for “double underscores.” These are special methods with a specific naming convention: they start and end with double underscores, like _init_, _str_, and _add_. These methods allow you to define behaviors for your classes that integrate seamlessly with Python’s built-in syntax and operations.

Magic methods should not be invoked directly. Instead, Python will internally invoke the magic method if you call certain things or use particular syntax. This is an in-depth listing of some common magic methods grouped by the purpose that each serves:

1. Initialization and Representation

Initialization: __init__

  • Called when an instance of a class is created.
  • Used to initialize the object’s attributes.
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

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

Representation: __str__ and __repr__

  • __str__: Defines the string representation of an object for users (print or str()).
  • __repr__: Defines the string representation for developers (repr() or interactive shell).
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

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

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

p = Person("Alice", 30)
print(p)  # Output: Person: Alice, 30 years old (from __str__)
repr(p)   # Output: Person(name='Alice', age=30) (from __repr__)

2. Arithmetic Operations

Binary Operators

  • __add__: Implements +
  • __sub__: Implements -
  • __mul__: Implements *
  • __truediv__: Implements /
  • __floordiv__: Implements //
  • __mod__: Implements %
  • __pow__: Implements **
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"Vector({self.x}, {self.y})"

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

Unary Operators

  • __neg__: Implements unary - (negation)
  • __pos__: Implements unary +
  • __abs__: Implements abs()

3. Comparison Operators

Comparison Methods

  • __eq__: Implements ==
  • __ne__: Implements !=
  • __lt__: Implements <
  • __le__: Implements <=
  • __gt__: Implements >
  • __ge__: Implements >=
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

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

p1 = Person("Alice", 30)
p2 = Person("Bob", 30)
print(p1 == p2)  # Output: True (calls __eq__)

4. Attribute Access

Custom Attribute Handling

  • getattr: When the attribute is accessed, it is called.
  • setattr: When an attribute is assigned, it is called.
  • delattr: When an attribute is deleted, it is called.
class Person:
    def __init__(self, name):
        self.name = name

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

p = Person("Alice")
print(p.age)  # Output: age not found (calls __getattr__)

5. Container Emulation

Methods for Collections

  • __len__: Implements len()
  • __getitem__: Implements indexing (obj[key])
  • __setitem__: Implements assignment to indexed items (obj[key] = value)
  • __delitem__: Implements item deletion (del obj[key])
  • __contains__: Implements membership test (in)
class CustomList:
    def __init__(self, data):
        self.data = data

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]

    def __setitem__(self, index, value):
        self.data[index] = value

my_list = CustomList([1, 2, 3])
print(len(my_list))   # Output: 3 (calls __len__)
print(my_list[1])     # Output: 2 (calls __getitem__)
my_list[1] = 42       # __setitem__ modifies the list
print(my_list[1])     # Output: 42

6. Callable Objects

__call__

  • Makes an instance callable like a function.
class Adder:
    def __init__(self, value):
        self.value = value

    def __call__(self, x):
        return self.value + x

add_five = Adder(5)
print(add_five(10))  # Output: 15 (calls __call__)

7. Context Management

__enter__ and __exit__

  • Implemented for use with with statements.
class FileManager:
    def __init__(self, filename, mode):
        self.file = open(filename, mode)

    def __enter__(self):
        return self.file

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

with FileManager("test.txt", "w") as f:
    f.write("Hello, World!")

8. Miscellaneous

  • __iter__: Makes an object iterable.
  • __next__: Implements iteration (next()).
  • __del__: Defines destructor behavior (called when the object is deleted).
class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def __next__(self):
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

for num in Counter(1, 5):
    print(num)  # Output: 1, 2, 3, 4, 5

Why Use Magic Methods?

  1. Readable Code: They let your special classes interact smoothly with built-in syntax for things like arithmetic and iteration.
  2. Custom Behavior: Define how your objects behave in specific situations.
  3. Pythonic Design: Leverage the strengths of Python, such as duck typing and operator overloading.