skip to content

Why does contextvars.ContextVar.set() return a Token, and what do you do with it?

level: middleimportance: must knowfreq 55%

answer

  1. set gives you back a ticket
  2. It records what was displaced
  3. Unset is a state worth restoring
  4. One ticket, one use, LIFO
  5. 3.14 lets the with statement hold it

basics

~20 s

set() returns a Token recording what the variable held before that write, including the fact that it held nothing. Passing the token to var.reset(token) restores exactly that previous state. Each token may be reset once, so nested writes unwind last-in-first-out.

solid answer

~40 s

`ContextVar.set(value)` returns a `contextvars.Token` describing the state the write replaced: `token.old_value` is the previous value, or the `Token.MISSING` sentinel if the variable had no entry at all, and `token.var` points back at the variable. `var.reset(token)` puts that state back — restoring the old value, or removing the entry entirely so reads fall through to the default again. That is why you cannot fake it by saving the old value yourself: a hand-rolled save cannot express "there was nothing here", so it leaves a stale entry behind. Tokens are single use; resetting twice raises `RuntimeError`. Pair every `set()` with a `reset()` in a `finally` so an exception cannot leave the value in place. In Python 3.14 the token is also a context manager, so `with var.set(value):` does the pairing for you.

code

pycon · 16 lines
pycon
>>> import contextvars
>>> stage = contextvars.ContextVar("stage", default="idle")
>>> outer = stage.set("fetch")
>>> outer.old_value is contextvars.Token.MISSING
True
>>> inner = stage.set("write")
>>> inner.old_value
'fetch'
>>> stage.reset(inner); stage.get()
'fetch'
>>> stage.reset(outer); stage.get()
'idle'
>>> stage.reset(outer)
Traceback (most recent call last):
  ...
RuntimeError: <Token used var=<ContextVar name='stage' default='idle' at 0x100210630> at 0x1057fafc0> has already been used once

go deeper

for a junior

Remember the pairing: capture what set() gives you and pass it to reset() when the scope ends. Do not try to restore the old value by hand, and do not expect the value to disappear when the function returns.

for a middle

Explain the token's contents — the previous value or the MISSING sentinel — and why reset() can therefore restore 'no entry at all'. Show the try/finally pairing and know that a token is single use.

for a senior

Demonstrate the operational consequence: a missed reset on a long-lived worker leaks an identifier into the next unit of work, so the records look like one owner was processed twice. Enforce the pairing with a context manager rather than trusting call sites.

for a principal

Decide how scoped ambient writes are expressed across the codebase — a single blessed helper that owns set/reset, versus ad-hoc tokens in every module — and how that survives version differences now that 3.14 offers the with-form.

A `contextvars.ContextVar` write is a *scoped* write, and the token is what makes the scope closable. **What the token carries.** `token = var.set(value)` returns a `contextvars.Token` with two attributes. `token.var` is the variable it came from. `token.old_value` is the value the variable had in this context immediately before the write — or the class-level sentinel `Token.MISSING` if the variable had no entry at all. The sentinel is the whole reason the token exists rather than a plain "old value" return. "No entry" and "an entry holding None" are different states: the first falls back to the variable's default on a read, the second returns `None`. Only a token can express the difference, so only `reset()` can restore it faithfully. **What reset does.** `var.reset(token)` restores the recorded state: it writes `old_value` back, or, when the recorded state was `MISSING`, deletes the entry so subsequent reads fall through to the variable's default (or raise `LookupError` if it has none). It does *not* clear the variable to `None`, and it does not pop "one level" off some stack — it re-establishes precisely what that one `set()` displaced. **Single use and ordering.** A token may be reset once. Calling `reset` with a token that has already been used raises `RuntimeError` with a message saying the token has already been used. A token must also be reset in the same context that produced it. And because each token restores what its own `set()` displaced, nested writes must unwind last-in-first-out: set A, set B, reset B, reset A. Resetting out of order does not error — it simply reinstalls a stale value, which is far worse than an exception because the wrong value then propagates silently. **Why the finally matters.** The pattern is: ```python token = current_job.set(job_id) try: run(job) finally: current_job.reset(token) ``` Without the `finally`, an exception between `set` and `reset` leaves the value installed. On a short-lived process that is invisible; on a long-lived worker that pulls one unit of work after another off a queue it is a real defect, because the *next* unit of work inherits the previous identifier and every log line, metric tag or audit row it emits is attributed to the wrong owner. The failure looks like the system doing something twice for one owner and nothing for another — a duplicated side effect in the record even though the code ran once for each. **The 3.14 shorthand.** From Python 3.14, `Token` implements the context-manager protocol, so the pairing can be written `with var.set(value):` and the block exit resets the variable. This is the same mechanism — the `with` statement holds the token and calls `reset` on exit — just with the bookkeeping made unforgettable. On 3.13 and earlier the token is not a context manager and the `with` form is a `TypeError`; there you write the explicit `try`/`finally`, or wrap it once in your own `contextlib.contextmanager` helper and use that everywhere. **Why not a plain module attribute with save-and-restore?** Two reasons live in this question rather than in any wider discussion of contexts. First, the `MISSING` case above: a hand-rolled "save the old value, run, put it back" cannot restore "unset". Second, the token binds the restore to a specific write. If two layers of code both save and restore an attribute by hand and the inner one is skipped by an early return, nothing detects it; with tokens the inner scope's `reset` is a statement in a `finally` next to the `set` it undoes, and a double `reset` fails loudly rather than corrupting the state. **What to say about API shape.** `set()` returning a token, rather than the old value, is a deliberate design: it makes restoration the only supported undo path, it keeps the sentinel case representable, and it lets the implementation treat the context as an immutable mapping under the hood, where the token identifies the revision being undone. Interviewers who ask this are usually checking whether you can pair a scoped write with a guaranteed scoped restore — the mechanics of the token are the vehicle for that judgment.

  • What is Token.old_value when the variable had no value before that set()?
    It is the class attribute `Token.MISSING`, a sentinel meaning "there was no entry". Resetting with such a token removes the entry rather than writing anything, so later reads fall back to the variable's `default=`, or raise `LookupError` if it was declared without one.
  • What happens if you reset with the same Token twice?
    The second call raises `RuntimeError` saying the token has already been used. Tokens are single use by design: reusing one would mean undoing a write twice, which has no meaningful semantics. If you need the same scope again, do another `set()` and keep the new token.
  • Why is resetting nested tokens out of order worse than forgetting to reset at all?
    Because it fails silently. Each token restores what its own `set()` displaced, so resetting the outer token first reinstalls the value the inner scope had already replaced, and the inner reset then puts back a stale value. Nothing raises; the wrong value simply propagates. Unwind last-in-first-out, or let a `with` block or `finally` enforce the pairing.

The token is a coat-check ticket: set() takes your coat and hands you the ticket, reset() is the only way to get exactly that coat back, and the ticket is torn up after one use.

saying these in an interview costs you the question

  • Says set() returns the previous value
  • Thinks reset() clears the variable to None
  • Reuses a single token for several resets
  • Resets nested tokens in the order they were created
  • Assumes returning from the function restores the old value
  • Calls reset only on the success path, not in finally

context