Python Magic Methods

Python magic methods, also known as special methods or dunder methods (double underscore methods), are powerful tools that allow you to define how your Python objects behave with built-in operations. These Python magic methods enable you to customize fundamental operations like addition, comparison, string representation, and more. Understanding Python magic methods is crucial for creating robust, intuitive classes that integrate seamlessly with Python’s built-in functions and operators.

Python magic methods are automatically invoked by the Python interpreter when specific operations are performed on objects. Every time you use operators like +, -, ==, or functions like len() and str(), Python magic methods are working behind the scenes. By implementing these Python magic methods in your classes, you can make your custom objects behave like built-in Python types.

What Are Python Magic Methods?

Python magic methods are special methods that start and end with double underscores (__). These methods define how objects of your class respond to various operations and built-in functions. Python magic methods are automatically called by the Python interpreter - you rarely call them directly.

class SimpleClass:
    def __init__(self, value):
        self.value = value
    
    def __str__(self):
        return f"SimpleClass with value: {self.value}"

obj = SimpleClass(42)
print(obj)  # Automatically calls __str__ magic method

The __init__ and __str__ methods shown above are examples of Python magic methods. The __init__ method is called when creating new instances, while __str__ defines how the object appears when converted to a string.

Object Creation and Initialization Magic Methods

__new__ Method

The __new__ magic method is responsible for creating new instances of a class. It’s called before __init__ and is particularly useful when you need to control object creation.

class Singleton:
    _instance = None
    
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
    
    def __init__(self, name):
        self.name = name

# Both variables reference the same instance
s1 = Singleton("First")
s2 = Singleton("Second")
print(s1 is s2)  # True

__init__ Method

The __init__ magic method initializes newly created objects. This is the most commonly used Python magic method for setting up instance attributes.

class Student:
    def __init__(self, name, age, grades=None):
        self.name = name
        self.age = age
        self.grades = grades or []
        self.enrollment_id = id(self)

student = Student("Alice", 20, [85, 92, 78])

__del__ Method

The __del__ magic method is called when an object is about to be destroyed. It’s useful for cleanup operations like closing files or network connections.

class FileManager:
    def __init__(self, filename):
        self.filename = filename
        self.file = open(filename, 'w')
    
    def __del__(self):
        if hasattr(self, 'file') and not self.file.closed:
            self.file.close()
            print(f"File {self.filename} has been closed")

manager = FileManager("test.txt")
del manager  # Triggers __del__ method

String Representation Magic Methods

__str__ Method

The __str__ magic method defines the informal string representation of an object, intended for end users. It’s called by str() function and print() statement.

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages
    
    def __str__(self):
        return f"'{self.title}' by {self.author} ({self.pages} pages)"

book = Book("1984", "George Orwell", 328)
print(book)  # '1984' by George Orwell (328 pages)

__repr__ Method

The __repr__ magic method provides the official string representation of an object, intended for developers. It should return a string that could recreate the object.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"
    
    def __str__(self):
        return f"({self.x}, {self.y})"

point = Point(3, 4)
print(repr(point))  # Point(3, 4)
print(str(point))   # (3, 4)

Arithmetic Magic Methods

Python magic methods allow you to define how your objects behave with arithmetic operators. These methods make your custom classes work seamlessly with mathematical operations.

__add__ and __radd__ Methods

The __add__ magic method defines addition behavior, while __radd__ handles reverse addition when the left operand doesn’t support the operation.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
    
    def __radd__(self, other):
        return self.__add__(other)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
v2 = Vector(1, 4)
print(v1 + v2)  # Vector(3, 7)
print(5 + v1)   # Vector(7, 8)

__sub__, __mul__, and __truediv__ Methods

These Python magic methods handle subtraction, multiplication, and division operations respectively.

class Complex:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
    
    def __sub__(self, other):
        return Complex(self.real - other.real, self.imag - other.imag)
    
    def __mul__(self, other):
        if isinstance(other, Complex):
            real = self.real * other.real - self.imag * other.imag
            imag = self.real * other.imag + self.imag * other.real
            return Complex(real, imag)
        else:
            return Complex(self.real * other, self.imag * other)
    
    def __truediv__(self, other):
        if isinstance(other, (int, float)):
            return Complex(self.real / other, self.imag / other)
    
    def __str__(self):
        return f"{self.real} + {self.imag}i"

c1 = Complex(3, 4)
c2 = Complex(1, 2)
print(c1 - c2)  # 2 + 2i
print(c1 * 2)   # 6 + 8i

Comparison Magic Methods

Python magic methods for comparison allow you to define how objects are compared using operators like ==, <, >, etc.

__eq__ and __ne__ Methods

The __eq__ magic method defines equality comparison, while __ne__ defines inequality (though Python automatically provides __ne__ if __eq__ is defined).

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __eq__(self, other):
        if isinstance(other, Person):
            return self.name == other.name and self.age == other.age
        return False
    
    def __hash__(self):
        return hash((self.name, self.age))

person1 = Person("John", 25)
person2 = Person("John", 25)
person3 = Person("Jane", 25)

print(person1 == person2)  # True
print(person1 == person3)  # False

__lt__, __le__, __gt__, __ge__ Methods

These Python magic methods define less than, less than or equal, greater than, and greater than or equal comparisons.

class Grade:
    def __init__(self, score):
        self.score = score
    
    def __lt__(self, other):
        return self.score < other.score
    
    def __le__(self, other):
        return self.score <= other.score
    
    def __gt__(self, other):
        return self.score > other.score
    
    def __ge__(self, other):
        return self.score >= other.score
    
    def __eq__(self, other):
        return self.score == other.score

grade1 = Grade(85)
grade2 = Grade(92)

print(grade1 < grade2)   # True
print(grade1 >= grade2)  # False

Container Magic Methods

These Python magic methods allow your objects to behave like containers, supporting operations like indexing, slicing, and length checking.

__len__ Method

The __len__ magic method defines the behavior of the len() function for your objects.

class Playlist:
    def __init__(self):
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __len__(self):
        return len(self.songs)
    
    def __str__(self):
        return f"Playlist with {len(self)} songs"

playlist = Playlist()
playlist.add_song("Song 1")
playlist.add_song("Song 2")
print(len(playlist))  # 2

__getitem__ and __setitem__ Methods

The __getitem__ magic method enables indexing and slicing, while __setitem__ allows item assignment.

class Matrix:
    def __init__(self, rows, cols):
        self.rows = rows
        self.cols = cols
        self.data = [[0 for _ in range(cols)] for _ in range(rows)]
    
    def __getitem__(self, key):
        row, col = key
        return self.data[row][col]
    
    def __setitem__(self, key, value):
        row, col = key
        self.data[row][col] = value
    
    def __str__(self):
        return '\n'.join([' '.join(map(str, row)) for row in self.data])

matrix = Matrix(3, 3)
matrix[0, 0] = 5
matrix[1, 1] = 10
print(matrix[0, 0])  # 5

__contains__ Method

The __contains__ magic method defines the behavior of the in operator.

class WordSet:
    def __init__(self):
        self.words = set()
    
    def add_word(self, word):
        self.words.add(word.lower())
    
    def __contains__(self, word):
        return word.lower() in self.words

word_set = WordSet()
word_set.add_word("Python")
word_set.add_word("Programming")

print("python" in word_set)     # True
print("java" in word_set)       # False

Iterator Magic Methods

Python magic methods for iteration allow your objects to be used in loops and with iterator functions.

__iter__ and __next__ Methods

The __iter__ magic method makes an object iterable, while __next__ defines what happens on each iteration step.

class NumberSequence:
    def __init__(self, start, end, step=1):
        self.start = start
        self.end = end
        self.step = step
        self.current = start
    
    def __iter__(self):
        self.current = self.start
        return self
    
    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        else:
            self.current += self.step
            return self.current - self.step

sequence = NumberSequence(1, 10, 2)
for num in sequence:
    print(num, end=" ")  # 1 3 5 7 9

Context Manager Magic Methods

Python magic methods for context management enable your objects to work with with statements.

__enter__ and __exit__ Methods

The __enter__ magic method is called when entering a with block, while __exit__ is called when leaving it.

class DatabaseConnection:
    def __init__(self, database_name):
        self.database_name = database_name
        self.connected = False
    
    def __enter__(self):
        print(f"Connecting to {self.database_name}")
        self.connected = True
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Disconnecting from {self.database_name}")
        self.connected = False
        return False  # Don't suppress exceptions
    
    def query(self, sql):
        if self.connected:
            return f"Executing: {sql}"
        else:
            return "Not connected to database"

with DatabaseConnection("myapp_db") as db:
    result = db.query("SELECT * FROM users")
    print(result)

Callable Magic Methods

__call__ Method

The __call__ magic method makes instances of your class callable like functions.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))   # 10
print(triple(4))   # 12

Complete Example: Custom Number Class

Here’s a comprehensive example demonstrating multiple Python magic methods working together to create a custom number class:

import math

class CustomNumber:
    """A custom number class demonstrating various Python magic methods."""
    
    def __init__(self, value):
        """Initialize CustomNumber with a numeric value."""
        if not isinstance(value, (int, float)):
            raise TypeError("Value must be numeric")
        self.value = value
    
    # String representation magic methods
    def __str__(self):
        """Return user-friendly string representation."""
        return f"CustomNumber({self.value})"
    
    def __repr__(self):
        """Return developer-friendly string representation."""
        return f"CustomNumber({self.value!r})"
    
    # Arithmetic magic methods
    def __add__(self, other):
        """Addition operation."""
        if isinstance(other, CustomNumber):
            return CustomNumber(self.value + other.value)
        elif isinstance(other, (int, float)):
            return CustomNumber(self.value + other)
        return NotImplemented
    
    def __radd__(self, other):
        """Reverse addition operation."""
        return self.__add__(other)
    
    def __sub__(self, other):
        """Subtraction operation."""
        if isinstance(other, CustomNumber):
            return CustomNumber(self.value - other.value)
        elif isinstance(other, (int, float)):
            return CustomNumber(self.value - other)
        return NotImplemented
    
    def __mul__(self, other):
        """Multiplication operation."""
        if isinstance(other, CustomNumber):
            return CustomNumber(self.value * other.value)
        elif isinstance(other, (int, float)):
            return CustomNumber(self.value * other)
        return NotImplemented
    
    def __truediv__(self, other):
        """Division operation."""
        if isinstance(other, CustomNumber):
            if other.value == 0:
                raise ZeroDivisionError("Cannot divide by zero")
            return CustomNumber(self.value / other.value)
        elif isinstance(other, (int, float)):
            if other == 0:
                raise ZeroDivisionError("Cannot divide by zero")
            return CustomNumber(self.value / other)
        return NotImplemented
    
    def __pow__(self, other):
        """Power operation."""
        if isinstance(other, CustomNumber):
            return CustomNumber(self.value ** other.value)
        elif isinstance(other, (int, float)):
            return CustomNumber(self.value ** other)
        return NotImplemented
    
    # Comparison magic methods
    def __eq__(self, other):
        """Equality comparison."""
        if isinstance(other, CustomNumber):
            return abs(self.value - other.value) < 1e-10
        elif isinstance(other, (int, float)):
            return abs(self.value - other) < 1e-10
        return False
    
    def __lt__(self, other):
        """Less than comparison."""
        if isinstance(other, CustomNumber):
            return self.value < other.value
        elif isinstance(other, (int, float)):
            return self.value < other
        return NotImplemented
    
    def __le__(self, other):
        """Less than or equal comparison."""
        return self.__lt__(other) or self.__eq__(other)
    
    def __gt__(self, other):
        """Greater than comparison."""
        if isinstance(other, CustomNumber):
            return self.value > other.value
        elif isinstance(other, (int, float)):
            return self.value > other
        return NotImplemented
    
    def __ge__(self, other):
        """Greater than or equal comparison."""
        return self.__gt__(other) or self.__eq__(other)
    
    # Unary operations
    def __neg__(self):
        """Negation operation."""
        return CustomNumber(-self.value)
    
    def __abs__(self):
        """Absolute value operation."""
        return CustomNumber(abs(self.value))
    
    def __round__(self, ndigits=0):
        """Round operation."""
        return CustomNumber(round(self.value, ndigits))
    
    # Type conversion magic methods
    def __int__(self):
        """Convert to integer."""
        return int(self.value)
    
    def __float__(self):
        """Convert to float."""
        return float(self.value)
    
    def __bool__(self):
        """Convert to boolean."""
        return self.value != 0
    
    # Hash method for use in sets and dictionaries
    def __hash__(self):
        """Return hash value."""
        return hash(self.value)

# Demonstration of CustomNumber class
if __name__ == "__main__":
    # Create CustomNumber instances
    num1 = CustomNumber(10.5)
    num2 = CustomNumber(3.2)
    num3 = CustomNumber(0)
    
    # Test string representations
    print(f"String representation: {num1}")
    print(f"Repr representation: {repr(num1)}")
    
    # Test arithmetic operations
    print(f"\nArithmetic Operations:")
    print(f"{num1} + {num2} = {num1 + num2}")
    print(f"{num1} - {num2} = {num1 - num2}")
    print(f"{num1} * {num2} = {num1 * num2}")
    print(f"{num1} / {num2} = {num1 / num2}")
    print(f"{num1} ** 2 = {num1 ** 2}")
    
    # Test operations with regular numbers
    print(f"{num1} + 5 = {num1 + 5}")
    print(f"5 + {num1} = {5 + num1}")
    
    # Test comparison operations
    print(f"\nComparison Operations:")
    print(f"{num1} == {num2}: {num1 == num2}")
    print(f"{num1} > {num2}: {num1 > num2}")
    print(f"{num1} < {num2}: {num1 < num2}")
    print(f"{num1} >= {num2}: {num1 >= num2}")
    
    # Test unary operations
    print(f"\nUnary Operations:")
    print(f"Negative of {num1}: {-num1}")
    print(f"Absolute of {-num1}: {abs(-num1)}")
    print(f"Rounded {num1}: {round(num1)}")
    
    # Test type conversions
    print(f"\nType Conversions:")
    print(f"Integer value of {num1}: {int(num1)}")
    print(f"Float value of {num1}: {float(num1)}")
    print(f"Boolean value of {num1}: {bool(num1)}")
    print(f"Boolean value of {num3}: {bool(num3)}")
    
    # Test usage in collections
    number_set = {num1, num2, CustomNumber(10.5)}
    print(f"\nSet with CustomNumbers: {number_set}")
    print(f"Length of set (duplicates removed): {len(number_set)}")
    
    # Test in conditional statements
    if num1:
        print(f"{num1} is truthy")
    
    if not num3:
        print(f"{num3} is falsy")

Expected Output:

String representation: CustomNumber(10.5)
Repr representation: CustomNumber(10.5)

Arithmetic Operations:
CustomNumber(10.5) + CustomNumber(3.2) = CustomNumber(13.7)
CustomNumber(10.5) - CustomNumber(3.2) = CustomNumber(7.3)
CustomNumber(10.5) * CustomNumber(3.2) = CustomNumber(33.6)
CustomNumber(10.5) / CustomNumber(3.2) = CustomNumber(3.28125)
CustomNumber(10.5) ** 2 = CustomNumber(110.25)
CustomNumber(10.5) + 5 = CustomNumber(15.5)
5 + CustomNumber(10.5) = CustomNumber(15.5)

Comparison Operations:
CustomNumber(10.5) == CustomNumber(3.2): False
CustomNumber(10.5) > CustomNumber(3.2): True
CustomNumber(10.5) < CustomNumber(3.2): False
CustomNumber(10.5) >= CustomNumber(3.2): True

Unary Operations:
Negative of CustomNumber(10.5): CustomNumber(-10.5)
Absolute of CustomNumber(-10.5): CustomNumber(10.5)
Rounded CustomNumber(10.5): CustomNumber(10)

Type Conversions:
Integer value of CustomNumber(10.5): 10
Float value of CustomNumber(10.5): 10.5
Boolean value of CustomNumber(10.5): True
Boolean value of CustomNumber(0): False

Set with CustomNumbers: {CustomNumber(3.2), CustomNumber(10.5)}
Length of set (duplicates removed): 2
CustomNumber(10.5) is truthy
CustomNumber(0) is falsy

This comprehensive example showcases how Python magic methods enable you to create custom classes that integrate seamlessly with Python’s built-in operations and functions. By implementing these Python magic methods, your objects can behave like native Python types, making your code more intuitive and pythonic.

The CustomNumber class demonstrates practical applications of Python magic methods including arithmetic operations, comparisons, string representations, type conversions, and collection usage. Understanding and implementing these Python magic methods will significantly enhance your object-oriented programming skills and allow you to create more sophisticated, user-friendly classes.