dict.get(k, default) and falsy values
Two related gotchas
Gotcha 1: default is evaluated even when the key exists
def expensive(): print("called"); return []
d = {"a": 1}
d.get("a", expensive()) # prints "called" — default evaluated eagerly
get(k, default) is a regular method call. Python evaluates all arguments before calling. The default object is constructed even if it’s not used.
Gotcha 2: or with falsy values gives the wrong fallback
config = {"timeout": 0, "retries": None}
t = config.get("timeout") or 30
print(t) # 30 ← but the user explicitly set 0!
r = config.get("retries") or 5
print(r) # 5 ← here you wanted the fallback, but pattern conflates "missing" and "None/0/empty"
x or y returns x if x is truthy, else y. Falsy values include 0, 0.0, "", [], None, False. So config.get("timeout") or 30 overrides legitimate 0 values.
How to avoid
For #1 (expensive default), use dict.setdefault only if you actually want to insert, or check first:
val = d[k] if k in d else expensive() # short-circuit
val = d.get(k) or expensive() # only if you accept the falsy collapse
For caches/memoization-style use, prefer defaultdict:
from collections import defaultdict
d = defaultdict(list) # factory only called on missing access
d["a"].append(1)
For #2 (falsy collision), use is None explicitly or test in:
t = config.get("timeout")
if t is None:
t = 30
# or:
t = config["timeout"] if "timeout" in config else 30
# or with type-driven defaults:
t = config.get("timeout", 30)
# but this still doesn't distinguish missing from explicit None
For pydantic / dataclasses, use Optional[int] = None and treat None as “use default” explicitly.
Interview angle
“What’s wrong with port = config.get('port') or 8080?” — the trap when 0 is a valid (or invalid, but explicit) value. Bonus: “What’s the difference between dict.get and dict.setdefault?”