Skip to main content

Python Magic Methods

Introduction

Have you ever wondered how Python objects know what to do when you use operators like +, -, or functions like len(), str() on them? The answer lies in magic methods, also known as dunder methods (short for "double underscore methods").

Magic methods are special methods in Python that begin and end with double underscores (__). They allow you to define how objects of your custom classes should behave when used with built-in operations, making your objects behave more like Python's built-in types.

In this tutorial, you'll learn:

  • What magic methods are and why they're important
  • How to implement common magic methods
  • How to use magic methods to make your classes more intuitive and Pythonic

What Are Magic Methods?

Magic methods (or dunder methods) are special methods that Python calls internally when performing specific operations. Their names start and end with double underscores.

For example:

  • __init__: Called when creating a new instance of a class
  • __str__: Called when using str() function or when printing an object
  • __add__: Called when using the + operator
  • __len__: Called when using the len() function

Let's explore some of the most commonly used magic methods and how to implement them.

Common Magic Methods

Constructor and Initialization

__init__

The most familiar magic method is probably __init__, which is called when creating a new instance of a class:

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

# When you create a new Person, __init__ is automatically called
person = Person("Alice", 30)
print(person.name) # Output: Alice
print(person.age) # Output: 30

__new__

While __init__ initializes an instance, __new__ is responsible for creating it:

python
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.name) # Output: Second
print(s2.name) # Output: Second
print(s1 is s2) # Output: True

String Representation

__str__

The __str__ method defines the string representation of an object when using str() or print():

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

def __str__(self):
return f"{self.title} by {self.author}"

book = Book("Python Programming", "John Smith")
print(book) # Output: Python Programming by John Smith

__repr__

The __repr__ method returns a string that should ideally return a code representation that could recreate the object:

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

def __str__(self):
return f"{self.title} by {self.author}"

def __repr__(self):
return f"Book('{self.title}', '{self.author}')"

book = Book("Python Programming", "John Smith")
print(str(book)) # Output: Python Programming by John Smith
print(repr(book)) # Output: Book('Python Programming', 'John Smith')

Comparison Operators

You can define how objects should be compared using the following magic methods:

python
class Temperature:
def __init__(self, celsius):
self.celsius = celsius

def __eq__(self, other):
if isinstance(other, Temperature):
return self.celsius == other.celsius
return False

def __lt__(self, other):
if isinstance(other, Temperature):
return self.celsius < other.celsius
return NotImplemented

def __le__(self, other):
if isinstance(other, Temperature):
return self.celsius <= other.celsius
return NotImplemented

t1 = Temperature(25)
t2 = Temperature(30)
t3 = Temperature(25)

print(t1 == t3) # Output: True
print(t1 < t2) # Output: True
print(t2 <= t1) # Output: False

Other comparison methods include __ne__ (!=), __gt__ (>), and __ge__ (>=).

Mathematical Operations

Magic methods make it possible for your objects to respond to mathematical operators:

python
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)
return NotImplemented

def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return NotImplemented

def __mul__(self, scalar):
if isinstance(scalar, (int, float)):
return Vector(self.x * scalar, self.y * scalar)
return NotImplemented

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2) # Output: Vector(4, 6)
print(v2 - v1) # Output: Vector(2, 2)
print(v1 * 3) # Output: Vector(3, 6)

Container Methods

These methods allow your objects to behave like containers (lists, dictionaries, etc.):

python
class Inventory:
def __init__(self):
self.items = {}

def add_item(self, name, quantity):
self.items[name] = quantity

def __getitem__(self, key):
return self.items.get(key, 0)

def __setitem__(self, key, value):
self.items[key] = value

def __contains__(self, key):
return key in self.items

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

inventory = Inventory()
inventory.add_item("apple", 5)
inventory.add_item("banana", 3)

# Using __getitem__
print(inventory["apple"]) # Output: 5
print(inventory["orange"]) # Output: 0

# Using __setitem__
inventory["apple"] = 10
print(inventory["apple"]) # Output: 10

# Using __contains__
print("banana" in inventory) # Output: True
print("orange" in inventory) # Output: False

# Using __len__
print(len(inventory)) # Output: 2

Callable Objects

You can make objects callable like functions with __call__:

python
class Adder:
def __init__(self, n):
self.n = n

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

add5 = Adder(5)
print(add5(10)) # Output: 15
print(add5(20)) # Output: 25

Context Managers

Context managers (used with with statements) use __enter__ and __exit__:

python
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None

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

def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()

# Using our context manager
# Assuming you have a file named "test.txt"
with FileManager("test.txt", "w") as f:
f.write("Hello, World!")

# File is automatically closed after the with block

Real-World Example: A Custom Number Class

Let's create a complete example showing how magic methods make your classes more intuitive to use:

python
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ZeroDivisionError("Denominator cannot be zero")

# Simplify the fraction
gcd = self._find_gcd(abs(numerator), abs(denominator))
self.numerator = numerator // gcd
self.denominator = denominator // gcd

# Handle negative signs
if self.denominator < 0:
self.numerator = -self.numerator
self.denominator = -self.denominator

def _find_gcd(self, a, b):
"""Find the greatest common divisor of a and b."""
while b:
a, b = b, a % b
return a

def __str__(self):
if self.denominator == 1:
return str(self.numerator)
return f"{self.numerator}/{self.denominator}"

def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"

def __add__(self, other):
if isinstance(other, Fraction):
new_num = (self.numerator * other.denominator +
other.numerator * self.denominator)
new_denom = self.denominator * other.denominator
return Fraction(new_num, new_denom)
elif isinstance(other, int):
return self + Fraction(other, 1)
return NotImplemented

def __sub__(self, other):
if isinstance(other, Fraction):
new_num = (self.numerator * other.denominator -
other.numerator * self.denominator)
new_denom = self.denominator * other.denominator
return Fraction(new_num, new_denom)
elif isinstance(other, int):
return self - Fraction(other, 1)
return NotImplemented

def __mul__(self, other):
if isinstance(other, Fraction):
return Fraction(self.numerator * other.numerator,
self.denominator * other.denominator)
elif isinstance(other, int):
return Fraction(self.numerator * other, self.denominator)
return NotImplemented

def __truediv__(self, other):
if isinstance(other, Fraction):
return Fraction(self.numerator * other.denominator,
self.denominator * other.numerator)
elif isinstance(other, int):
return Fraction(self.numerator, self.denominator * other)
return NotImplemented

def __eq__(self, other):
if isinstance(other, Fraction):
return (self.numerator == other.numerator and
self.denominator == other.denominator)
return False

def __float__(self):
return self.numerator / self.denominator

# Using our Fraction class
f1 = Fraction(1, 4) # 1/4
f2 = Fraction(1, 2) # 1/2

print(f1) # Output: 1/4
print(f1 + f2) # Output: 3/4
print(f2 - f1) # Output: 1/4
print(f1 * f2) # Output: 1/8
print(f1 / f2) # Output: 1/2
print(f1 == Fraction(1, 4)) # Output: True
print(float(f1)) # Output: 0.25

Summary

Python's magic methods (dunder methods) allow you to customize how instances of your classes behave with standard Python operations. By implementing these methods, you can make your objects:

  • Behave like built-in types
  • Work with operators like +, -, *, /
  • Support comparison operations
  • Act as containers, callables, or context managers
  • Provide clean string representations

When you're designing a class, think about which operations would make sense for your objects, and implement the corresponding magic methods to make your code more intuitive and Pythonic.

Exercises

  1. Create a Money class that supports addition, subtraction, and comparison operations between different currency amounts.
  2. Implement a custom Matrix class with support for matrix addition, subtraction, and multiplication.
  3. Design a Range class similar to Python's range, but implement the ability to add and subtract ranges.
  4. Create a Temperature class that can convert between Celsius and Fahrenheit and supports comparison operations.

Additional Resources

Understanding and using magic methods effectively is a key step toward writing more idiomatic Python code. As you get more comfortable with them, you'll find that they allow you to express complex behavior in a clean, intuitive way.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)