Dunder Methods

Deep dive · part of Python OOP Advanced

Double-underscore ("dunder") methods like __len__, __iter__, __getitem__, __contains__, __call__ let your objects integrate with Python's built-in protocols: iteration, subscription, truth-testing, formatting and operator overloading.

Double-underscore methods wire user classes into Python protocols: len, iter, containment, indexing, operators, context managers, and formatting. Implementing the right subset makes objects feel native in for loops, with statements, and f-strings.

You rarely need every dunder—only those matching how objects are used. A Bag needs __len__, __iter__, __contains__, __getitem__; a Money type needs __format__ and rich comparison.

Production code combines this topic with logging, tests, and clear module boundaries so refactors stay safe when requirements grow.

__repr__ should be unambiguous; __str__ is human-friendly for print().

__eq__ without __hash__ makes instances unhashable (dataclass frozen fixes this).

__call__ turns instances into callables (functor pattern).

__enter__/__exit__ implement context managers alongside contextlib.contextmanager.

NotImplemented return from rich comparison delegates to other types.

__getitem__ enables slicing when you accept slice objects explicitly.

Operator overloading should mirror mathematical or domain expectations—surprising __add__ breaks trust. For performance, __slots__ interacts with weakrefs and pickle; test serialization if you add dunders to networked objects.

dataclasses generate many dunders; override selectively rather than reimplementing.

collections.abc protocols let gradual adoption: __iter__ and __len__ may register virtual subclasses without explicit inheritance.

collections.abc lets you implement only the dunders you need and still pass isinstance checks for Sized or Iterable.

Read the parent tutorial on pythondeck.com for runnable snippets, then reproduce them locally in a virtual environment with pinned dependency versions matching your deployment target.

When pairing with teammates, agree on one idiomatic pattern per concern—mixed styles in one repo slow reviews and invite subtle integration bugs during merges.

Implementing __eq__ but not __hash__ then using instances as dict keys.

Returning NotImplemented incorrectly (raising instead) blocking comparisons.

__bool__ always True accidentally making empty containers truthy.

Recursive __repr__ on mutually referencing graphs without reprlib.recursive_repr.

Mirror builtins users expect (list-like -> __iter__ and __len__).

Use @functools.total_ordering when defining __lt__ alone suffices.

Keep __repr__ cheap—no I/O or network.

Add doctest examples showing object in sorted() or with statements.

Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.

The program below demonstrates container protocol. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Example: Container protocol
# Run in the REPL or save as a .py file and execute with python.
class Bag:
    def __init__(self, items):
        self.items = list(items)
    def __len__(self):   return len(self.items)
    def __iter__(self):  return iter(self.items)
    def __contains__(self, x): return x in self.items
    def __getitem__(self, i):  return self.items[i]

b = Bag("abcdef")
print(len(b), list(b), "c" in b, b[1:4])

This sample walks through callable object in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# Example: Callable object
# Run in the REPL or save as a .py file and execute with python.
class Adder:
    def __init__(self, n): self.n = n
    def __call__(self, x): return x + self.n

plus10 = Adder(10)
print(plus10(5), plus10(7))

Here is a hands-on illustration of formatting. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.

# Example: Formatting
# Run in the REPL or save as a .py file and execute with python.
class Money:
    def __init__(self, amount, ccy="USD"):
        self.amount, self.ccy = amount, ccy
    def __format__(self, spec):
        return format(self.amount, spec) + " " + self.ccy

print(f"{Money(1234.5678):,.2f}")

The program below demonstrates container protocol. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# __len__, __iter__, __contains__, __getitem__ make custom containers
class Bag:  # wrapper
    def __init__(self, items): self.items = list(items)  # copy
    def __len__(self): return len(self.items)  # len(b)
    def __iter__(self): return iter(self.items)  # for x in b
    def __contains__(self, x): return x in self.items  # x in b
    def __getitem__(self, i): return self.items[i]  # b[i]

b = Bag("abc")  # construct
print(len(b), list(b), "b" in b, b[1])  # protocols work

This sample walks through callable object in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# __call__ lets instances behave like functions
class Adder:  # callable class
    def __init__(self, n): self.n = n  # addend
    def __call__(self, x): return x + self.n  # invoke instance

plus3 = Adder(3)  # callable object
print(plus3(10), plus3(7))  # 13 10
print(callable(plus3))  # True

« back to Python OOP Advanced All tutorials