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 usingstr()
function or when printing an object__add__
: Called when using the+
operator__len__
: Called when using thelen()
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:
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:
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()
:
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:
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:
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:
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.):
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__
:
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__
:
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:
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
- Create a
Money
class that supports addition, subtraction, and comparison operations between different currency amounts. - Implement a custom
Matrix
class with support for matrix addition, subtraction, and multiplication. - Design a
Range
class similar to Python'srange
, but implement the ability to add and subtract ranges. - Create a
Temperature
class that can convert between Celsius and Fahrenheit and supports comparison operations.
Additional Resources
- Python Documentation on Special Method Names
- Python Magic Methods Guide
- Real Python: Object-Oriented Programming in Python 3
- Fluent Python by Luciano Ramalho (book)
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! :)