We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency Without Regret step 31 of 38
request_context: an id that survives tasks and threads
Carry a request id through tasks and threads without leaking it — and without losing it.
@contextmanager
def request_context(request_id: str) -> Iterator[None]: ...
def current_request_id() -> str | None: ...
async def with_request[T](request_id: str, coro: Coroutine[Any, Any, T]) -> T: ...
You also declare the ContextVar itself. That declaration is half the exercise.
What the report proves
Seven scenarios, each checking a different propagation rule:
-
nested_task— tasks created inside the scope see the id.create_taskcopies the creating context, so this works without you doing anything. -
isolation— two concurrent requests, interleaved at every hop, and neither ever observes the other’s id. This is the one whose failure is a data-exposure incident rather than a missing log field. -
no_leak— a value set inside a spawned task does not leak back to the parent. The child got a copy, not a reference. -
exception— the block raises and the previous value is restored anyway. That is what thefinallyis for. -
cancel— the same, when the task is cancelled rather than raising. Without the restore, the id survives into whatever runs next on that task, which in a worker loop is the next request. -
thread—asyncio.to_threadexplicitly propagates the context, so a blocking function reached through it sees the id. (loop.run_in_executordoes not — that difference is exactly onecopy_context().runthatto_threadperforms for you.) -
nested_ctx— nested scopes unwind to what they shadowed:A,B, back toA, then back to nothing.
The two things this is built to catch
reset(token), not set(None). ContextVar.set() returns a Token[T],
and only the token knows what the previous value was. Setting None on the way
out looks equivalent and destroys the nesting case — scenario nested_ctx
fails immediately.
The finally. A restore that only happens on the success path is a restore
that does not happen when it matters. Scenarios exception and cancel both
exit abnormally on purpose.
Where the type system earns its keep
ContextVar[str | None]("request_id", default=None) versus
ContextVar[str]("request_id") is a decision --strict will not let you
avoid, and the two behave differently at runtime: the second raises
LookupError on an unset read.
That makes “no request in flight” an exception rather than a value, so every
caller — including a logging filter that runs on every record, including the
ones emitted at startup — needs a try/except LookupError. For observability
data, which is by nature sometimes absent, the | None form with a default is
almost always right. Reserve the no-default form for values whose absence is
genuinely a bug, where the LookupError is the feature.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.