Magic (Dunder) Methods

Dunder methods (double underscore, e.g. __init__) let custom classes hook into Python’s built-in syntax and functions: printing, comparisons, arithmetic, iteration, and more.

Object Representation

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
 
    def __repr__(self):
        # unambiguous, developer-facing, ideally something that could recreate the object
        return f"Point({self.x!r}, {self.y!r})"
 
    def __str__(self):
        # readable, user-facing, used by print() and str()
        return f"({self.x}, {self.y})"
 
p = Point(1, 2)
print(p)          # uses __str__ -> (1, 2)
repr(p)             # uses __repr__ -> 'Point(1, 2)'
[p]                   # in a container/repl, falls back to __repr__ -> [Point(1, 2)]

If you only define one, define __repr__

__str__ falls back to __repr__ automatically if not defined. The reverse is not true. __repr__ should be unambiguous, ideally valid Python that could recreate the object.

Equality and Hashing

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
 
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
 
    def __hash__(self):
        return hash((self.x, self.y))
 
Point(1, 2) == Point(1, 2)     # True, without __eq__ this would be False (identity check)

Defining __eq__ disables the default __hash__

If you override __eq__, Python sets __hash__ to None automatically, making instances unhashable (cannot be dict keys or set members) unless you also explicitly define __hash__.

Comparison Operators

class Money:
    def __init__(self, amount):
        self.amount = amount
 
    def __lt__(self, other): return self.amount < other.amount
    def __le__(self, other): return self.amount <= other.amount
    def __gt__(self, other): return self.amount > other.amount
    def __ge__(self, other): return self.amount >= other.amount

functools.total_ordering

Define just __eq__ and one of __lt__/__le__/__gt__/__ge__, then decorate the class with @functools.total_ordering to auto-generate the rest.

Arithmetic Operator Overloading

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
 
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
 
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
 
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
 
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
 
Vector(1, 2) + Vector(3, 4)     # Vector(4, 6)
Vector(1, 2) * 3                  # Vector(3, 6)

Container/Sequence Protocol

Implementing these lets your objects behave like built-in lists/dicts.

class Playlist:
    def __init__(self, songs):
        self.songs = songs
 
    def __len__(self):
        return len(self.songs)
 
    def __getitem__(self, index):
        return self.songs[index]
 
    def __setitem__(self, index, value):
        self.songs[index] = value
 
    def __contains__(self, item):
        return item in self.songs
 
    def __iter__(self):
        return iter(self.songs)
 
p = Playlist(["A", "B", "C"])
len(p)             # 3
p[0]                 # 'A'
"B" in p               # True
for song in p: ...       # iteration works automatically

Callable Objects

class Multiplier:
    def __init__(self, factor):
        self.factor = factor
 
    def __call__(self, x):
        return x * self.factor
 
double = Multiplier(2)
double(5)     # 10, the instance itself can be "called" like a function

Context Manager Protocol

class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self
 
    def __exit__(self, exc_type, exc_value, traceback):
        import time
        print(f"Elapsed: {time.perf_counter() - self.start:.4f}s")
 
with Timer():
    sum(range(1_000_000))

See Context-Managers for the full protocol.

Quick Reference Table

DunderTriggered byPurpose
__init__ClassName(...)Initialize a new instance
__new__ClassName(...) (before __init__)Actually create the instance
__repr__repr(obj), console displayUnambiguous developer representation
__str__str(obj), print(obj)Readable user-facing representation
__eq__==Value equality
__lt__/__le__/__gt__/__ge__<, <=, >, >=Ordering
__hash__hash(obj), dict/set keysHashability
__len__len(obj)Length
__getitem__obj[key]Index/key access
__setitem__obj[key] = valueIndex/key assignment
__contains__x in objMembership test
__iter__for x in objIteration
__call__obj(...)Making an instance callable
__enter__/__exit__with obj:Context manager protocol
__add__, __sub__, __mul__, etc+, -, *Arithmetic overloading
__bool__bool(obj), if obj:Truthiness
__del__garbage collectionCleanup before destruction (rarely needed)