Python Dunder (Magic) Methods
Dunder (“double underscore”) or magic methods are special methods, named __like_this__, that Python calls automatically to implement built-in behaviour for a type — instantiation, printing, equality, arithmetic operators, and more. You don’t call them directly; Python invokes them on your behalf when the corresponding syntax/built-in is used.
Common dunder methods
| Method | Called by | Purpose |
|---|---|---|
__init__(self, ...) |
ClassName(...) |
Initializes a new instance. |
__str__(self) |
print(obj), str(obj) |
User-friendly display string. |
__repr__(self) |
the REPL, repr(obj) |
Unambiguous, developer-facing string — ideally a valid Python expression that recreates the object via eval(). |
__eq__(self, other) |
obj == other |
Custom equality (instead of identity). |
__add__(self, other) |
obj + other |
Addition. |
__sub__(self, other) |
obj - other |
Subtraction. |
__neg__(self) |
-obj |
Unary negation. |
__len__(self) |
len(obj) |
Length. |
__repr__ vs. __str__
__repr__is for the programmer: unambiguous, meant for debugging/logging, ideallyeval(repr(obj))reconstructs an equal object.__str__is for the user: a friendly, readable string, used byprint().- If only
__repr__is defined,print()falls back to it. If neither is defined, printing an object shows its default<... object at 0x...>representation.
Overloadable operators
| Binary | Method | Unary | Method | Comparison | Method |
|---|---|---|---|---|---|
+ |
__add__ |
- |
__neg__ |
< |
__lt__ |
- |
__sub__ |
abs |
__abs__ |
<= |
__le__ |
* |
__mul__ |
~ |
__invert__ |
== |
__eq__ |
** |
__pow__ |
!= |
__ne__ |
||
// |
__floordiv__ |
> |
__gt__ |
||
/ |
__truediv__ |
>= |
__ge__ |
Instance variables vs. class variables
An instance variable (self.x = ...) belongs to one specific object — each instance has its own copy. A class variable (declared directly in the class body) is shared by all instances of the class, and can be accessed either via an instance (obj.class_var) or via the class itself (ClassName.class_var), without needing any instance at all.
class Clicker():
_all_clicks = 0 # class variable -- shared by every instance
def __init__(self) -> None:
self._clicks = 0 # instance variable -- unique per object
def click(self) -> None:
self._clicks += 1
Clicker._all_clicks += 1
Worked example: Vector2D
A fuller worked example combining several dunder methods together:
import math
class Vector2D():
def __init__(self, x, y):
self.x = x
self.y = y
def length(self):
return math.sqrt(self.x**2 + self.y**2)
def __repr__(self):
return f"Vector2D(x={self.x}, y={self.y})"
def __str__(self):
return f"2D Vector: ({self.x}, {self.y}) --- length: {self.length()}"
def __eq__(self, other):
return isinstance(other, Vector2D) and self.x == other.x and self.y == other.y
def __add__(self, other):
if not isinstance(other, Vector2D):
return NotImplemented
return Vector2D(self.x + other.x, self.y + other.y)
def __len__(self):
return 2
>>> u = Vector2D(2, 3)
>>> v = Vector2D(4, 5)
>>> print(u + v)
2D Vector: (6, 8) --- length: 10.0
>>> print(u) # calls __str__
2D Vector: (2, 3) --- length: 3.605551275463989
>>> repr(u) # calls __repr__
'Vector2D(x=2, y=3)'
>>> u == v # calls __eq__
False
>>> len(u) # calls __len__
2
Returning NotImplemented (rather than raising or returning False) from a method like __add__ is the standard way to signal “I don’t know how to combine these two types” — it lets Python fall back to the other object’s reflected method, or raise a clean TypeError if nothing handles it.