Learn Python Magic Methods: A Simple Explanation
Understanding Magic Methods in Python
Magic methods in Python, also known as dunder methods (because they have double underscores at the beginning and end of their names), allow us to define the behavior of our objects for various operations. They enable custom behavior and can make our classes act like built-in types. In this blog, we will explore different categories of magic methods, provide detailed explanations, and give practical examples and use cases.
1. Attribute Access Methods
These magic methods control how attributes of your objects are accessed, modified, or deleted.
__getattr__
and __getattribute__
__getattr__
: Called when an attribute is not found in an object.__getattribute__
: Called unconditionally to access any attribute.
Example: Custom Attribute Access with Logging
class LoggedAttributes:
def __init__(self, name):
self.name = name
def __getattr__(self, item):
print(f"Accessing non-existent attribute: {item}")
return None
def __getattribute__(self, item):
print(f"Getting attribute: {item}")
return super().__getattribute__(item)
# Usage
obj = LoggedAttributes("Alice")
print(obj.name) # Output: Getting attribute: name\nAlice
print(obj.age) # Output: Accessing non-existent attribute: age\nNone
Practical Use Case: Logging attribute access in a debugging scenario to trace when and how attributes are accessed or modified.
__setattr__
and __delattr__
__setattr__
: Called when an attribute assignment is attempted.__delattr__
: Called when an attribute deletion is attempted.
Example: Custom Attribute Modification with Validation
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __setattr__(self, key, value):
if key == "age" and value < 0:
raise ValueError("Age cannot be negative")
super().__setattr__(key, value)
def __delattr__(self, item):
if item == "name":
raise AttributeError("Can't delete attribute 'name'")
super().__delattr__(item)
# Usage
p = Person("Alice", 30)
p.age = 25 # Works fine
# p.age = -1 # Raises ValueError
# del p.name # Raises AttributeError
Practical Use Case: Enforcing validation rules or restrictions when setting or deleting attributes.
2. Container Methods
These magic methods allow your objects to behave like containers (lists, dictionaries, etc.).
__len__
, __getitem__
, __setitem__
, __delitem__
, and __iter__
__len__
: Returns the length of the container.__getitem__
: Retrieves an item at a given index or key.__setitem__
: Sets an item at a given index or key.__delitem__
: Deletes an item at a given index or key.__iter__
: Returns an iterator object.
Example: Custom List-like Object
class CustomList:
def __init__(self):
self._items = []
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
def __iter__(self):
return iter(self._items)
def append(self, item):
self._items.append(item)
# Usage
cl = CustomList()
cl.append(1)
cl.append(2)
cl.append(3)
print(len(cl)) # Output: 3
print(cl[1]) # Output: 2
for item in cl:
print(item) # Output: 1 2 3
Practical Use Case: Creating a custom collection class that needs specialized behavior or additional methods while still supporting standard list operations.
3. Numeric and Comparison Methods
These methods define how objects of your class interact with numeric operations and comparisons.
Numeric Methods
__add__
,__sub__
,__mul__
,__truediv__
,__floordiv__
,__mod__
,__pow__
: Define arithmetic operations.
Example: Custom Complex Number Class
class Complex:
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __repr__(self):
return f"({self.real} + {self.imag}i)"
# Usage
c1 = Complex(1, 2)
c2 = Complex(3, 4)
print(c1 + c2) # Output: (4 + 6i)
print(c1 - c2) # Output: (-2 + -2i)
Practical Use Case: Implementing custom numeric types like complex numbers, vectors, or matrices.
Comparison Methods
__eq__
,__ne__
,__lt__
,__le__
,__gt__
,__ge__
: Define comparison operations.
Example: Implementing Total Ordering for a Custom Class
from functools import total_ordering
@total_ordering
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __eq__(self, other):
return (self.title, self.author) == (other.title, other.author)
def __lt__(self, other):
return (self.title, self.author) < (other.title, other.author)
def __repr__(self):
return f"{self.title} by {self.author}"
# Usage
book1 = Book("Title1", "Author1")
book2 = Book("Title2", "Author2")
books = [book2, book1]
print(sorted(books)) # Output: [Title1 by Author1, Title2 by Author2]
Practical Use Case: Enabling custom objects to be sorted or compared, useful in data structures like heaps, binary search trees, or simply when sorting lists of custom objects.
4. Container Methods: Practical Use Case
Custom Dictionary with Case-Insensitive Keys
Creating a dictionary-like object that treats keys as case-insensitive.
Example: Case-Insensitive Dictionary
class CaseInsensitiveDict:
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key.lower()]
def __setitem__(self, key, value):
self._data[key.lower()] = value
def __delitem__(self, key):
del self._data[key.lower()]
def __contains__(self, key):
return key.lower() in self._data
def keys(self):
return self._data.keys()
def items(self):
return self._data.items()
def values(self):
return self._data.values()
# Usage
cid = CaseInsensitiveDict()
cid["Name"] = "Alice"
print(cid["name"]) # Output: Alice
print("NAME" in cid) # Output: True
Practical Use Case: Creating dictionaries where keys should be treated as case-insensitive, useful for handling user inputs, configuration settings, etc.
Conclusion
Magic methods provide a powerful way to customize the behavior of your objects in Python. Understanding and effectively using these methods can make your classes more intuitive and integrate seamlessly with Python's built-in functions and operators. Whether you're implementing custom numeric types, containers, or attribute access patterns, magic methods can greatly enhance the flexibility and functionality of your code
Subscribe to my newsletter
Read articles from Tarun Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Tarun Sharma
Tarun Sharma
Hi there! I’m Tarun, a Senior Software Engineer with a passion for technology and coding. With experience in Python, Java, and various backend development practices, I’ve spent years honing my skills and working on exciting projects. On this blog, you’ll find insights, tips, and tutorials on topics ranging from object-oriented programming to tech trends and interview prep. My goal is to share valuable knowledge and practical advice to help fellow developers grow and succeed. When I’m not coding, you can find me exploring new tech trends, working on personal projects, or enjoying a good cup of coffee. Thanks for stopping by, and I hope you find my content helpful!