Table of Contents
- What Are Dunder Methods?
- Commonly Used Dunder Methods
- Initialization and Construction:
__init__,__new__,__del__ - String Representation:
__str__and__repr__ - Arithmetic Operations:
__add__,__sub__, and More - Comparison Operations:
__eq__,__lt__, and Others - Container Types:
__len__,__getitem__, and Friends - Attribute Access:
__getattr__,__setattr__, and Beyond - Context Managers:
__enter__and__exit__
- Initialization and Construction:
- Practical Example: Building a
ShoppingCartClass - Best Practices for Using Dunder Methods
- Conclusion
- References
What Are Dunder Methods?
Dunder methods (short for “double underscore methods”) are special methods in Python that allow you to define how objects of a class interact with built-in operations and syntax. They are defined with double underscores at the start and end of their names (e.g., __init__, __len__).
Unlike regular methods, dunder methods are not called explicitly by the user. Instead, Python invokes them behind the scenes when you perform actions like:
- Creating an object (
__init__). - Printing an object (
__str__or__repr__). - Adding two objects (
__add__). - Checking the length of an object (
__len__).
In essence, dunder methods let you “teach” Python how to handle your custom objects, making them behave like native types (e.g., lists, integers) and enhancing code readability.
Commonly Used Dunder Methods
Let’s explore the most useful dunder methods and how to implement them.
Initialization and Construction: __init__, __new__, __del__
These methods control object creation, initialization, and cleanup.
__new__(cls, *args, **kwargs): The first method called when creating an object. It creates and returns a new instance of the class. Use this for immutable types (e.g.,int,str) or to customize object creation logic.__init__(self, *args, **kwargs): Initializes the newly created instance (returned by__new__). This is where you set initial attributes.__del__(self): Called when an object is about to be destroyed (garbage collected). Use this for cleanup tasks (e.g., closing files).
Example:
class Person:
def __new__(cls, name, age):
print(f"Creating a new {cls.__name__} instance")
return super().__new__(cls) # Delegate to parent class (object)
def __init__(self, name, age):
print(f"Initializing {name}")
self.name = name
self.age = age
def __del__(self):
print(f"{self.name} is being destroyed")
# Usage
p = Person("Alice", 30) # Output: "Creating a new Person instance" → "Initializing Alice"
del p # Output: "Alice is being destroyed"
String Representation: __str__ and __repr__
These methods control how objects are converted to strings, critical for debugging and user communication.
__str__(self): Returns a human-readable string representation of the object (used bystr(obj)andprint(obj)).__repr__(self): Returns an unambiguous string representation (used byrepr(obj)). Ideally,eval(repr(obj))should recreate the object.
Example:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person: {self.name}, Age: {self.age}" # Readable for users
def __repr__(self):
return f"Person('{self.name}', {self.age})" # Unambiguous for developers
p = Person("Bob", 25)
print(str(p)) # Output: "Person: Bob, Age: 25"
print(repr(p)) # Output: "Person('Bob', 25)"
Arithmetic Operations: __add__, __sub__, and More
Dunder methods let you overload arithmetic operators (e.g., +, -, *) to work with custom objects.
Common arithmetic dunder methods:
__add__(self, other): Handlesself + other__sub__(self, other): Handlesself - other__mul__(self, other): Handlesself * other__truediv__(self, other): Handlesself / other
Example: Vector Addition
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
if not isinstance(other, Vector):
raise TypeError(f"Cannot add Vector and {type(other).__name__}")
return Vector(self.x + other.x, self.y + other.y) # Return new Vector
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(4, 5)
v3 = v1 + v2 # Invokes v1.__add__(v2)
print(v3) # Output: "Vector(6, 8)"
Comparison Operations: __eq__, __lt__, and Others
These methods define how objects are compared with operators like ==, <, >, etc.
Common comparison dunder methods:
__eq__(self, other):self == other__ne__(self, other):self != other(defaults tonot __eq__)__lt__(self, other):self < other__gt__(self, other):self > other(defaults toother < self)
Example: Comparing Person Ages
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.age == other.age # Compare ages
def __lt__(self, other):
if not isinstance(other, Person):
raise TypeError(f"Cannot compare Person and {type(other).__name__}")
return self.age < other.age # Compare ages for "less than"
p1 = Person("Alice", 30)
p2 = Person("Bob", 25)
print(p1 == p2) # Output: False (30 != 25)
print(p1 > p2) # Output: True (30 > 25, invokes p2.__lt__(p1))
Container Types: __len__, __getitem__, and Friends
To make your class behave like a container (e.g., list, dictionary), implement these methods:
__len__(self): Return length (used bylen(obj)).__getitem__(self, key): Access items (used byobj[key]).__setitem__(self, key, value): Modify items (used byobj[key] = value).__delitem__(self, key): Delete items (used bydel obj[key]).__contains__(self, item): Check membership (used byitem in obj).
Example: Custom BookShelf Container
class BookShelf:
def __init__(self):
self.books = [] # Internal storage
def add_book(self, book):
self.books.append(book)
def __len__(self):
return len(self.books) # Number of books
def __getitem__(self, index):
return self.books[index] # Access book by index
def __contains__(self, book):
return book in self.books # Check if book exists
shelf = BookShelf()
shelf.add_book("1984")
shelf.add_book("To Kill a Mockingbird")
print(len(shelf)) # Output: 2 (invokes __len__)
print(shelf[0]) # Output: "1984" (invokes __getitem__)
print("1984" in shelf) # Output: True (invokes __contains__)
Attribute Access: __getattr__, __setattr__, and Beyond
These methods control how attributes are accessed, modified, or deleted, enabling validation or dynamic behavior.
__getattr__(self, name): Called when accessing an undefined attribute.__setattr__(self, name, value): Called when setting an attribute (self.name = value).__getattribute__(self, name): Called always when accessing any attribute (use cautiously!).
Example: Validating Attributes
class ValidatedPerson:
def __init__(self, name, age):
self.name = name # Invokes __setattr__
self.age = age # Invokes __setattr__
def __setattr__(self, name, value):
if name == "age":
if not isinstance(value, int) or value < 0:
raise ValueError("Age must be a non-negative integer")
# Call parent class to actually set the attribute
super().__setattr__(name, value)
p = ValidatedPerson("Charlie", 35)
p.age = -5 # Raises ValueError: "Age must be a non-negative integer"
Context Managers: __enter__ and __exit__
These methods enable objects to work with the with statement, ensuring resources (e.g., files, network connections) are properly managed.
__enter__(self): Called when entering thewithblock; returns the resource to use.__exit__(self, exc_type, exc_val, exc_tb): Called when exiting the block; cleans up the resource.
Example: Safe File Handling
class FileHandler:
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 # Expose the file object to the `with` block
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close() # Ensure file is closed
# Return False to propagate exceptions, True to suppress them
return False
# Usage with `with` statement
with FileHandler("test.txt", "w") as f:
f.write("Hello, Dunder Methods!") # File is auto-closed after this block
Practical Example: Building a ShoppingCart Class
Let’s combine multiple dunder methods to create a ShoppingCart that behaves like a native Python container, supports arithmetic, and has readable output.
class ShoppingCart:
def __init__(self):
self.items = {} # Format: {product_name: quantity}
def add_item(self, product, quantity=1):
self.items[product] = self.items.get(product, 0) + quantity
# Container methods
def __len__(self):
return sum(self.items.values()) # Total items in cart
def __getitem__(self, product):
return self.items.get(product, 0) # Get quantity of a product
# Arithmetic method: Combine two carts
def __add__(self, other):
if not isinstance(other, ShoppingCart):
raise TypeError("Can only add ShoppingCart instances")
combined = ShoppingCart()
for product, qty in self.items.items():
combined.add_item(product, qty)
for product, qty in other.items.items():
combined.add_item(product, qty)
return combined
# String representation
def __str__(self):
return f"Cart with {len(self)} items: {', '.join(self.items.keys())}"
# Usage
cart1 = ShoppingCart()
cart1.add_item("Apple", 2)
cart1.add_item("Banana", 3)
cart2 = ShoppingCart()
cart2.add_item("Apple", 1)
cart2.add_item("Orange", 4)
combined_cart = cart1 + cart2
print(combined_cart) # Output: "Cart with 10 items: Apple, Banana, Orange"
print(combined_cart["Apple"]) # Output: 3 (2 + 1)
Best Practices for Using Dunder Methods
- Follow Pythonic Conventions: Make objects behave like built-in types (e.g.,
__len__returns an integer,__add__returns a new instance). - Prioritize Readability:
__str__should be human-readable;__repr__should be unambiguous (ideally reconstructible witheval). - Handle Edge Cases: In arithmetic/comparison methods, validate input types (e.g.,
if not isinstance(other, Vector): raise TypeError). - Avoid Overuse: Don’t implement dunder methods just because you can. Only add them if they make the class more intuitive.
- Beware of
__getattribute__: Overriding this can lead to infinite recursion (always callsuper().__getattribute__(name)).
Conclusion
Dunder methods are the backbone of Python’s OOP flexibility, allowing you to craft classes that integrate seamlessly with Python’s syntax and built-in functions. From initializing objects to defining custom arithmetic or container behavior, these methods empower you to write clean, Pythonic code. By mastering dunder methods, you’ll create objects that feel native to Python—intuitive, readable, and powerful.