Python Dunder (Magic) Methods

definitions
python
dunder-methods
oop

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, ideally eval(repr(obj)) reconstructs an equal object.
  • __str__ is for the user: a friendly, readable string, used by print().
  • 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.