backend / python core / tricky questions / 16_dict_get_falsy_default.md

dict.get(k, default) and falsy values

1 min read source

dict.get(k, default) and falsy values

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?”