Dunder methods are the hooks Python uses to make your own classes behave like built-in types. The name is short for "double underscore", because each method begins and ends with two underscores (__init__, __len__, __repr__). Writing the right dunder method on a class is what lets len(cart) work on your Cart, print(order) produce a readable string, or product in wishlist answer the right question. This chapter walks through the dunder methods you'll reach for most often.
When you write len(cart), Python doesn't know in advance what "length" means for your Cart class. It can't, because Cart is something you defined. So Python uses a simple rule: take the call len(cart) and rewrite it as cart.__len__(). If that method exists on the object, the call succeeds. If it doesn't, Python raises TypeError.
Every built-in operation in Python works this way. print(order) calls order.__str__(). item in cart calls cart.__contains__(item). wishlist[0] calls wishlist.__getitem__(0). The language doesn't have a privileged list of types that get to use these operations. Any class that defines the right dunder method gets to play.
The call len(cart) triggers Python's lookup of __len__ on the Cart instance. It finds the method, calls it with no arguments (the instance itself is self), and uses whatever the method returns. The result is 3, exactly as if cart were a built-in list.
This dispatch is what the rest of the chapter is about. Pick the operation you want your class to support, find the dunder method that backs it, and define it. The interpreter takes care of the rest.
The diagram shows the two paths Python takes when you call a built-in like len() on a custom object. The cyan node is the user-facing call, the orange nodes show the lookup, and the result is either a successful dispatch (green) or a TypeError (red). Every dunder-backed operation in the chapter follows this shape: a friendly syntax on the outside, a method lookup on the inside.
__str__ vs __repr__: Two Strings, Two AudiencesAlmost every object you print in Python ends up going through one of two dunder methods: __str__ or __repr__. They both turn an object into a string, but they serve different audiences. __str__ is for end users; __repr__ is for developers.
print(obj) calls obj.__str__(). The REPL, when it echoes a value, calls repr(obj) which calls obj.__repr__(). The repr() builtin is also what print uses on objects inside a list or dict, which is why [Order(1)] shows up with the bracket-y form even when you just print the list.
Neither output is useful. Without a __str__ or __repr__ on the class, Python falls back to the default: the class name and the memory address. Define __repr__ and __str__ and the output snaps into shape.
The __str__ form is what you'd put on a receipt or in a log line meant for humans. The __repr__ form looks like the call that would re-create the object, which is exactly what the convention asks for. The official rule of thumb from the Python docs: __repr__ should be unambiguous, and ideally a string that, if you typed it into the REPL, would give you back an equal object.
Here's the trick that saves work most of the time: define `__repr__` first. If a class has a __repr__ but no __str__, Python uses __repr__ for both. So a single well-written __repr__ covers prints, lists, dicts, error messages, and debugger output all at once. Only define __str__ separately when you genuinely need a different presentation for end users.
Cost: repr() is what shows up in tracebacks. A class with no __repr__ makes every error message harder to read because you're looking at <__main__.Order object at 0x10455a700> instead of Order(order_id=101, total=59.99). Adding __repr__ is cheap and pays back every time the code breaks.
| Method | Called by | Audience | Goal |
|---|---|---|---|
__repr__ | repr(obj), REPL echo, print([obj]), tracebacks | Developers | Unambiguous, debug-friendly, ideally re-creatable |
__str__ | print(obj), str(obj), f-strings with {obj} | End users | Readable, presentation-style |
__len__: Making len(obj) Work__len__ is the dunder that powers the len() builtin. Any class that represents a collection of things should define it. A Cart has a number of items, a Wishlist has a number of products, a Catalog has a number of entries: each of those is a natural candidate.
The method must return a non-negative integer. Returning a float, a negative number, or a non-numeric value raises TypeError when len() is called. Python enforces this for a reason: a length is a count, and counts don't go negative or fractional.
__len__ also affects truthiness. If a class defines __len__ but no __bool__, then bool(obj) returns False when the length is zero and True otherwise. That matches the way built-in containers behave (bool([]) is False, bool([1]) is True), and it usually does the right thing for free.
__bool__: What Truthy Looks Like for Your ClassEvery Python object can be tested in an if statement. The rule Python uses is simple but layered:
__bool__, call it and use what it returns.__len__, the object is truthy when the length is nonzero.That third rule is why a fresh class with no special methods is always truthy. An Order object with all-zero fields is still truthy unless you tell Python otherwise.
That's almost certainly the wrong answer for an empty cart. Two ways to fix it. The first is to add __len__, which gives you truthiness as a free side effect:
The second is to define __bool__ explicitly, which is the right call when "truthiness" doesn't line up with "has elements". An Order is truthy when it's been placed, regardless of how many items are in it:
__bool__ must return a bool. Returning 0 or 1 works in practice because of Python's leniency, but the convention is to return True or False directly.
Cost: If both __bool__ and __len__ are defined, __bool__ wins. That means a class that defines __bool__ based on a different field can drift out of sync with len(). Pick one mental model and stick with it.
__contains__: Making in WorkThe in operator is sugar for __contains__. item in cart becomes cart.__contains__(item). If the method isn't defined, Python falls back to iterating the object and comparing each element with ==, which is slow but at least makes in work as long as the object is iterable.
The advantage of defining __contains__ is that you control the comparison. Maybe membership in your Catalog is checked by product ID, not by object equality. Maybe a customer counts as being "in" a region if their address matches a prefix. Defining __contains__ lets you express that directly:
__contains__ should return a bool. Python coerces the return value to a bool anyway, so returning a truthy or falsy value works, but returning True or False is cleaner and easier to read.
Cost: Without __contains__, x in obj falls back to iterating the whole object and comparing every element. For a Cart of three items that's fine. For a Catalog of 100,000 products backed by a dict, iterating means O(n) instead of O(1). Defining __contains__ to delegate to the underlying dict keeps the lookup fast.
__getitem__, __setitem__, __delitem__The square bracket syntax (cart[0], cart[0] = "mouse", del cart[0]) is backed by three dunder methods. Define them and your class behaves like a list or dict from the outside.
| Syntax | Method called |
|---|---|
cart[key] | cart.__getitem__(key) |
cart[key] = value | cart.__setitem__(key, value) |
del cart[key] | cart.__delitem__(key) |
The key can be anything: an integer (list-like), a string (dict-like), a slice, a tuple. Your method decides what to do with it.
A class that defines __getitem__ also gets a free fallback for iteration. If __iter__ isn't defined, Python iterates the object by calling __getitem__(0), __getitem__(1), and so on until it sees an IndexError. This is a legacy mechanism, but it's still in the language, and it's why some old code skips __iter__ entirely on list-like classes.
Slicing also works through __getitem__. When you write cart[1:3], Python passes a slice object as the index, and the method has to handle it. The simplest way is to delegate to the underlying list:
The list's __getitem__ already knows how to handle integers and slices, so delegating to it is the cleanest approach. Hand-rolling slice support is mostly an exercise; for real code, delegate.
__iter__ and __next__ (Brief)The modern way to make a class iterable is to define __iter__. The method must return an iterator, which is any object with a __next__ method. Each call to __next__ returns the next value or raises StopIteration when there are no more.
That's the easy case: delegate to the underlying list's iterator. The harder case is when you want to write the iterator yourself, with __next__ and StopIteration. The headline: __iter__ returns an iterator, __next__ advances it, StopIteration ends it.
__call__: Making Objects CallablePython makes a hard distinction between "function" and "object" in most languages, but not in Python. Any object can be made callable by defining __call__. Once defined, the object can be invoked with parentheses just like a function.
ten_percent and black_friday look like functions when you call them, but they're instances of DiscountCalculator. Each one carries its own state (self.percent_off), and that state is part of every call. This is a clean way to build configurable "function-like" objects: decorators, validators, predicates, anything that needs to remember settings across calls.
Functions in Python are themselves callable because they have __call__. That's not a metaphor; you can literally inspect it:
The callable() builtin returns True for anything with __call__, which is why functions, methods, classes, and __call__-defining objects all pass the same test.
__eq__ and __hash__: The Contract You Must Not BreakThis section matters more than it looks. Getting __eq__ wrong gives you confusing equality. Getting __hash__ wrong, or forgetting it, breaks sets and dicts in subtle ways that don't blow up immediately but corrupt your program's logic.
The default __eq__ compares object identity: two objects are equal only if they are literally the same object in memory. That's almost never what you want for value-like classes.
Two distinct Product instances with identical fields aren't equal because Python's default __eq__ is "are these the same object?". Override __eq__ to compare by value:
Two things in that __eq__:
isinstance check protects against comparing apples to oranges. Returning NotImplemented (not False) tells Python "I don't know how to compare these"; Python then tries the other object's __eq__. Returning False would mask a real mismatch.product_id, not by all the fields. That's a deliberate choice: a product is "the same product" if it has the same ID, even if the name changes.Here's where it gets serious. Python's data model has a rule: if you override `__eq__`, you must also override `__hash__`, or the class becomes unhashable. When you define __eq__ without __hash__, Python automatically sets __hash__ to None, which makes the object unhashable, which means it can't go in a set or be used as a dict key.
That's the failure. Python turned off hashing because you redefined equality and didn't tell it how to hash. The fix is to add __hash__:
The set contains two products, not three, because a and b compare equal and hash to the same value. The set treats them as one entry. That's the whole point of the contract.
The rule, stated formally: if `a == b`, then `hash(a) == hash(b)`. The reverse isn't required (two unequal objects can share a hash, that's just a collision). But equal objects sharing a hash is mandatory, because hash sets and dicts use the hash to find the bucket, then == to confirm a match. If equal objects hashed differently, the dict would miss its own keys.
Two practical rules to remember:
| If you override... | You must also... | Why |
|---|---|---|
__eq__ | Define __hash__ (or accept the class becomes unhashable) | Python sets __hash__ to None automatically when you override __eq__ |
__hash__ | Make sure it agrees with __eq__ | Equal objects must hash equal, or sets and dicts will lose entries |
And one more practical rule: `__hash__` must be based on immutable state. If you hash by an attribute that can change after the object is in a set, the set silently loses the object. The hash put it in one bucket, the mutated value points to a different bucket, and lookups go to the new bucket and find nothing.
The product is in the set, but in reports False. The hash at insert time was computed from "mouse". After mutation, hash(p) is computed from "monitor", which points to a different bucket. The set still has the object physically, but its lookup machinery can't find it. The fix is to hash on something that doesn't change (a stable ID), and to either freeze the class (don't allow mutation) or hash on an immutable subset of fields.
__init__, __new__, __del____init__ is the dunder you've already seen on every class: it runs after the object is created, and its job is to set up the instance's state. It's not technically a constructor; it's an initializer. The actual constructor is __new__, which runs first and is responsible for creating the object.
For 99% of classes, you only ever define __init__. __new__ exists in the background, doing its default job: allocate the instance, then hand it to __init__. The rare cases where you override __new__ are when you need to customize creation itself: enforcing a singleton, returning a cached instance, subclassing an immutable type like tuple or str.
__new__ runs first, creates the instance, and returns it. Python then calls __init__ on that instance to populate it. In day-to-day code, you'll define __init__ and never touch __new__. __new__ is mainly needed for advanced patterns like immutable subclasses and metaclasses.
__del__ is the destructor: Python calls it when the object is about to be garbage-collected. In practice, this is the worst place to put cleanup logic. The exact timing of when an object is collected isn't guaranteed, and __del__ can be skipped entirely if the interpreter exits with the object still alive. The Pythonic way to handle resource cleanup is the context manager protocol (__enter__ / __exit__).
That output looks tidy because CPython uses reference counting, which collects the object the moment its last reference disappears. On other Python implementations (PyPy, Jython), garbage collection runs less predictably, and __del__ might fire much later or not at all. Don't rely on it for anything important.
Here's the consolidated reference for the methods covered in this chapter. The full data-model docs list dozens more; these are the ones you'll reach for first.
| Dunder | Triggered by | Typical use |
|---|---|---|
__init__(self, ...) | Object creation | Initialize instance state |
__new__(cls, ...) | Object creation (before __init__) | Customize object creation (rare) |
__del__(self) | Garbage collection | Cleanup (use context managers instead) |
__repr__(self) | repr(obj), REPL echo, lists/dicts | Unambiguous developer-facing string |
__str__(self) | print(obj), str(obj), f-strings | Readable user-facing string |
__len__(self) | len(obj) | Return count of items |
__bool__(self) | bool(obj), if obj: | Define truthiness |
__contains__(self, item) | item in obj | Membership test |
__getitem__(self, key) | obj[key] | Subscript access (also enables old-style iteration) |
__setitem__(self, key, value) | obj[key] = value | Subscript assignment |
__delitem__(self, key) | del obj[key] | Subscript deletion |
__iter__(self) | iter(obj), for x in obj | Return an iterator |
__next__(self) | next(iterator) | Return next value or raise StopIteration |
__call__(self, ...) | obj(...) | Make instance callable |
__eq__(self, other) | obj == other | Value equality (must pair with __hash__) |
__hash__(self) | hash(obj), sets, dict keys | Return integer hash; must agree with __eq__ |
The dunders for arithmetic (__add__, __sub__, __mul__), ordering (__lt__, __le__, __gt__, __ge__), context management (__enter__, __exit__), and attribute access (__getattr__, __setattr__) are covered separately.
Here's a single Cart class that uses these dunder methods together. It can be printed, measured with len(), tested with in, subscripted, iterated, called, and compared.
Every line of that output is a different dunder method firing. None of the calls are special syntax for Cart; they're the same len, print, in, and indexing you'd use on a list. That's the whole point of the data model: define the right methods and the language treats your class like one of its built-ins.
10 quizzes