W3docs

Python Magic (Dunder) Methods

Learn Python magic methods (__init__, __str__, __repr__, operator overloading, container and context manager protocols) with working examples.

Magic methods — also called dunder methods (short for double-underscore) — are special methods whose names begin and end with two underscores, like __init__ or __len__. They are Python's hook system: by defining them in your own classes you tell Python how an object should behave with built-in operators and functions such as +, len(), print(), in, and with.

You never call dunder methods directly. Instead Python calls them automatically behind the scenes:

Python expressionDunder called
str(obj)obj.__str__()
len(obj)obj.__len__()
a + ba.__add__(b)
a == ba.__eq__(b)
item in objobj.__contains__(item)
with obj as x:obj.__enter__() / obj.__exit__(...)

This chapter covers:

  • String representation — __repr__ and __str__
  • Comparison operators — __eq__, __lt__, and friends
  • Arithmetic operators — __add__, __mul__, __rmul__, and more
  • Container protocol — __len__, __getitem__, __contains__
  • Iterator protocol — __iter__ and __next__
  • Truthiness — __bool__
  • Callable objects — __call__
  • Context manager protocol — __enter__ and __exit__
  • __hash__ — making objects usable as dictionary keys

Before reading, make sure you are comfortable with Python classes and objects and Python inheritance. For computed attributes, see @property.

String Representation: __repr__ and __str__

These two methods control how an object is converted to a string.

MethodCalled byPurpose
__repr__repr(), interactive shellUnambiguous, developer-facing representation
__str__str(), print(), f-stringsHuman-friendly display

If __str__ is not defined, Python falls back to __repr__. It is therefore best practice to always define __repr__ and define __str__ only when you want a different human-readable format.

class Book:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def __repr__(self):
        return f"Book(title={self.title!r}, author={self.author!r}, pages={self.pages})"

    def __str__(self):
        return f'"{self.title}" by {self.author} ({self.pages} pages)'

b = Book("Clean Code", "Robert C. Martin", 431)
print(repr(b))  # Book(title='Clean Code', author='Robert C. Martin', pages=431)
print(str(b))   # "Clean Code" by Robert C. Martin (431 pages)
print(b)        # "Clean Code" by Robert C. Martin (431 pages)

The !r conversion flag inside an f-string calls repr() on that value, which wraps strings in quotes. This makes __repr__ output copy-pasteable Python code.

Tip: a good __repr__ lets you reconstruct the object from its output. Think of it as eval(repr(obj)) == obj as a mental model (even when not literally true).

Comparison Operators

Python comparison operators all map to dunder methods. Define them when you want ==, <, >, <=, or >= to compare your objects meaningfully.

OperatorMethodReflected method
==__eq____eq__
!=__ne____ne__
<__lt____gt__
<=__le____ge__
>__gt____lt__
>=__ge____le__

Reflected means Python tries the right operand's method when the left operand returns NotImplemented. For example, if a < b calls a.__lt__(b) and that returns NotImplemented, Python then tries b.__gt__(a).

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius

    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

    def __le__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius <= other.celsius

    def __repr__(self):
        return f"Temperature({self.celsius}°C)"

t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)

print(t1 == t3)  # True
print(t1 < t2)   # True
print(t2 > t1)   # True  — Python derives __gt__ from __lt__ via reflection
print(t1 <= t3)  # True

Shortcut: if you only want objects to be sortable without caring about the individual six operators, use the @functools.total_ordering decorator. Define __eq__ and one of __lt__, __le__, __gt__, or __ge__, and total_ordering fills in the rest automatically.

from functools import total_ordering

@total_ordering
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius == other.celsius

    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius

Arithmetic Operators

Arithmetic dunders let your objects work with +, -, *, /, //, %, and **.

ExpressionMethodNotes
a + b__add__
a - b__sub__
a * b__mul__
b * a__rmul__right-hand version; called when b.__mul__(a) returns NotImplemented
-a__neg__unary negation
abs(a)__abs__
a += b__iadd__in-place; falls back to __add__ if not defined

A classic use-case is a 2D vector class:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = 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):   # supports: 3 * v
        return self.__mul__(scalar)

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

    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)  # Vector(4, 6)
print(v2 - v1)  # Vector(2, 2)
print(v1 * 3)   # Vector(3, 6)
print(3 * v1)   # Vector(3, 6)  — uses __rmul__
print(-v1)      # Vector(-1, -2)
print(abs(v2))  # 5.0

__rmul__ is what allows 3 * v1 to work. When Python evaluates 3 * v1, it first calls int.__mul__(3, v1). The built-in integer class does not know how to multiply an integer by a Vector, so it returns NotImplemented. Python then tries the reflected method: v1.__rmul__(3), which succeeds.

Container Protocol

Implement these methods to make your class behave like a sequence or collection.

MethodCalled byWhat it enables
__len__len(obj)Length of the container
__getitem__obj[index]Index and slice access
__setitem__obj[index] = valAssignment by index
__delitem__del obj[index]Deletion by index
__contains__item in objMembership test

Defining __len__ together with __getitem__ is enough to make your class automatically iterable — Python's for loop will call __getitem__ with successive indices starting from 0 until it gets an IndexError.

class WordBag:
    def __init__(self, *words):
        self._words = list(words)

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

    def __contains__(self, item):
        return item in self._words

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

    def __repr__(self):
        return f"WordBag({self._words!r})"

bag = WordBag("apple", "banana", "cherry")

print(len(bag))         # 3
print("banana" in bag)  # True
print("grape" in bag)   # False
print(bag[0])           # apple
print(bag[-1])          # cherry

# __len__ + __getitem__ makes the object iterable automatically
for word in bag:
    print(word)
# apple
# banana
# cherry

Iterator Protocol

If you want full iterator behaviour (working with iter() and next() directly, or being usable in places that require an iterator rather than just an iterable), define both __iter__ and __next__:

  • __iter__ — called by iter(obj) and at the start of a for loop; should return the iterator object (usually self).
  • __next__ — called repeatedly to produce the next value; should raise StopIteration when exhausted.
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        self.current = self.start
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

for n in Countdown(3):
    print(n)
# 3
# 2
# 1
# 0

For more powerful iteration patterns — especially lazy sequences that produce values on demand — see Python Generators and Python Iterators.

Truthiness: __bool__

Python calls __bool__ when an object is used in a boolean context (an if statement, while loop, not, and, or). If __bool__ is not defined but __len__ is, Python uses len(obj) != 0 as the truth value. If neither is defined, the object is always truthy.

class Stack:
    def __init__(self):
        self._data = []

    def push(self, item):
        self._data.append(item)

    def pop(self):
        return self._data.pop()

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

    def __bool__(self):
        return len(self._data) > 0

    def __repr__(self):
        return f"Stack({self._data!r})"

s = Stack()
print(bool(s))  # False — empty stack is falsy

s.push(1)
print(bool(s))  # True
print(len(s))   # 1

if s:
    print("stack has items")  # stack has items

This mirrors how built-in collections work: an empty list, dict, or set is falsy; a non-empty one is truthy.

Callable Objects: __call__

Defining __call__ lets you use an instance as if it were a function. This is useful for objects that maintain state between calls — something a plain function cannot do without a closure or a global variable.

class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, value):
        return value * self.factor

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))       # 10
print(triple(5))       # 15
print(callable(double))  # True

double and triple are ordinary objects, but you call them with () just like functions. The built-in callable() function returns True for any object that has __call__.

This pattern is common in machine-learning frameworks (layers, loss functions) and in decorator factories. See Python Decorators for a closely related use-case.

Context Manager Protocol: __enter__ and __exit__

The with statement is Python's way to set up and tear down a resource reliably — even if an exception occurs. Any object that defines __enter__ and __exit__ can be used as a context manager.

  • __enter__(self) — runs when the with block starts; its return value is bound to the as variable.
  • __exit__(self, exc_type, exc_val, exc_tb) — runs when the block exits, whether normally or via an exception. Return True to suppress the exception; return False (or None) to let it propagate.
class ManagedFile:
    def __init__(self, path, mode="r"):
        self.path = path
        self.mode = mode
        self._file = None

    def __enter__(self):
        self._file = open(self.path, self.mode)
        return self._file   # the value bound to the "as" variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._file:
            self._file.close()
        return False  # do not suppress exceptions

with ManagedFile("/etc/hostname") as f:
    content = f.read()

# The file is guaranteed to be closed here, even if an exception occurred inside the block.

The standard library's contextlib.contextmanager decorator lets you write the same logic as a generator function — a lighter alternative for simple cases. See Python with Statement for full coverage.

Hashing: __hash__

Python uses __hash__ to place objects into sets and dictionaries. The default __hash__ is based on the object's memory address (identity). When you override __eq__, Python automatically sets __hash__ to None, making your objects unhashable — you must define __hash__ explicitly if you still want them to work in sets or as dict keys.

The rule: objects that compare equal must have the same hash.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = 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))  # hash of an immutable tuple

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

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)             # True
print(p1 is p2)             # False — different objects in memory
print(hash(p1) == hash(p2)) # True

seen = {p1, p2, p3}
print(len(seen))   # 2 — p1 and p2 are equal, so only one copy kept
print(p1 in seen)  # True

If your class is mutable (its fields can change after creation), do not define __hash__. Mutable objects should not be hashable because changing their fields would change their hash, breaking any set or dict that already contains them.

Summary Table

CategoryMethodTriggered by
Representation__repr__repr(obj), interactive shell
Representation__str__str(obj), print(obj), f-strings
Comparison__eq__, __ne__==, !=
Comparison__lt__, __le__, __gt__, __ge__<, <=, >, >=
Arithmetic__add__, __sub__, __mul__+, -, *
Arithmetic__rmul__, __radd__, …right-hand reflected forms
Arithmetic__neg__, __abs__unary -, abs()
Container__len__len(obj)
Container__getitem__, __setitem__, __delitem__obj[i], obj[i] = v, del obj[i]
Container__contains__item in obj
Iterator__iter__iter(obj), for loop
Iterator__next__next(obj)
Truthiness__bool__bool(obj), if obj:
Callable__call__obj(args)
Context manager__enter__, __exit__with obj as x:
Hashing__hash__hash(obj), dict keys, sets

When to Use Magic Methods

  • Use them when your class represents a value type (a point, a vector, a money amount, a date range) — overloading operators and comparison makes the class feel natural.
  • Use them when your class wraps a resource (a file, a database connection, a network socket) — __enter__/__exit__ ensures the resource is always released.
  • Use them when your class is a custom collection — the container and iterator protocols let it work with for, in, len(), and list comprehensions.
  • Avoid them for ordinary application classes that are not value types or containers. Overloading + on a User class would be confusing.

For more advanced OOP patterns, see Python Abstract Classes, Python Encapsulation, and Python Polymorphism.

Practice

Practice
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Which dunder method does Python call when you use an object in a boolean context such as 'if obj:'?
Was this page helpful?