Dunder or Magic Methods#
Dunder (double underscore) methods — __init__, __repr__, __eq__, and dozens more — are Python's data model hooks. They let your objects integrate with built-in syntax: print(obj), obj[key], for x in obj, len(obj), obj + other. Mastering the essential subset separates intermediate Python from production-ready code.
This page is the full reference promised in Introdicion to OOPs and Classes and OOP Basics.
How to use this page
Skim the category tables first, then deep-read object representation, equality/hashing, and container protocols. Related: Polymorphism, Custom Containers.
At a glance
| Track | Python Advanced → Object Oriented Programming |
| Sections | 12 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Naming and philosophy · Object creation and destruction · String representation · Comparison and hashing · Arithmetic and reflected operations · Container and sequence protocol · Callable objects · Context managers · … (+4 more)
- Naming and philosophy
- Object creation and destruction
- String representation
- Comparison and hashing
- Arithmetic and reflected operations
- Container and sequence protocol
- Callable objects
- Context managers
- Attribute access hooks
- Boolean and numeric conversion
- Dunder methods for interviews — priority list
__slots__— memory, not really a dunder method
Naming and philosophy#
| Term | Meaning |
|---|---|
| Dunder | Double underscore prefix and suffix: __name__ |
| Magic method | Informal — not actually magic; invoked by Python syntax |
| Special method | Official term in Python data model |
| Rich comparison | __eq__, __lt__, etc. |
You never call obj.__add__(other) directly in application code — use obj + other. Python may fall back to reversed operations (__radd__) when needed.
Object creation and destruction#
class Resource:
def __new__(cls, *args, **kwargs):
"""Allocate object — rarely overridden."""
print("new")
return super().__new__(cls)
def __init__(self, name: str):
"""Initialize instance — common."""
print("init")
self.name = name
def __del__(self):
"""Destructor — unreliable timing; prefer context managers."""
print("del")
| Method | When | Typical override? |
|---|---|---|
__new__(cls, ...) |
Before __init__, creates instance |
Singletons, immutable types, subclasses of str/int |
__init__(self, ...) |
After __new__, configures instance |
Yes — primary constructor |
__del__(self) |
Garbage collection | Rarely — use with / __enter__/__exit__ |
class ImmutablePoint(tuple):
def __new__(cls, x: float, y: float):
return super().__new__(cls, (x, y))
@property
def x(self) -> float:
return self[0]
@property
def y(self) -> float:
return self[1]
String representation#
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def __repr__(self) -> str:
return f"User({self.name!r}, {self.email!r})"
def __str__(self) -> str:
return f"{self.name} <{self.email}>"
| Method | Called by | Audience | Goal |
|---|---|---|---|
__repr__ |
repr(obj), interactive REPL, containers |
Developers | Unambiguous, ideally reconstructable |
__str__ |
str(obj), print(obj) |
End users | Readable |
__format__ |
format(obj, spec) |
Both | Custom format specs |
__bytes__ |
bytes(obj) |
Binary protocols | Byte serialization |
Rule: always define __repr__. If you skip __str__, __str__ falls back to __repr__.
users = [User("Alice", "a@x.com")]
print(users) # [User('Alice', 'a@x.com')] — uses __repr__ in list
__format__ example#
class Percent:
def __init__(self, value: float):
self.value = value
def __format__(self, spec: str) -> str:
if spec == ".1":
return f"{self.value:.1f}%"
return f"{self.value}%"
f"{Percent(0.875):.1}" # "0.9%"
Comparison and hashing#
class Interval:
__slots__ = ("start", "end")
def __init__(self, start: int, end: int):
self.start = start
self.end = end
def __eq__(self, other: object) -> bool:
if not isinstance(other, Interval):
return NotImplemented
return self.start == other.start and self.end == other.end
def __lt__(self, other: "Interval") -> bool:
return self.end <= other.start
def __hash__(self) -> int:
return hash((self.start, self.end))
def __repr__(self) -> str:
return f"Interval({self.start}, {self.end})"
Rich comparison methods#
| Method | Operator |
|---|---|
__eq__ |
== |
__ne__ |
!= (defaults to not __eq__ in 3.x) |
__lt__ |
< |
__le__ |
<= |
__gt__ |
> |
__ge__ |
>= |
Define __eq__ and __lt__; Python can derive the others via functools.total_ordering:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int):
self.major = major
self.minor = minor
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other: "Version") -> bool:
return (self.major, self.minor) < (other.major, other.minor)
__eq__ and __hash__ contract#
| Rule | Detail |
|---|---|
| Equal objects must have equal hashes | If a == b, then hash(a) == hash(b) |
Defining __eq__ disables default hash |
Unless you define __hash__ |
| Mutable objects | Should not be dict keys or set members |
__hash__ = None |
Explicitly unhashable |
class MutableKey:
def __init__(self, x: int):
self.x = x
def __eq__(self, other: object) -> bool:
if not isinstance(other, MutableKey):
return NotImplemented
return self.x == other.x
__hash__ = None # unhashable — safe for mutable objects
Interview trap — dataclass defaults
- Default@dataclass generates __eq__ and __hash__ (if frozen=False, hash is disabled in 3.10+ when eq=True)
- @dataclass(frozen=True) generates both __eq__ and __hash__ — safe for dict keys
Arithmetic and reflected operations#
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other: "Vector") -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __radd__(self, other) -> "Vector":
"""Called when left operand doesn't support __add__."""
if other == 0:
return self
return NotImplemented
def __mul__(self, scalar: float) -> "Vector":
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar: float) -> "Vector":
return self * scalar
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
Vector(1, 2) + Vector(3, 4) # Vector(4, 6)
3 * Vector(1, 2) # Vector(3, 6) — via __rmul__
| Category | Methods |
|---|---|
| Addition | __add__, __radd__, __iadd__ (in-place +=) |
| Subtraction | __sub__, __rsub__, __isub__ |
| Multiplication | __mul__, __rmul__, __imul__ |
| Division | __truediv__, __rtruediv__, __floordiv__, ... |
| Unary | __neg__, __pos__, __abs__ |
Return NotImplemented (not raise TypeError) when the operation doesn't apply — Python tries the other operand's reflected method.
Container and sequence protocol#
class Deck:
def __init__(self, cards: list[str]):
self._cards = list(cards)
def __len__(self) -> int:
return len(self._cards)
def __getitem__(self, index: int | slice):
return self._cards[index]
def __setitem__(self, index: int, value: str) -> None:
self._cards[index] = value
def __delitem__(self, index: int) -> None:
del self._cards[index]
def __contains__(self, item: str) -> bool:
return item in self._cards
def __iter__(self):
return iter(self._cards)
def __reversed__(self):
return reversed(self._cards)
| Method | Syntax | Notes |
|---|---|---|
__len__ |
len(obj) |
Also enables truthiness if no __bool__ |
__getitem__ |
obj[key] |
Slice support requires handling slice type |
__setitem__ |
obj[key] = val |
Mutable mapping/sequence |
__delitem__ |
del obj[key] |
|
__contains__ |
x in obj |
Optional — falls back to iteration |
__iter__ |
for x in obj |
Must return iterator |
__next__ |
next(it) |
On iterator object, not always on container |
Minimal iterable vs full sequence#
# Iterable only — works with for loops
class CountDown:
def __init__(self, start: int):
self.start = start
def __iter__(self):
return CountDownIterator(self.start)
class CountDownIterator:
def __init__(self, n: int):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
Or use a generator:
class CountDown:
def __init__(self, start: int):
self.start = start
def __iter__(self):
for i in range(self.start, 0, -1):
yield i
Callable objects#
class Multiplier:
def __init__(self, factor: int):
self.factor = factor
def __call__(self, x: int) -> int:
return x * self.factor
double = Multiplier(2)
double(5) # 10 — same as double.__call__(5)
Useful for stateful decorators and functor patterns. See Callable Objects.
Context managers#
class Timer:
def __enter__(self):
import time
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self._start
return False # don't suppress exceptions
with Timer() as t:
sum(range(1_000_000))
print(t.elapsed)
Prefer @contextmanager from contextlib for simple cases.
Attribute access hooks#
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name: str):
"""Called when normal lookup fails."""
return getattr(self._target, name)
def __setattr__(self, name: str, value):
if name == "_target":
super().__setattr__(name, value)
else:
setattr(self._target, name, value)
| Method | When invoked |
|---|---|
__getattribute__ |
Every attribute access (dangerous to override carelessly) |
__getattr__ |
Access fails via normal lookup |
__setattr__ |
Any attribute assignment |
__delattr__ |
del obj.attr |
__dir__ |
dir(obj) — customize autocomplete |
Avoid overriding __getattribute__ unless necessary — easy to cause infinite recursion.
Boolean and numeric conversion#
class Stack:
def __init__(self):
self._items: list[int] = []
def push(self, x: int) -> None:
self._items.append(x)
def __len__(self) -> int:
return len(self._items)
def __bool__(self) -> bool:
return len(self._items) > 0
def __int__(self) -> int:
return self._items[-1] if self._items else 0
| Method | Called by |
|---|---|
__bool__ |
bool(obj), if obj: |
__int__ |
int(obj) |
__float__ |
float(obj) |
__index__ |
obj in slice/index contexts needing integer |
If __bool__ is undefined, Python falls back to __len__ (non-zero → True).
Dunder methods for interviews — priority list#
| Priority | Methods | Why |
|---|---|---|
| Must know | __init__, __repr__, __eq__, __hash__ |
Debugging, collections, equality |
| High | __lt__, __len__, __getitem__, __iter__, __contains__ |
Sorting, containers |
| Medium | __call__, __enter__/__exit__, __add__ |
Patterns, resources |
| Situational | __new__, __slots__, __getattr__ |
Advanced design |
__slots__ — memory, not really a dunder method#
| Effect | Detail |
|---|---|
No __dict__ per instance |
Fixed attributes only |
| Faster attribute access | Marginal |
| Lower memory | Meaningful at millions of instances |
| Restrictions | No arbitrary new attrs; weakref needs __weakref__ slot |
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
__eq__ without isinstance check |
TypeError comparing to other types |
Return NotImplemented |
| Mutable object as dict key | Corrupt hash table / subtle bugs | __hash__ = None or frozen dataclass |
a == b True but different hashes |
Violates invariant — set/dict break | Ensure hash matches equality |
Calling __add__ directly |
Bypasses Python coercion rules | Use + operator |
__getattr__ for existing attrs |
Never called for existing names | Use __getattribute__ or normal attrs |
Empty __iter__ without __next__ |
Returns non-iterator | Return iterator object or generator |
Defining only __eq__ expecting sort |
TypeError without __lt__ |
Add ordering or @total_ordering |
Mental model checklist#
- What is the difference between
__repr__and__str__? - Why must equal objects share the same hash value?
- When should
__eq__returnNotImplementedvsFalse? - What is the difference between
__iter__and__next__? - How does Python resolve
3 * Vector(...)whenintdoesn't know aboutVector?
What's next#
| Topic | Page |
|---|---|
| Custom container deep dive | Custom Containers |
| Polymorphism | Polymorphism |
| Callable instances | Callable Objects |
| Basics OOP | Introdicion to OOPs |