Table of Contents
- What Are Magic Methods?
- Initialization & Construction:
__new__and__init__ - String Representation:
__str__and__repr__ - Operator Overloading
- Attribute Access Control
- Emulating Container Types
- Context Managers:
__enter__and__exit__ - Advanced Magic Methods
- Best Practices for Using Magic Methods
- Conclusion
- References
What Are Magic Methods?
Magic methods are predefined methods in Python that start and end with double underscores (__method__). They are not meant to be called directly by the user; instead, Python invokes them implicitly in response to specific operations. For example, when you use print(obj), Python calls obj.__str__() behind the scenes.
These methods define how objects interact with Python’s core features, such as:
- Initialization (
__init__) - String conversion (
__str__,__repr__) - Operator operations (
+,-,==, etc.) - Attribute access (
obj.attr) - Container behavior (e.g.,
len(obj),obj[0])
By overriding these methods, you can make your custom classes behave like built-in types (e.g., int, list), leading to more expressive and maintainable code.
Initialization & Construction: __new__ and __init__
Two fundamental magic methods control object creation and initialization: __new__ and __init__.
__new__: The Constructor
__new__ is responsible for creating the instance. It is a static method (implicitly) that takes the class as its first argument and returns a new instance of that class. It is rarely overridden unless you need to customize object creation (e.g., for singletons or immutable types like int or str).
__init__: The Initializer
__init__ is responsible for initializing the instance. It takes the newly created instance (self) as its first argument and sets up attributes. This is the method you’ll override most often.
Example: Basic Initialization
class Person:
def __new__(cls, *args, **kwargs):
# Create a new instance (rarely overridden)
print(f"Creating a new {cls.__name__} instance...")
instance = super().__new__(cls)
return instance
def __init__(self, name, age):
# Initialize the instance
self.name = name
self.age = age
print(f"Initialized {self.name}, {self.age} years old.")
# Usage
person = Person("Alice", 30)
# Output:
# Creating a new Person instance...
# Initialized Alice, 30 years old.
Key Note: __new__ is called before __init__. If __new__ returns an instance of the class, __init__ is called with that instance as self.
String Representation: __str__ and __repr__
Python provides two magic methods to define how objects are converted to strings: __str__ and __repr__.
__str__: User-Friendly String
__str__ is called by str(obj) and print(obj). It should return a human-readable string representation of the object.
__repr__: Developer-Friendly String
__repr__ is called by repr(obj) and in debuggers. It should return a string that, when passed to eval(), recreates the object (or a unambiguous description if that’s not possible).
Example: String Representation
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
# Human-readable format
return f"{self.name} ({self.age})"
def __repr__(self):
# Developer-readable format (reproducible)
return f"Person(name='{self.name}', age={self.age})"
# Usage
alice = Person("Alice", 30)
print(alice) # Calls __str__ → "Alice (30)"
print(repr(alice)) # Calls __repr__ → "Person(name='Alice', age=30)"
Best Practice: Always define __repr__ (it’s used as a fallback if __str__ is missing). For __repr__, aim for eval(repr(obj)) == obj when possible.
Operator Overloading
Magic methods let you define how objects respond to operators like +, -, ==, or <. This is called “operator overloading.”
Arithmetic Operators
To overload arithmetic operators, override methods like __add__ (for +), __sub__ (for -), __mul__ (for *), etc. These methods take self and other as arguments and return the result of the operation.
Example: Vector Addition
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
# Overload +: Vector + Vector
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
raise TypeError("Unsupported operand type(s) for +: 'Vector' and '{}'".format(type(other).__name__))
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# Usage
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Calls v1.__add__(v2)
print(v3) # Output: Vector(6, 8)
Comparison Operators
To compare objects with ==, !=, <, >, etc., override methods like __eq__ (equality), __ne__ (inequality), __lt__ (less than), __gt__ (greater than), etc.
Example: Comparing Vectors
class Vector:
# ... (previous __init__ and __add__ methods)
def __eq__(self, other):
# Overload ==: Check if two vectors are equal
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y
def __lt__(self, other):
# Overload <: Compare magnitudes (sqrt(x² + y²))
if not isinstance(other, Vector):
raise TypeError("'<' not supported between instances of 'Vector' and '{}'".format(type(other).__name__))
self_mag = (self.x **2 + self.y** 2) **0.5
other_mag = (other.x** 2 + other.y **2)** 0.5
return self_mag < other_mag
# Usage
v1 = Vector(2, 3)
v2 = Vector(2, 3)
v3 = Vector(1, 1)
print(v1 == v2) # True (calls __eq__)
print(v3 < v1) # True (v3's magnitude ≈1.414 < v1's ≈3.606)
Attribute Access Control
Magic methods like __getattr__, __setattr__, __delattr__, and __getattribute__ let you control how attributes are accessed, modified, or deleted.
__getattr__: Handle Missing Attributes
__getattr__ is called when an attribute is not found via normal lookup (e.g., obj.attr where attr doesn’t exist).
Example: Dynamic Attribute Lookup
class DynamicAttributes:
def __init__(self):
self.data = {"name": "Alice", "age": 30}
def __getattr__(self, name):
# Called when 'name' is not an instance attribute
if name in self.data:
return self.data[name]
raise AttributeError(f"'DynamicAttributes' object has no attribute '{name}'")
# Usage
obj = DynamicAttributes()
print(obj.name) # Found in data → "Alice"
print(obj.age) # Found in data → 30
print(obj.email) # Not found → AttributeError
__setattr__: Control Attribute Assignment
__setattr__ is called when assigning an attribute (e.g., obj.attr = value). Use it to validate or log assignments.
Example: Validate Attribute Values
class Person:
def __init__(self, name, age):
self.name = name # Calls __setattr__
self.age = age # Calls __setattr__
def __setattr__(self, name, value):
if name == "age" and (not isinstance(value, int) or value < 0):
raise ValueError("Age must be a non-negative integer")
# Use super().__setattr__ to avoid recursion!
super().__setattr__(name, value)
# Usage
person = Person("Alice", 30)
person.age = -5 # Raises ValueError: Age must be a non-negative integer
Emulating Container Types
You can make your objects behave like built-in containers (e.g., list, dict) by overriding methods like __len__, __getitem__, __setitem__, or __iter__.
Example: Custom List
class CustomList:
def __init__(self, items=None):
self.items = items or []
def __len__(self):
# Called by len(obj)
return len(self.items)
def __getitem__(self, index):
# Called by obj[index]
return self.items[index]
def __setitem__(self, index, value):
# Called by obj[index] = value
self.items[index] = value
def __iter__(self):
# Called by iter(obj) (e.g., for loops)
return iter(self.items)
# Usage
my_list = CustomList([1, 2, 3])
print(len(my_list)) # 3 (calls __len__)
print(my_list[1]) # 2 (calls __getitem__)
my_list[0] = 10 # Calls __setitem__
for item in my_list: # Calls __iter__
print(item) # 10, 2, 3
Context Managers: __enter__ and __exit__
Context managers (used with the with statement) ensure resources are properly managed (e.g., files, network connections). They rely on __enter__ and __exit__.
__enter__: Called when entering thewithblock. Returns the resource to use.__exit__: Called when exiting the block (even if an error occurs). Cleans up the resource.
Example: File Handler Context Manager
class FileHandler:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
# Open the file and return it
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
# Close the file, even if an error occurred
if self.file:
self.file.close()
# Return False to propagate exceptions, True to suppress them
return False
# Usage
with FileHandler("example.txt", "w") as f:
f.write("Hello, Context Managers!")
# File is automatically closed when exiting the block
Advanced Magic Methods
__call__: Making Instances Callable
Override __call__ to make instances of your class callable like functions.
Example: Callable Counter
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
# Usage
counter = Counter()
print(counter()) # 1 (calls __call__)
print(counter()) # 2
print(counter.count) # 2
__hash__: Hashing for Dictionaries
To use objects as keys in dictionaries or elements in sets, they must be hashable. Define __hash__ (along with __eq__) to control hashing.
Example: Hashable Person
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return isinstance(other, Person) and self.name == other.name and self.age == other.age
def __hash__(self):
# Use a tuple of hashable attributes for hashing
return hash((self.name, self.age))
# Usage
person1 = Person("Alice", 30)
person2 = Person("Alice", 30)
person_dict = {person1: "Engineer"}
print(person_dict[person2]) # "Engineer" (person1 and person2 are equal and hash the same)
Note: If __eq__ is defined, __hash__ must also be defined (or set to None for unhashable objects).
Best Practices for Using Magic Methods
- Follow the “Pythonic” Contract: Magic methods have implicit contracts (e.g.,
__add__should return a new instance, not modifyself). Adhere to these to avoid confusion. - Don’t Overuse: Only override magic methods when necessary. For example, don’t define
__eq__if object identity (is) is sufficient. - Handle Edge Cases: In operator methods, check if
otheris of the correct type (e.g.,isinstance(other, Vector)in__add__). - Document Behavior: Clearly document what magic methods do (e.g., “
__add__returns a new Vector with summed components”).
Conclusion
Magic methods are a powerful feature of Python OOP that let you customize how objects interact with the language’s core functionality. By overriding these methods, you can create classes that feel natural to use, mimicking the behavior of built-in types. From initialization to operator overloading, attribute control, and context management, magic methods unlock endless possibilities for writing clean, expressive, and Pythonic code.
Experiment with the examples in this blog, and refer to Python’s official documentation for a full list of magic methods!
References
- Python Data Model (Official Documentation)
- Real Python: Magic Methods in Python
- Fluent Python by Luciano Ramalho (Chapter 1: The Python Data Model)