Which special methods let a custom class support len(obj), obj[k] and obj[k] = v?
answer
- Operators are syntax over dunder methods
- Three bracket operations, three methods
- Length, read, assign — and delete
- __len__ also decides truthiness
- IndexError versus KeyError matters
basics
~10 sPython calls len for len(obj), getitem for reading obj[k], and setitem for assigning obj[k] = v. All three are looked up on the type, not the instance, and len must return a non-negative int.
solid answer
~40 s`len(obj)` dispatches to `__len__`, `obj[k]` to `__getitem__`, `obj[k] = v` to `__setitem__`, and `del obj[k]` to `__delitem__`. Python looks these up on the **type**, so an attribute set on the instance is ignored. `__len__` must return a non-negative `int` — a negative raises `ValueError`, a non-int raises `TypeError` — and it doubles as the truth test: with no `__bool__`, an object whose length is 0 is falsy. `__getitem__` receives whatever sat inside the brackets: an `int`, a `slice` for `obj[1:5]`, a tuple for `obj[1, 'a']`, or a string key. Nothing normalizes it, so `obj[-1]` arrives as `-1` and you decide whether that means the last element. Raise `IndexError` for a bad integer index and `KeyError` for a missing key — the rest of the language keys off exactly those two.
code
python · 18 linesclass Deck:
def __init__(self, cards):
self._cards = list(cards)
def __len__(self):
return len(self._cards)
def __getitem__(self, key):
return self._cards[key]
def __setitem__(self, key, value):
self._cards[key] = value
d = Deck(["A", "B", "C"])
print(len(d), d[0], d[-1], d[0:2])
d[0] = "Z"
print(d[0], bool(Deck([])))go deeper
Recall the mapping cold: len for len(), getitem for reading obj[k], setitem for assigning it. Being able to write a five-line class that wraps a list and supports all three is the whole bar here.
Explain the mechanics: lookup happens on the type, len must return a non-negative int and drives truthiness, and getitem receives the raw key — int, negative int, slice or tuple — with no normalization.
Show that exception choice is part of the contract: IndexError for a bad index, KeyError for a missing key, because built-ins key off those. Be ready to say why len must stay cheap when truth tests call it everywhere.
Own the API decision: whether a type should look like a container at all, and how far to go — the bare four methods, or an abstract base class that derives the extras — given that every operator you expose is a contract callers will lean on forever.
## Subscription is a protocol, not a built-in privilege Nothing about square brackets is reserved for the types that ship with Python. The expression `obj[k]`, the statement `obj[k] = v`, the statement `del obj[k]` and the call `len(obj)` are surface syntax that the interpreter rewrites into lookups of four special ("dunder") methods on the object's type: | syntax | method | |---|---| | `len(obj)` | `__len__` | | `obj[k]` | `__getitem__` | | `obj[k] = v` | `__setitem__` | | `del obj[k]` | `__delitem__` | Implement them and your class behaves like a container everywhere the language expects one. Leave one out and only that operation fails — the protocol is à la carte, not all-or-nothing. A read-only view defines `__len__` and `__getitem__` and stops there; attempting `obj[k] = v` then raises `TypeError: 'X' object does not support item assignment`, which is exactly the right message. ## Lookup happens on the type, never on the instance Implicit special-method lookup skips the instance dictionary and `__getattr__` entirely: `obj[k]` resolves `type(obj).__getitem__`. Assigning `obj.__getitem__ = something` changes only what an *explicit* `obj.__getitem__(k)` call does; the bracket syntax keeps using the class's method. This is why per-instance monkeypatching of operators does not work, and why the dispatch is fast: the interpreter reads a slot on the type object rather than walking an attribute lookup. ## The `__len__` contract `__len__` must return an `int` that is zero or greater. Returning `-1` raises `ValueError: __len__() should return >= 0`; returning `"3"` raises `TypeError`. Beyond `len()`, it participates in truth testing: `bool(obj)` tries `__bool__` first and falls back to `__len__`, so once you define `__len__` the idiom `if my_container:` silently means "is it non-empty". An object with neither method is always truthy. Because truth tests are everywhere, keep `__len__` cheap — an implementation that runs a query or walks a linked structure turns an innocent `if` into real work. ## `__getitem__` gets the raw key The single biggest surprise for people coming from languages with an indexing operator is how little Python does on your behalf. Whatever is between the brackets is passed through untouched: * `obj[3]` → the `int` `3` * `obj[-1]` → the `int` `-1`, **not** normalized to `len(obj) - 1`. Wrapping negative indices is your job; `list` does it because `list` does it, not because the language does. * `obj[1:5]` → a single `slice` object; read `key.start`, `key.stop`, `key.step`, and call `slice.indices()` with your own length to clamp them. It is one call, never a loop of single-index calls. * `obj[1, 'a']` → the tuple `(1, 'a')`; multi-axis containers branch on `isinstance(key, tuple)`. * `obj['name']` → the string; a mapping-flavoured container simply treats the key space as strings. A container that wants to support both integers and slices branches on `isinstance(key, slice)` at the top of `__getitem__`, and by convention returns the *same* container type from a slice so that `obj[1:3]` composes with the rest of your API. ## Which exception to raise The choice is load-bearing, not stylistic. Raise `IndexError` when an integer index is out of range and `KeyError` when a key is absent. The rest of the language reads those two signals: the legacy iteration fallback and `reversed()` stop on `IndexError`, and mapping-shaped helpers key off `KeyError`. Raising a bare `Exception`, returning `None`, or raising `ValueError` for a missing key means built-ins that would otherwise work with your class either loop forever or blow up with a confusing traceback. `__setitem__` returns nothing useful — its return value is discarded, and an assignment expression is a statement, not a value. Augmented subscript assignment (`obj[k] += 1`) is not a fifth method: it is a `__getitem__`, then the in-place add, then a `__setitem__`. ## What you do not get for free Defining these four methods buys you the operators and nothing else. There is no `append`, `index`, `count`, `keys`, `+`, `*`, `==` or `sort`; equality still falls back to identity unless you write `__eq__`. That is a deliberate floor: the protocol is the minimum contract, and richer behaviour is either written by hand or inherited from an abstract base class that derives the extras from your two or three core methods. Start with the floor. Most custom containers in real codebases never need more than `__len__`, `__getitem__` and a well-chosen exception.
- What does `__getitem__` receive when the caller writes `obj[1:5]` or `obj[1, 'a']`?A single `slice(1, 5, None)` object for the first, and the tuple `(1, 'a')` for the second — one call each, never a loop of single-index calls. Nothing unpacks them for you: a sliceable container branches on `isinstance(key, slice)` and reads `key.start`, `key.stop` and `key.step`, or calls the slice's `indices()` method with its own length to clamp them. A multi-axis container branches on `tuple` instead.
- How does defining `__len__` change `bool(obj)`?Truth testing tries `__bool__` first and falls back to `__len__`, so an instance whose length is 0 becomes falsy and any positive length truthy. An object with neither method is always truthy. That means adding `__len__` silently redefines what `if my_container:` asks — usually what you want, but a trap if computing the length is expensive, since truth tests appear in far more places than explicit `len()` calls.
- Why does raising `IndexError` rather than a generic exception matter for a custom container?`IndexError` is the agreed end-of-sequence signal. The legacy `__getitem__` iteration fallback and `reversed()` both stop when they see it, so a container that raises `ValueError` or a bare `Exception` for an out-of-range index makes `list(obj)` and `reversed(obj)` blow up instead of finishing. Missing keys use `KeyError` for the same reason: mapping-shaped helpers catch that specific type.
The bracket syntax is a plug and __getitem__ is the socket: the interpreter does not care what is wired behind the socket, only that your type installed one.
saying these in an interview costs you the question
- Claiming len(obj) reads a .length attribute or field
- Saying __getitem__ receives a normalized, always-positive index
- Assuming obj[1:5] becomes repeated single-index __getitem__ calls
- Thinking __len__ may return None, a float or a negative
- Believing special methods are looked up on the instance
- Raising a bare Exception instead of IndexError or KeyError