Magic Methods (Dunder Methods)
📖 Concept
Magic methods (also called dunder methods for "double underscore") are special methods that Python calls implicitly to perform operations. They let you customize how objects behave with operators, built-in functions, and language constructs.
Categories of magic methods:
| Category | Methods | Triggered By |
|---|---|---|
| Construction | __new__, __init__, __del__ |
Object creation/destruction |
| String | __str__, __repr__, __format__ |
str(), repr(), f"{}" |
| Comparison | __eq__, __lt__, __le__, etc. |
==, <, <= |
| Arithmetic | __add__, __sub__, __mul__, etc. |
+, -, * |
| Container | __len__, __getitem__, __contains__ |
len(), [], in |
| Callable | __call__ |
obj() |
| Context | __enter__, __exit__ |
with statement |
| Attribute | __getattr__, __setattr__ |
. access |
| Hashing | __hash__ |
hash(), dict keys |
Essential rules:
__str__is for end-users (readable),__repr__is for developers (unambiguous). If only one is defined, define__repr__.- If you define
__eq__, you should usually define__hash__too (or set it toNoneto make the object unhashable). - Arithmetic methods have "reflected" versions (
__radd__,__rsub__) for when the left operand doesn't support the operation. - Use
@functools.total_orderingto auto-generate comparison methods from just__eq__and__lt__.
💻 Code Example
1# ============================================================2# String representation: __str__ and __repr__3# ============================================================4class Vector:5 def __init__(self, x, y):6 self.x = x7 self.y = y89 def __repr__(self):10 """Unambiguous representation (for developers)."""11 return f"Vector({self.x}, {self.y})"1213 def __str__(self):14 """Readable representation (for users)."""15 return f"({self.x}, {self.y})"1617 def __format__(self, spec):18 """Custom formatting: f'{v:.2f}'"""19 if spec:20 return f"({self.x:{spec}}, {self.y:{spec}})"21 return str(self)2223v = Vector(3.14159, 2.71828)24print(repr(v)) # Vector(3.14159, 2.71828)25print(str(v)) # (3.14159, 2.71828)26print(f"{v:.2f}") # (3.14, 2.72)2728# ============================================================29# Arithmetic operators30# ============================================================31class Vector:32 def __init__(self, x, y):33 self.x = x34 self.y = y3536 def __add__(self, other):37 if isinstance(other, Vector):38 return Vector(self.x + other.x, self.y + other.y)39 return NotImplemented4041 def __sub__(self, other):42 if isinstance(other, Vector):43 return Vector(self.x - other.x, self.y - other.y)44 return NotImplemented4546 def __mul__(self, scalar):47 if isinstance(scalar, (int, float)):48 return Vector(self.x * scalar, self.y * scalar)49 return NotImplemented5051 def __rmul__(self, scalar):52 """Reflected multiply: allows 3 * vector."""53 return self.__mul__(scalar)5455 def __abs__(self):56 """Magnitude: abs(vector)."""57 return (self.x ** 2 + self.y ** 2) ** 0.55859 def __neg__(self):60 return Vector(-self.x, -self.y)6162 def __repr__(self):63 return f"Vector({self.x}, {self.y})"6465v1 = Vector(1, 2)66v2 = Vector(3, 4)6768print(v1 + v2) # Vector(4, 6)69print(v1 - v2) # Vector(-2, -2)70print(v1 * 3) # Vector(3, 6)71print(3 * v1) # Vector(3, 6) — works because of __rmul__72print(abs(v2)) # 5.073print(-v1) # Vector(-1, -2)7475# ============================================================76# Comparison operators with total_ordering77# ============================================================78from functools import total_ordering7980@total_ordering81class Temperature:82 def __init__(self, celsius):83 self.celsius = celsius8485 def __eq__(self, other):86 if not isinstance(other, Temperature):87 return NotImplemented88 return self.celsius == other.celsius8990 def __lt__(self, other):91 if not isinstance(other, Temperature):92 return NotImplemented93 return self.celsius < other.celsius9495 def __hash__(self):96 return hash(self.celsius)9798 def __repr__(self):99 return f"Temperature({self.celsius}°C)"100101t1 = Temperature(20)102t2 = Temperature(30)103t3 = Temperature(20)104105print(t1 < t2) # True106print(t1 >= t3) # True (auto-generated by total_ordering)107print(t1 == t3) # True108print(sorted([t2, t1, Temperature(25)])) # [20°C, 25°C, 30°C]109110# Can use as dict key because __hash__ is defined111temps = {t1: "comfortable", t2: "hot"}112113# ============================================================114# Container protocol: __len__, __getitem__, __contains__115# ============================================================116class Deck:117 """A deck of playing cards."""118 suits = ["Hearts", "Diamonds", "Clubs", "Spades"]119 ranks = ["2", "3", "4", "5", "6", "7", "8", "9", "10",120 "Jack", "Queen", "King", "Ace"]121122 def __init__(self):123 self.cards = [124 f"{rank} of {suit}"125 for suit in self.suits126 for rank in self.ranks127 ]128129 def __len__(self):130 return len(self.cards)131132 def __getitem__(self, index):133 return self.cards[index]134135 def __contains__(self, card):136 return card in self.cards137138 def __iter__(self):139 return iter(self.cards)140141deck = Deck()142print(len(deck)) # 52143print(deck[0]) # "2 of Hearts"144print(deck[-1]) # "Ace of Spades"145print(deck[10:13]) # Slicing works!146print("Ace of Spades" in deck) # True147148# Iteration works because of __iter__149for card in deck:150 if "Ace" in card:151 print(card)152153# ============================================================154# __call__ — making objects callable155# ============================================================156class Validator:157 def __init__(self, min_val, max_val):158 self.min_val = min_val159 self.max_val = max_val160161 def __call__(self, value):162 """Makes the object callable like a function."""163 if not self.min_val <= value <= self.max_val:164 raise ValueError(165 f"{value} not in range [{self.min_val}, {self.max_val}]"166 )167 return True168169# Use like a function170validate_age = Validator(0, 150)171validate_score = Validator(0, 100)172173print(validate_age(25)) # True174print(validate_score(85)) # True175# validate_age(200) # ValueError176177# Callable check178print(callable(validate_age)) # True
🏋️ Practice Exercise
Exercises:
Create a
Moneyclass with currency and amount. Implement+,-,*, comparison operators, and__format__for currency display. Raise errors for different currencies.Build a
Matrixclass that supports+,*(matrix multiplication),len(),[row][col]indexing, and__repr__.Implement a
Range-like class with__len__,__getitem__,__contains__, and__iter__. Support slicing via__getitem__.Create a
Throttleclass using__call__that limits how many times a function can be called per second.Implement
__enter__and__exit__to create aTimercontext manager:with Timer() as t: ...prints elapsed time.Build a class that supports attribute access via dot notation AND dictionary syntax by implementing
__getattr__and__getitem__.
⚠️ Common Mistakes
Only implementing
__str__without__repr__.__repr__is used in more contexts (debugger, containers, fallback forstr()). Always implement__repr__; optionally add__str__.Returning something other than
NotImplementedwhen an operation isn't supported. ReturnNotImplemented(not raiseNotImplementedError) to let Python try the reflected method on the other operand.Defining
__eq__without__hash__. In Python 3, if you define__eq__,__hash__is set toNone(making instances unhashable). Define__hash__if objects need to be dict keys or set members.Using magic methods directly:
obj.__len__()instead oflen(obj). The built-in functions have additional checks and optimizations. Always use the built-in functions.Implementing too many magic methods. Only implement those that make sense for your class. A
Userobject probably shouldn't support+or*operators.
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Magic Methods (Dunder Methods)