Dunder (double-underscore) methods let your objects hook into Python’s built-in operations. They’re what makes len(obj), str(obj), obj[key], and with obj work.

class Demo:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"Demo({self.name})"

    def __repr__(self):
        return f"Demo({self.name!r})"

    def __len__(self):
        return len(self.name)

    def __getitem__(self, index):
        return self.name[index]

Construction & Representation

Method Triggers Purpose
__new__(cls, ...) cls(...) Allocate instance (rarely overridden)
__init__(self, ...) After __new__ Initialize instance state
__del__(self) del obj / GC Cleanup (not a destructor — use context managers)
__repr__(self) repr(obj), REPL Unambiguous developer-readable string
__str__(self) str(obj), print(obj) Readable user-facing string
__bytes__(self) bytes(obj) Byte representation
__format__(self, spec) f"{obj:spec}", format(obj) Custom formatting
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"User({self.name!r}, {self.age!r})"

    def __str__(self):
        return self.name

u = User("Alice", 30)
repr(u)   # User('Alice', 30)
str(u)    # Alice
print(u)  # Alice

Container & Sequence Methods

Make your object behave like a list, dict, or set.

class Deck:
    def __init__(self, cards):
        self._cards = list(cards)

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, pos):
        return self._cards[pos]

    def __setitem__(self, pos, val):
        self._cards[pos] = val

    def __delitem__(self, pos):
        del self._cards[pos]

    def __contains__(self, card):
        return card in self._cards

    def __iter__(self):
        return iter(self._cards)

    def __reversed__(self):
        return reversed(self._cards)

    def __add__(self, other):
        return Deck(self._cards + other._cards)

deck = Deck(["A♠", "K♥", "Q♦"])
len(deck)          # 3
deck[0]            # A♠
"A♠" in deck       # True
for c in deck: ... # iteration
Method Description
__len__ len(obj)
__getitem__ obj[key], slicing, iteration fallback
__setitem__ obj[key] = val
__delitem__ del obj[key]
__contains__ x in obj
__iter__ for x in obj, iter(obj)
__reversed__ reversed(obj)
__missing__ Called by dict-like on missing key (d[key] when key not in dict subclass)
__length_hint__ Hint for list(obj) optimization

Numeric Operators

Override +, -, *, /, ==, etc.

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 __rmul__(self, scalar):
        return self * scalar

    def __neg__(self):
        return Vector(-self.x, -self.y)

    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __bool__(self):
        return self.x != 0 or self.y != 0

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2          # Vector(4, 6)
3 * v1           # Vector(3, 6)   ← __rmul__ handles this
bool(v1)         # True

Forward vs Reflected vs In-place:

Category Examples Called when
Forward __add__, __mul__ a + b (type of a wins)
Reflected __radd__, __rmul__ b + a but a doesn’t implement __add__
In-place __iadd__, __imul__ a += b (falls back to __add__ if absent)

Full list: __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__, __lshift__, __rshift__, __and__, __or__, __xor__, and their __r*__ / __i*__ variants.


Comparison Methods

class Priority:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return self.value != other.value

    def __lt__(self, other):
        return self.value < other.value

    def __le__(self, other):
        return self.value <= other.value

    def __gt__(self, other):
        return self.value > other.value

    def __ge__(self, other):
        return self.value >= other.value

    def __hash__(self):
        return hash(self.value)

Friendly reminder: defining __eq__ without __hash__ makes the class unhashable (can’t be used in sets or as dict keys). Restore hashing by implementing both.


Context Managers — __enter__ / __exit__

Make your object work with with statements.

class File:
    def __init__(self, path):
        self.path = path

    def __enter__(self):
        self.fh = open(self.path)
        return self.fh

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.fh.close()
        return False  # don't suppress exceptions

with File("/tmp/data.txt") as f:
    print(f.read())
  • __enter__ runs on entry, returns the bound variable (as x).
  • __exit__ runs on exit (even if an exception occurred). Return True to suppress the exception.

Callable Objects — __call__

Make an instance callable like a function. Useful for callable classes (e.g. decorators, factories).

class Adder:
    def __init__(self, n):
        self.n = n

    def __call__(self, x):
        return x + self.n

add5 = Adder(5)
add5(10)  # 15
callable(add5)  # True

Attribute Access

Method Triggers Use
__getattr__ obj.x only when normal lookup fails Fallback / computed attributes
__getattribute__ Every obj.x access Intercept all access (careful — infinite recursion risk)
__setattr__ obj.x = val Validation, logging
__delattr__ del obj.x Cleanup on delete
__dir__ dir(obj) Custom attribute listing
class Validated:
    def __setattr__(self, name, val):
        if name == "age" and val < 0:
            raise ValueError("age must be >= 0")
        super().__setattr__(name, val)

__slots__ — Memory Optimization

Tell Python not to use a per-instance __dict__, saving memory.

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(1, 2)
p.z = 3  # AttributeError: 'Point' has no attribute 'z'

__slots__ disables __dict__ and __weakref__ by default. Add "__dict__" to __slots__ if you still need dynamic attributes.


Pickling — __getstate__ / __setstate__

Control how your object is serialized by pickle.

class Connection:
    def __init__(self, url):
        self.url = url
        self.socket = open_connection(url)

    def __getstate__(self):
        state = self.__dict__.copy()
        del state["socket"]  # can't pickle sockets
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)
        self.socket = open_connection(self.url)

Cheat Sheet: Quick Reference

Category Methods
Construction __new__, __init__, __del__
Representation __repr__, __str__, __bytes__, __format__
Container __len__, __getitem__, __setitem__, __delitem__, __contains__, __iter__, __reversed__, __missing__
Numeric __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__, __lshift__, __rshift__, __and__, __or__, __xor__, plus __r*__ and __i*__ variants
Unary __neg__, __pos__, __abs__, __invert__, __bool__, __int__, __float__, __complex__, __index__
Comparison __eq__, __ne__, __lt__, __le__, __gt__, __ge__, __hash__
Context __enter__, __exit__
Callable __call__
Attribute __getattr__, __getattribute__, __setattr__, __delattr__, __dir__
Serialization __getstate__, __setstate__, __reduce__
Slots __slots__

Golden rule: Implement dunders only when your type genuinely is that kind of thing. Don’t add __len__ just because you can — add it when len(obj) is a natural operation for your object.