skip to content

What does contextvars.ContextVar.get() do if the variable was never set in the current context?

level: juniorimportance: should knowfreq 32%

answer

  1. A miss is not a None
  2. The lookup fails loudly
  3. Two places can supply a fallback
  4. Parent class of KeyError
  5. default= at construction stores nothing

basics

~20 s

ContextVar.get() raises LookupError when nothing has been set and the variable was declared without a default. Pass default= when you create the variable, or hand get() a fallback argument, and the miss returns that value instead of raising.

solid answer

~40 s

A `contextvars.ContextVar` holds no value until something calls `set()` on it in the current context, so a bare `get()` on an untouched variable raises `LookupError` — not `KeyError`, and it never quietly returns `None`. There are two ways to make a miss harmless: give the variable a value at construction with `contextvars.ContextVar("request_id", default="-")`, or pass a per-call fallback as `var.get("-")`. Both only affect *reads*; neither stores anything in the context, so the variable is still "unset" as far as `set()` and `reset()` are concerned. The distinction matters most at read sites that must not raise — a logging filter that stamps records, for instance, runs on every log call including ones outside any request, so it wants a module-level default rather than a try/except.

code

pycon · 13 lines
pycon
>>> import contextvars
>>> plain = contextvars.ContextVar("plain")
>>> plain.get()
Traceback (most recent call last):
  ...
LookupError: <ContextVar name='plain' at 0x104414680>
>>> plain.get("fallback")
'fallback'
>>> withdefault = contextvars.ContextVar("withdefault", default="-")
>>> withdefault.get()
'-'
>>> isinstance(KeyError(), LookupError)
True

go deeper

for a junior

Recall the three outcomes of a read: the stored value, then a fallback argument, then the construction-time default, then LookupError. Be ready to say plainly that it raises rather than returning None.

for a middle

Explain that the ContextVar object is the key and the value lives in the current context, so a default is a read-side convenience that stores nothing. Know that LookupError is the parent of KeyError.

for a senior

Show where the default earns its keep: read sites like a logging filter that run on every record, including outside any request, must not be able to raise. Say when a bare raising get() is the better choice.

for a principal

Own the convention for the codebase: which ambient variables are optional with a documented placeholder and which must fail loudly when unset, and where that decision is declared so no team re-litigates it per call site.

`contextvars.ContextVar` is Python's mechanism for ambient state: a variable whose current value depends on the execution context doing the reading. The object you create at module level is a *key*, not a slot. The values live in a context mapping, and a `ContextVar` simply has, or does not have, an entry in the context that is current when you read it. That framing explains the lookup rule exactly. `var.get()` looks for an entry for this variable in the current context. If there is one, you get it. If there is not, the fallback ladder runs: 1. If the call supplied an argument — `var.get(fallback)` — that argument is returned. 2. Otherwise, if the variable was constructed with `default=...`, the default is returned. 3. Otherwise `LookupError` is raised. So the failure is a raise, not a `None`. This trips people who reach for `ContextVar` after using a dictionary, because `dict.get` returns `None` on a miss while `ContextVar.get` does not. There is no key argument either: the variable *is* the key, so `var.get()` takes at most one positional argument and that argument is the fallback. The exception type is worth memorising. `LookupError` is the base class of `KeyError` and `IndexError`, so `except KeyError:` will **not** catch a missing context value — the relationship runs the other way. Write `except LookupError:` when you genuinely want to branch on "nothing set here". The two ways of supplying a value differ in where the decision lives. A `default=` supplied at construction is a single declaration every reader shares: one place in the module states what "no value here" means, and no call site can forget it. A fallback passed to `get()` is per call, which is right when different readers legitimately want different substitutes, or when one particular read wants to distinguish "absent" without paying for a try/except. Most production code uses the construction-time default and treats a per-call fallback as the exception. A subtlety that is easy to state wrongly in an interview: a default does not put a value into the context. Nothing is stored, no entry exists, and the variable is still unset. You can see that through the token machinery — the first `set()` on a variable that has a default still hands back a token whose `old_value` is the `Token.MISSING` sentinel, because there was no previous entry to record. Resetting with that token removes the entry again, and reads fall back to the default once more. Defaults are a read-side convenience; entries are the actual state. Where does the distinction pay off? At read sites that cannot afford to raise. The classic one is logging. A `logging.Filter` whose job is to copy an ambient identifier onto each record runs for *every* record the handler sees, including records emitted at import time, from library code, or during shutdown — long before or after any code set the variable. If the filter body calls a bare `get()`, one stray log line becomes a `LookupError` inside the logging machinery. A module-level default such as `"-"` or `"none"` makes that read total, and the log line simply shows the placeholder. The same argument applies to metrics tags, trace identifiers, tenant or locale lookups, and anything else read from a hot path where the ambient value is optional by design. Reserve the bare, raising `get()` for places where a missing value is a genuine programming error you want surfaced loudly — an internal helper that must only ever run inside a request, for example, where a `LookupError` is a better outcome than silently attributing work to the wrong owner. One last practical note: `LookupError` from a `ContextVar` carries the variable in its message, so the traceback names which variable was missing. That is usually enough to find the call path that skipped the `set()`, which is why swallowing the exception and substituting `None` at the read site is a bad trade — you lose the only signal that the value never got established. Finally, keep the read shape simple. `get()` takes no key and at most one positional argument, there is no membership test and no dedicated "is it set?" predicate, and the only supported ways to ask "is anything set here?" are to catch `LookupError` or to compare against a sentinel object you supplied as the default. Using a private sentinel — `_UNSET = object()` at module level, then `var.get(_UNSET) is _UNSET` — is the idiomatic way to distinguish "absent" from "set to None" without a try/except on a hot path.

  • Does giving contextvars.ContextVar a default actually store a value in the current context?
    No. The default is consulted only when a read finds no entry; the context stays empty for that variable. You can see it in the token machinery: the first `set()` still reports `Token.MISSING` as the previous state, and resetting with that token removes the entry so reads fall back to the default again.
  • Why does `except KeyError` fail to catch a missing contextvars.ContextVar value?
    Because the variable raises `LookupError`, and `KeyError` is a *subclass* of `LookupError`, not a parent. Catching the subclass never catches the base. Use `except LookupError:` — or, better, avoid the branch entirely by declaring the variable with `default=` so the read cannot fail.
  • When would you prefer a per-call fallback in get() over a default= at construction?
    When different readers legitimately want different substitutes, or when one call site needs a sentinel it can test for while other readers should still fail loudly. Otherwise prefer `default=`: it is one declaration that every reader shares, and no call site can forget it.

saying these in an interview costs you the question

  • Says an unset ContextVar.get() returns None
  • Catches KeyError instead of LookupError
  • Thinks default= writes a value into the context
  • Passes a key string to ContextVar.get() like a dict lookup
  • Claims a missing value comes back as an empty string

context