Python Dunder Methods
Make your objects behave like built-in Python types
What are Dunder Methods?
Dunder methods (short for double underscore) are special methods like __init__ and __str__ that let your classes integrate seamlessly with Python’s syntax and built-in functions.
__new__ and __init__: Object Creation
__new__ creates the object instance before __init__ initializes it. Use __new__ for immutable types or singleton patterns.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.data = []
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True - same instance__new__ returns the instance, __init__ initializes it__str__ and __repr__: String Representation
These methods control how objects are displayed as strings.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name}: ${self.price}"
def __repr__(self):
return f"Product(name={self.name!r}, price={self.price})"
p = Product("Laptop", 1200)
print(p) # Laptop: $1200
print([p]) # [Product(name='Laptop', price=1200)]__str__ → user-friendly, __repr__ → developer/debuggingComparison Methods
Implement rich comparison operators: ==, !=,<, <=, >, >=
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
return self.grade == other.grade
def __lt__(self, other):
return self.grade < other.grade
def __le__(self, other):
return self.grade <= other.grade
def __gt__(self, other):
return self.grade > other.grade
def __ge__(self, other):
return self.grade >= other.grade
s1 = Student("Alice", 95)
s2 = Student("Bob", 87)
print(s1 > s2) # True
print(s1 == s2) # False
print(sorted([s1, s2], key=lambda s: s.grade)) # Works!@functools.total_ordering decorator to auto-generate comparison methods from just __eq__ and one other!Arithmetic Operator Overloading
Dunder methods let objects respond to operators like +,-, *, and /.
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 __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)__enter__ and __exit__: Context Managers
Make your objects work with the with statement for automatic resource management.
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Opening connection to {self.db_name}")
self.connection = f"Connected to {self.db_name}"
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.db_name}")
self.connection = None
return False # Don't suppress exceptions
with DatabaseConnection("mydb") as db:
print(db.connection)
# Connection automatically closed after with block__exit__ receives exception info and can suppress exceptions by returning True__len__, __getitem__, __setitem__: Container Protocol
Make your objects behave like lists, dictionaries, or other containers.
class PlayList:
def __init__(self):
self.songs = []
def __len__(self):
return len(self.songs)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
self.songs[index] = value
def __contains__(self, item):
return item in self.songs
playlist = PlayList()
playlist.songs = ["Song A", "Song B", "Song C"]
print(len(playlist)) # 3
print(playlist[1]) # Song B
print("Song A" in playlist) # True
playlist[0] = "New Song" # Works!__call__: Make Objects Callable
Allow instances to be called like functions using __call__.
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(callable(double)) # True__getattr__, __setattr__, __delattr__: Attribute Access
Control how attributes are accessed, set, and deleted on your objects.
class Config:
def __init__(self):
self._data = {}
def __getattr__(self, name):
# Called when attribute not found normally
return self._data.get(name, f"No config for {name}")
def __setattr__(self, name, value):
if name == "_data":
super().__setattr__(name, value)
else:
self._data[name] = value
def __delattr__(self, name):
if name in self._data:
del self._data[name]
config = Config()
config.timeout = 30
config.debug = True
print(config.timeout) # 30
print(config.unknown) # No config for unknown__setattr__ to avoid infinite recursion!__bool__ and __len__: Truthiness
Control how objects behave in boolean contexts and with len().
class ShoppingCart:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
def __bool__(self):
return len(self.items) > 0
cart = ShoppingCart()
print(len(cart)) # 0
print(bool(cart)) # False
if cart:
print("Cart has items")
else:
print("Cart is empty") # This prints__hash__ and __eq__: Hashable Objects
Make objects usable as dictionary keys and in sets.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
def __repr__(self):
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
# Can use as dict keys
positions = {p1: "start"}
print(positions[p2]) # "start" - works because same hash
# Can use in sets
points = {Point(1, 2), Point(3, 4), Point(1, 2)}
print(len(points)) # 2 - duplicates removedKey Takeaways
__new__creates instances,__init__initializes them- Comparison methods enable sorting and ordering of custom objects
- Context managers with
__enter__/__exit__handle resources cleanly - Container methods make objects behave like lists or dictionaries
__call__makes objects callable like functions- Attribute access methods provide dynamic behavior
__hash__and__eq__enable use in sets and as dict keys- Well-implemented dunders make your code feel truly Pythonic
Quick Reference
| Category | Methods | Purpose |
|---|---|---|
| Creation | __new__, __init__ | Object instantiation |
| Representation | __str__, __repr__ | String conversion |
| Comparison | __eq__, __lt__, __le__, etc. | Ordering and equality |
| Arithmetic | __add__, __sub__, __mul__, etc. | Math operations |
| Context | __enter__, __exit__ | Resource management |
| Container | __len__, __getitem__, __setitem__ | Sequence behavior |
| Callable | __call__ | Function-like behavior |
| Attributes | __getattr__, __setattr__ | Attribute access control |
| Hashing | __hash__, __eq__ | Dictionary keys, sets |
What's Next?
You've learned how to customize object behavior with dunder methods! Now let's explore powerful patterns for handling large datasets efficiently.
- Iterators - Create custom iteration logic for your objects
- Generators - Process big data efficiently with lazy evaluation
- Generator Expressions - Write memory-efficient data pipelines