Python magic methods, often referred to as dunder methods (short for "double underscore"), are special methods that enable customization of Python's built-in operations. For instance, when you write a + b, Python internally calls a.__add__(b). This is one of many magic methods that can be implemented in your custom classes to enable various behaviors.
__init__(self, ...): Initializes an object after it's created.__str__(self): Defines how the object should be represented as a string (used by print() and str()).__repr__(self): Returns a detailed representation of the object, useful for debugging.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(title={self.title!r}, author={self.author!r})"
book = Book("1984", "George Orwell")
print(book) # Output: '1984' by George Orwell
print(repr(book)) # Output: Book(title='1984', author='George Orwell')
You can control how attributes are accessed, set, and deleted by defining these methods:
__getattr__(self, name): Invoked when an attribute is accessed that does not exist.__getattribute__(self, name): Controls access to all attributes.__setattr__(self, name, value): Controls how attributes are set.__delattr__(self, name): Defines what happens when an attribute is deleted.class Person:
def __init__(self, name):
self.name = name
def __getattr__(self, item):
return f"{item} attribute not found."
def __setattr__(self, name, value):
if name == 'name' and not value.isalpha():
raise ValueError("Name must contain only letters.")
super().__setattr__(name, value)
person = Person("John")
print(person.age) # Output: age attribute not found.
person.name = "J0hn" # Raises ValueError: Name must contain only letters.
Magic methods can be used to overload arithmetic operators like +, -, *, etc.
__add__(self, other): Called for + operator.__sub__(self, other): Called for - operator.__mul__(self, other): Called for * operator.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 __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 5)
print(v1 + v2) # Output: Vector(3, 8)
Magic methods for comparison include:
__eq__(self, other): For ==.__lt__(self, other): For <.__le__(self, other): For <=.class Box:
def __init__(self, size):
self.size = size
def __eq__(self, other):
return self.size == other.size
def __lt__(self, other):
return self.size < other.size
box1 = Box(10)
box2 = Box(20)
print(box1 == box2) # Output: False
print(box1 < box2) # Output: True
To make your objects behave like containers (e.g., lists, dictionaries), implement:
__len__(self): For len().__getitem__(self, key): For indexing.__setitem__(self, key, value): For setting values.__delitem__(self, key): For deleting items.class CustomList:
def __init__(self):
self.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
def __delitem__(self, index):
del self.data[index]
lst = CustomList()
lst.data = [1, 2, 3]
print(len(lst)) # Output: 3
print(lst[0]) # Output: 1
lst[1] = 10
del lst[2]
print(lst.data) # Output: [1, 10]
To enable an object to be used in a with statement, define:
__enter__(self): Called when the context is entered.__exit__(self, exc_type, exc_value, traceback): Called when the context exits, even if an exception is raised.class Resource:
def __enter__(self):
print("Acquiring resource")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Releasing resource")
with Resource():
print("Using resource")
# Output:
# Acquiring resource
# Using resource
# Releasing resource
__str__ and __repr__ to provide meaningful string representations of your objects.__enter__ and __exit__ for classes that require setup and teardown operations, such as file handling, database connections, etc.a < b is True, then b > a should also be True).__eq__ without __hash__: If you override __eq__, consider also overriding __hash__ to ensure the object behaves correctly in hash-based collections like sets or as dictionary keys.__getattribute__ and __getitem__ can be called frequently. Ensure they are optimized to avoid performance bottlenecks.__getattr__ and __getattribute__: __getattr__ should only be used for attributes that don't exist, and __getattribute__ should be used with caution to avoid infinite loops.