Dunder Methods
Dunder (double-underscore) methods, also called magic methods or special methods, define how objects behave with Python's built-in operations.
Object Lifecycle
class MyClass:
def __new__(cls, *args, **kwargs):
# Called to CREATE the instance (before __init__)
instance = super().__new__(cls)
return instance
def __init__(self, value):
# Called to INITIALIZE the instance
self.value = value
def __del__(self):
# Called when the object is garbage-collected (unreliable — avoid relying on it)
print("Deleted")
String Representation
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
# Unambiguous, developer-facing; used in REPL and repr()
return f"Point({self.x}, {self.y})"
def __str__(self):
# Human-readable; used by print() and str()
return f"({self.x}, {self.y})"
If __str__ is missing, __repr__ is used as a fallback.
Comparison Operators
from functools import total_ordering
@total_ordering # generates the remaining comparisons from __eq__ and __lt__
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
| Method | Operator |
|---|---|
__eq__ |
== |
__ne__ |
!= |
__lt__ |
< |
__le__ |
<= |
__gt__ |
> |
__ge__ |
>= |
Arithmetic Operators
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # self + other
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar): # self * scalar
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # scalar * self (reflected)
return self.__mul__(scalar)
def __iadd__(self, other): # self += other (in-place)
self.x += other.x
self.y += other.y
return self
Container Protocol
class Stack:
def __init__(self):
self._data = []
def __len__(self): # len(stack)
return len(self._data)
def __getitem__(self, index): # stack[i]
return self._data[index]
def __setitem__(self, index, value): # stack[i] = v
self._data[index] = value
def __delitem__(self, index): # del stack[i]
del self._data[index]
def __contains__(self, item): # item in stack
return item in self._data
def __iter__(self): # for x in stack
return iter(self._data)
Context Manager Protocol
class ManagedResource:
def __enter__(self):
print("Acquire")
return self # value bound to `as` variable
def __exit__(self, exc_type, exc_val, exc_tb):
print("Release")
return False # True would suppress exceptions
Callable Objects
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x): # instance can be called like a function
return x * self.factor
double = Multiplier(2)
double(5) # 10
Hashing
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self): # needed to use as dict key or in sets
return hash((self.x, self.y))
If you define
__eq__, Python sets__hash__toNoneunless you also define it.
Key Interview Points
__repr__should return a string that could recreate the object;__str__should be human-readable.- Define
__eq__+__hash__together — objects equal via__eq__must have the same__hash__. @functools.total_orderinglets you define only__eq__and one comparison, and generates the rest.__new__creates;__init__initializes. You rarely need__new__unless subclassing immutables likeintorstr.__slots__replaces__dict__with a fixed set of attributes — saves memory and speeds up attribute access.