__missing__ — and why .get() doesn’t trigger it
The gotcha
A dict subclass can define __missing__(self, key) to handle missing keys for d[key]. But .get() doesn’t call it — .get() returns its default (None) silently. Mixing the two leads to inconsistent behavior depending on access syntax.
Minimal repro
class SafeDict(dict):
def __missing__(self, key):
return f"<{key}>"
d = SafeDict(a=1)
print(d['a']) # 1 normal hit
print(d['b']) # "<b>" __missing__ fires
print(d.get('b')) # None __missing__ NOT called
print(d.get('b', "x")) # "x"
Why it happens
__missing__ is invoked from __getitem__ only:
d[k] → __getitem__(k)
→ if k in self: return self[k]
→ else: return self.__missing__(k)
d.get(k) is a separate method that doesn’t call __getitem__ at all — it does its own lookup and returns the explicit default on miss. By design, .get() says “I want a default, never raise.” __missing__ is “I want to handle the miss case for [] access.”
in (__contains__), keys(), items(), pop() — none call __missing__.
How defaultdict uses it
collections.defaultdict is the canonical __missing__ user:
from collections import defaultdict
class defaultdict(dict):
def __init__(self, default_factory=None, *args, **kw):
super().__init__(*args, **kw)
self.default_factory = default_factory
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
value = self.default_factory()
self[key] = value # ← critical: stores so future lookups hit
return value
The key bit is self[key] = value — __missing__ not only returns the default, it inserts it. So d['a'].append(1); d['a'].append(2) accumulates correctly:
d = defaultdict(list)
d['x'].append(1) # __missing__ creates [] and stores it; returns []; .append(1) mutates the stored list
d['x'].append(2) # 'x' now exists; __missing__ doesn't fire; .append(2) mutates same list
print(d['x']) # [1, 2]
When you’d write your own __missing__
Self-populating cache:
class HashLookup(dict):
def __init__(self, source):
super().__init__()
self.source = source
def __missing__(self, key):
result = self.source.fetch(key)
self[key] = result
return result
String formatting safe-fallback:
class FormatDict(dict):
def __missing__(self, key):
return '{' + key + '}'
template = "Hello {name}, your balance is {balance}"
template.format_map(FormatDict(name="Alice"))
# 'Hello Alice, your balance is {balance}'
format_map uses [] access on the dict; missing keys preserve the placeholder rather than raising.
Metric collection:
class CounterDict(dict):
def __missing__(self, key):
self[key] = 0
return 0
Better: use collections.Counter — same idea, more features.
Subtle caveat: __missing__ doesn’t cover all access
class MyDict(dict):
def __missing__(self, key):
return "default"
d = MyDict()
print(d['x']) # "default"
print(d.get('x')) # None
'x' in d # False
list(d.keys()) # []
d.pop('x', "fallback") # "fallback" ← __missing__ NOT called
If you want consistent miss behavior across the whole API, __missing__ alone isn’t enough — you’d need to override get, __contains__, etc., too. Easier path: use defaultdict if the goal is “auto-populate on miss,” or wrap your dict access in a helper.
Interview angle
- Q: “What’s
__missing__?” — dict-subclass hook called by__getitem__when the key is absent. - Q: “Why doesn’t
d.get('x')trigger it?” —.get()has its own lookup logic;__missing__is only called from[]access via__getitem__. - Follow-up: “How does
defaultdictuse it?” — calls factory, stores the result, returns it. - Follow-up: “Where is
__missing__useful in practice?” —format_mapwith safe placeholders, lazy caches, custom dict subclasses.
See stdlib/01_collections.md, 16_dict_get_falsy_default.md, 39_setdefault_evaluates_default.md.