d[k] vs d.get(k) vs d.setdefault(k, v) vs defaultdict
The four lookup mechanics
| Method | Missing key | Mutates dict | Default semantics |
|---|---|---|---|
d[k] |
raises KeyError |
no | calls __missing__ if defined |
d.get(k) |
returns None |
no | second arg is the default; eagerly evaluated |
d.get(k, default) |
returns default |
no | same as above |
d.setdefault(k, v) |
inserts {k: v}, returns v |
yes (on miss) | v is eagerly evaluated even on hit |
defaultdict(factory)[k] |
calls factory(), inserts, returns |
yes (on miss) | factory called lazily only on miss |
When each makes sense
d[k] — when missing IS an error
def get_config(d, key):
return d[key] # KeyError if key not there — caller's fault
Use when you want loud failure on missing keys. Idiomatic for “I expect this to exist; tell me if it doesn’t.”
d.get(k, default) — when missing is normal
timeout = config.get("timeout", 30)
Use for optional values with a fallback. get(k) (no default) returns None — fine for “missing is the same as None” cases.
The trap: get(k, expensive_call()) always evaluates expensive_call, even on hit. Use the if-pattern when default is expensive:
val = d[k] if k in d else expensive()
See 16_dict_get_falsy_default.md for the falsy-collision trap (get() or default overrides legitimate 0/“”/[]).
d.setdefault(k, v) — insert-if-missing, return current
groups = {}
for item in items:
groups.setdefault(item.kind, []).append(item)
Use for “either initialize this slot or use the existing one, then mutate.” Common for grouping.
The setdefault gotcha: v is evaluated every call, even on hit. Cheap defaults ([], 0, "") are fine; expensive ones aren’t. See 39_setdefault_evaluates_default.md.
defaultdict(factory) — lazy factory, no eager evaluation
from collections import defaultdict
counts = defaultdict(int)
for word in words:
counts[word] += 1
int() is called only when a key is accessed for the first time. No eager-evaluation tax.
The classic counter pattern
Three ways to count occurrences:
# 1. plain dict + setdefault — works, slight overhead from re-evaluating int(0)
counts = {}
for w in words:
counts.setdefault(w, 0)
counts[w] += 1
# 2. defaultdict — cleaner, no eager-eval
from collections import defaultdict
counts = defaultdict(int)
for w in words:
counts[w] += 1
# 3. Counter — the canonical answer
from collections import Counter
counts = Counter(words)
Counter is the canonical idiom — built on defaultdict(int) plus extras (most_common, arithmetic, multiset operations).
The grouping pattern
# 1. setdefault — works
groups = {}
for u in users:
groups.setdefault(u.role, []).append(u)
# 2. defaultdict — cleaner
from collections import defaultdict
groups = defaultdict(list)
for u in users:
groups[u.role].append(u)
# 3. itertools.groupby — different semantics (input must be sorted)
from itertools import groupby
users.sort(key=lambda u: u.role)
groups = {role: list(items) for role, items in groupby(users, key=lambda u: u.role)}
groupby is a different tool — it forms runs of consecutive equal keys, not full groups. Sort first, or use defaultdict.
Sentinel for “missing vs explicit None”
d.get(k) can’t distinguish “key not present” from “key present with value None”:
d = {"a": None}
d.get("a") # None
d.get("b") # None
If the distinction matters:
MISSING = object()
val = d.get(k, MISSING)
if val is MISSING:
... # truly absent
elif val is None:
... # present, set to None
Or use in:
if k in d:
val = d[k] # might be None
else:
val = ... # truly absent
When to use __missing__ instead
If you want d[k] (not .get) to have a custom missing-key behavior, subclass dict and define __missing__:
class SafeDict(dict):
def __missing__(self, key):
return f"<{key}>"
__missing__ only fires for [] access — not .get(). See 40_dict_missing_method.md.
Cheat sheet
| Use case | Best tool |
|---|---|
| “Must exist; raise if not” | d[k] |
| “Optional with cheap default” | d.get(k, default) |
| “Optional with expensive default” | if k in d: d[k] else compute() |
| “Insert empty container if missing, then mutate” | defaultdict(factory) |
| “Counter” | collections.Counter |
| “Group items by a key” | defaultdict(list) |
| “Lazy fetch + cache, custom miss handler” | subclass dict + __missing__ |
| “Distinguish missing from None” | MISSING sentinel + get(k, MISSING) |
Interview angle
- Q: “What’s the difference between
d[k]andd.get(k)?” —[]raisesKeyError;.getreturnsNone(or your default) silently. - Q: “What’s wrong with
d.setdefault(k, expensive())?” —expensive()always evaluates, even on hit. Usedefaultdictinstead. - Follow-up: “When would you reach for
defaultdictvsdict.setdefault?” — defaultdict for repeated grouping/counting; setdefault for one-off “insert if missing” with a cheap default. - Follow-up: “How do you distinguish ‘key absent’ from ‘key set to None’ with
.get?” — sentinel object +ischeck.
See 16_dict_get_falsy_default.md, 39_setdefault_evaluates_default.md, 40_dict_missing_method.md, stdlib/01_collections.md.