Back to blog
FILE 0xA2·THE DICT.GET() DEFAULT THAT WASN'T

The dict.get() default that wasn't

June 17, 2026 · python, debugging, homelab

A message-ingest pipeline on my homelab went quiet for a week before I noticed. Not loudly broken — no alert fired for days, the process was "running," the log file even had recent writes. It just wasn't storing anything. When I finally dug in, the root cause was a single line that looks completely fine until you stare at it:

src = env.get("source") if env.get("source", "").startswith("+") else env.get("sourceNumber")

The intent is obvious: if source is a +-prefixed phone number, use it; otherwise fall back. The env.get("source", "") is supposed to make that null-safe — return an empty string when there's no source, and "".startswith("+") is a harmless False.

Except that's not what dict.get does.

The footgun

dict.get(key, default) returns the default only when the key is absent. If the key is present with a value of None, you get None back — the default is never consulted:

>>> {}.get("source", "")          # key absent
''
>>> {"source": None}.get("source", "")   # key present, value None
>>>                                # -> None, NOT ""

The upstream JSON had "source": null on certain envelope shapes. So env.get("source", "") returned None, and None.startswith("+") threw AttributeError. That exception killed the parser, which broke the pipe feeding it, which made the producer throw a "connection closed, reconnecting" warning and loop. Every cycle: crash, reconnect, crash.

The fix is boring, which is the point:

src = env.get("source") if (env.get("source") or "").startswith("+") else env.get("sourceNumber")

(d.get(k) or "") coerces both "absent" and "present-but-null" to the empty string. Any time a value can be JSON null, that's the pattern you want — not the default argument.

Why it stayed hidden for a week

One bug got it broken; a second one kept it broken quietly. The job is a cron task guarded by flock -n so two copies never run at once. When the crashing process eventually wedged — stuck holding the lock instead of exiting — every subsequent cron invocation hit the non-blocking lock, got rejected, and exited silently with no output.

That had a nasty side effect on monitoring. My healthcheck asserted freshness by watching the log file's mtime. During the crash-loop phase the log kept getting fresh tracebacks appended every minute, so it looked healthy. Only once the process wedged and the log stopped growing did the monitor finally notice — days into an outage that had already lost a week of data the producer had acknowledged upstream and discarded.

What I changed my mind about

Monitor the thing you actually care about, not a proxy for it. The log was never the deliverable — the stored rows were. A healthcheck that asserted "the datastore got a write in the last N minutes" would have paged on day one. A log that's full of exceptions is more active than a healthy-but-idle one, so log-freshness is exactly backwards as a liveness signal for a pipeline that's supposed to be quietly succeeding.

Two cheap lessons, then: