skip to content

What does defining __missing__ on a dict subclass change about d[key] lookups?

level: juniorimportance: should knowfreq 35%

answer

  1. A last chance before the lookup error
  2. The class defines it, not the instance
  3. Subscription only, no other accessor
  4. It may return a value or store one
  5. Return value becomes the lookup result

basics

~20 s

When a key is absent, dict.getitem calls the subclass's missing with that key and returns whatever it returns, instead of raising KeyError. Only subscription d[key] triggers it: .get(), the in operator and .pop() never do.

solid answer

~40 s

`__missing__` is a hook that `dict.__getitem__` consults on a miss. If the key is not present and the object's class defines `__missing__`, `d[key]` calls `self.__missing__(key)` and returns its result; only if no such method exists does it raise `KeyError`. It is defined on a `dict` *subclass* — plain `dict` has no `__missing__`, so you cannot attach the behaviour to an existing dictionary instance. Crucially, nothing else in the mapping surface calls it: `d.get(key)` still returns `None`, `key in d` is still `False`, and `d.pop(key)` still raises. The method may simply compute a default and return it, or it may store the value first, which is what makes lookups mutate the mapping. `collections.defaultdict` is the standard-library mapping built on exactly this hook.

code

python · 9 lines
python
class Zeros(dict):
    def __missing__(self, key):
        return 0

counts = Zeros(a=1)
print(counts["b"])       # 0 - dict.__getitem__ delegated the miss
print(counts.get("b"))   # None - .get() never consults __missing__
print("b" in counts)     # False - nothing was stored
print(dict(counts))      # {'a': 1}

go deeper

for a junior

Recall that a miss on subscription calls the hook instead of raising, that it must be defined on a subclass, and that the value it returns is what the lookup evaluates to. Be able to write the five-line example.

for a middle

Explain that the check lives inside the subscription path only, list the accessors that skip it, and distinguish the pure variant from the storing variant that makes the append-to-a-list idiom work.

for a senior

Judge when a miss hook belongs in production code: unbounded key variety plus the storing variant is a slow leak, and code doing anything expensive on a path readers assume is a plain lookup is a maintenance hazard.

for a principal

Own the API question: a mapping that never raises on an unknown key hides typos and bad input from every caller. Decide where in a system silence about missing data is acceptable and where it must be loud.

## The hook `dict.__getitem__` — the code behind the `d[key]` syntax — does two things on a miss. First it checks whether the object's *type* defines a `__missing__` method. If it does, it calls `type(d).__missing__(d, key)` and returns the result. If it does not, it raises `KeyError(key)`. That is the whole protocol. ```python class Zeros(dict): def __missing__(self, key): return 0 counts = Zeros(a=1) counts["b"] # 0, no exception ``` Two constraints follow from where the check lives. **It is a class-level hook, on a subclass.** The built-in `dict` type does not define `__missing__`, and because implicit dunder lookups go through the type rather than the instance, assigning `d.__missing__ = f` on an existing dictionary does nothing. You must define it on a class that derives from `dict` (or on a `collections.UserDict` subclass — `UserDict.__getitem__` performs the same check). **Only subscription consults it.** `dict.get()` is a separate C function with its own miss path that returns the supplied default, or `None`. `in` runs the containment check against the hash table. `dict.pop()` raises or returns its default. `dict.setdefault()` inserts its own default. None of them touch `__missing__`. So: ```python counts.get("b") # None, not 0 "b" in counts # False counts.pop("b") # KeyError ``` That split trips people constantly, because `d[key]` and `d.get(key)` are usually interchangeable in the reader's head, and here they are not. ## Two flavours of __missing__ The hook can be **pure** — compute and return a value, store nothing: ```python class Zeros(dict): def __missing__(self, key): return 0 ``` The mapping never grows. Reading a million absent keys leaves it empty. This is what `collections.Counter` does for its miss behaviour, and it is the safe default. Or it can be **inserting** — store a value under the key and return it: ```python class Buckets(dict): def __missing__(self, key): self[key] = [] return self[key] b = Buckets() b["x"].append(1) # works: the list is stored, so the append survives ``` Insertion is what makes the `d[k].append(...)` idiom work — without storing, you would mutate a temporary list and lose it. But it also means *reading* mutates: every lookup of an unknown key permanently grows the mapping. Over a long-lived process with unbounded key variety, that is a slow leak, and the mapping's own `len()` and iteration silently reflect keys nobody ever wrote. `collections.defaultdict` is the stdlib packaging of the inserting flavour: it stores a factory and its `__missing__` calls that factory, inserts, and returns. Writing your own `__missing__` is worth it when the default *depends on the key* — a factory taking no arguments cannot see which key it is being asked for — or when you want the pure, non-inserting behaviour. ## The class-level return contract Whatever `__missing__` returns becomes the value of the expression `d[key]`. It can raise instead — raising `KeyError(key)` yourself is a perfectly good way to say "only some misses are defaultable" — and it can log, validate, or normalize. Keep it cheap and total: it runs on a code path readers believe is a plain lookup, so anything surprising in it will surprise them at the worst moment. ## What an interviewer is checking At junior level, three things: that you know `d[key]` has a documented escape hatch before `KeyError`; that you can define it on a subclass and demonstrate it; and that you notice `.get()` and `in` do not participate. If you can add why `d[k].append(v)` needs the *inserting* variant to work, you have covered the part that actually shows up in real code. ## Where the standard library uses it Two stdlib mappings are built on this hook, and the contrast between them is the whole design space in miniature. `collections.Counter` answers a miss with zero and stores nothing, so counting keys you never incremented leaves the counter untouched. `collections.defaultdict` calls a factory, inserts the result and returns it, so a miss grows the mapping. Both are one `__missing__` method. The hook receives the key, which is the reason to write your own rather than reach for the factory-based mapping: a zero-argument factory cannot see which key it is producing a value for, so a default that depends on the key - a per-tenant bucket, a value derived from a key prefix - has to be computed inside `__missing__`. The hook may also raise: raising `KeyError(key)` for keys that should not be defaulted lets you make only part of the key space forgiving, which is often what you actually want. Keep the body cheap and predictable. It runs on a line every reader parses as a plain lookup, so an expensive computation, a network call or a log write hidden in there will surprise whoever is reading the call site - and it will not run at all if that reader later switches the call to `.get()`.

  • Does d.get(key) call __missing__?
    No. `dict.get()` has its own miss path and returns the default you passed, or `None`. The same is true of the `in` operator, `dict.pop()` and `dict.setdefault()` — only the `d[key]` syntax routes through `dict.__getitem__`, which is where the `__missing__` check lives. A mapping with a miss hook therefore answers `d[key]` and `d.get(key)` differently for the same absent key.
  • Can you enable __missing__ on an existing plain dict by assigning the attribute to it?
    No. Implicit dunder lookups are performed on the type, not the instance, and the built-in `dict` type defines no `__missing__`. Setting `d.__missing__ = f` on an instance is ignored by `d[key]`, and on a plain `dict` you cannot set arbitrary attributes anyway. You need a subclass of `dict` — or of `collections.UserDict`, whose `__getitem__` performs the same check.
  • When is writing your own __missing__ better than reaching for defaultdict?
    When the default depends on the key — a factory taking no arguments cannot see which key it is producing a value for — or when you want the *non-inserting* flavour, returning a value without growing the mapping. `collections.defaultdict` always inserts, which is exactly what you do not want for a long-lived lookup table keyed by unbounded input.

missing is a receptionist who answers only when someone rings the front-desk bell. Ring the bell and you get an answer for a name that is not in the register; walk past and read the register yourself and the name simply is not there.

saying these in an interview costs you the question

  • Thinks .get() also routes through __missing__
  • Believes plain dict supports the hook without subclassing
  • Assumes a lookup miss always stores something
  • Confuses the hook with a default argument to .get()
  • Says the hook fires for the in operator
  • Cannot say what the return value of the hook becomes

context