What does contextvars.copy_context() return, and how do you use Context.run?
answer
- A snapshot you can hold on to
- It behaves like a mapping
- You enter it to run something
- Callable in, coroutine not accepted
- Entering the same one twice fails
basics
~20 scopy_context() returns a contextvars.Context — a snapshot mapping of every ContextVar set in the current context. Context.run(callable, *args) executes a plain callable with that snapshot entered, and any writes it makes land in the snapshot rather than in your context.
solid answer
~40 s`contextvars.copy_context()` gives you a `contextvars.Context` object: a shallow snapshot of every ContextVar binding active right now. It is a mapping, so you can inspect it with `Context.items` or `Context.keys`. `Context.run(fn, *args, **kwargs)` enters that snapshot, calls `fn`, and exits — so `ContextVar.set` inside `fn` mutates the snapshot, not the caller's context, which is the same isolation asyncio gives a task. This is the manual version of what `asyncio.create_task` does for you. Two constraints matter: `run` takes a **callable**, not a coroutine object, so you cannot await inside it directly; and a `Context` cannot be entered re-entrantly — running one that is already entered raises `RuntimeError`. The common uses are preparing a context to hand to `asyncio.create_task(coro, context=ctx)`, and running a callback in a captured context.
code
python · 13 linesimport contextvars
tenant = contextvars.ContextVar("tenant")
tenant.set("reports")
ctx = contextvars.copy_context()
tenant.set("adhoc")
def show():
tenant.set("mutated-inside")
return tenant.get()
print(ctx.run(show)) # mutated-inside
print(tenant.get()) # adhocgo deeper
Know that copy_context() takes a snapshot of the ambient ContextVar values and that Context.run executes a function with that snapshot active. You will rarely call either by hand day to day.
Explain that run takes a callable rather than a coroutine, that writes inside it stay in the snapshot, and that a Context cannot be entered while it is already entered.
Show a real use: capturing a context when a callback is registered so it runs with the ambient values from registration, or preparing a context to hand to a task instead of relying on whatever the creating frame happened to have set.
Weigh whether explicit context plumbing is worth it at all: prepared contexts make ambient state auditable, but every hand-managed snapshot is machinery a future reader must understand versus simply passing a parameter.
### The object `contextvars.copy_context()` returns a `contextvars.Context`: a snapshot of every `ContextVar` that currently has a value, paired with that value. It is a shallow copy — the bindings are copied, the objects they point at are not — and it implements the mapping protocol, so `Context.keys`, `Context.values`, `Context.items` and `Context.get` all work and make a context inspectable in a debugger or a log line. `Context.copy` produces a further snapshot of it. The snapshot is taken at the instant of the call. Anything you `set` afterwards in your own context is not in it, and anything set inside it later is not in yours. That independence is the entire point. ### Entering it A `Context` is not a container you read values out of by hand; it is something you **enter** in order to run code. `Context.run(fn, *args, **kwargs)` activates the snapshot, calls `fn(*args, **kwargs)`, restores whatever was active before, and returns the callable's result. Exceptions propagate normally and the context is still exited. While the snapshot is entered, `ContextVar.get` sees the snapshot's values and `ContextVar.set` writes into the snapshot. So `Context.run` is exactly the isolation boundary that asyncio applies per task — `asyncio.create_task` snapshots and enters a context for you; `Context.run` lets you do it explicitly for ordinary synchronous code. A neat consequence: you can populate a context without ever entering it in a block of your own by running the setter itself, `ctx.run(some_var.set, value)`. ### The two constraints people trip on **It takes a callable, not a coroutine.** `Context.run` is synchronous. Passing a coroutine object does not await it; the way to run *async* work in a prepared context is to pass the context to the task instead — `asyncio.create_task(coro, context=ctx)`, available since Python 3.11, and the same keyword on `asyncio.TaskGroup.create_task`. **A Context cannot be entered twice at once.** Calling `Context.run` on a context that is already entered — including recursively, from inside the callable — raises `RuntimeError: cannot enter context: ... is already entered`. Contexts are not reentrant and they are not thread-safe to enter concurrently: if you want two units of work isolated from each other, give each its own copy rather than sharing one. ### Where it earns its keep * **Preparing a task's ambient state.** Build a context, set the values into it, hand it to `asyncio.create_task` with the `context` keyword. The task no longer depends on what happened to be set in the creating frame. * **Running a callback later in the caller's context.** Capture with `copy_context()` when the callback is registered, then invoke it through `Context.run` when the event fires, so the callback sees the ambient values from registration time rather than from whatever code triggered it. * **Isolating a synchronous helper** that you know mutates ContextVars, so its writes cannot leak back into the caller. * **Inspection.** Since it is a mapping, dumping a context is a cheap way to see what ambient state a unit of work actually carried — much easier than guessing. ### The mental model to state out loud A ContextVar is a key; a Context is a mapping of keys to values; entering a context decides which mapping the key resolves against. `copy_context` makes a mapping, `Context.run` decides when it is the active one, and asyncio's per-task copying is this same machinery applied automatically at task creation. Candidates who can say that sentence rarely get the task-copying questions wrong either. ### Reading a context instead of guessing Because a `Context` implements the mapping protocol, you can iterate it and print what a unit of work is actually carrying. That turns questions like *did the run id reach this code?* from speculation into a two-line check, and it composes with the async side: `asyncio.Task.get_context()`, available since Python 3.12, hands you the context object a task is running in, and you can dump that the same way. Keys are the `ContextVar` objects themselves, so `ContextVar.name` is what you print for a readable label. ### The relationship to what asyncio does for you It is worth stating the equivalence explicitly, because it makes both halves easier to remember. `asyncio.create_task` is, in effect, *snapshot with copy_context, then enter that snapshot around every step of the coroutine*. `Context.run` is the same two moves, done by hand, for one synchronous call. Everything that is true of one is true of the other: writes stay inside, the copy is shallow, and the snapshot reflects the instant it was taken rather than the instant the code runs. If you can restate the task-copying rule in terms of `copy_context` and `run`, you understand both. In day-to-day application code you rarely call either directly — the framework or the task machinery does it for you. Reach for them when you need the capture to happen somewhere other than where the work is launched, and be ready to justify the extra machinery against simply passing the value as an argument.
- Can you pass a coroutine object to Context.run to await it in that context?No. `Context.run` is synchronous: it calls the object and returns the result, so a coroutine object would simply be created and never awaited. To run async work in a prepared context, pass the context to the task instead — `asyncio.create_task(coro, context=ctx)`, or the same `context` keyword on `asyncio.TaskGroup.create_task`. Both have accepted it since Python 3.11.
- What happens if two threads call Context.run on the same Context object at the same time?One of them fails. A `contextvars.Context` cannot be entered more than once at a time, and the second entry raises `RuntimeError` saying the context is already entered — the same error you get from re-entering it recursively in one thread. Give each unit of work its own snapshot from `contextvars.copy_context()`, or copy the context per use, rather than sharing one object across concurrent runners.
- Is the snapshot returned by copy_context() a deep copy?No, it is shallow. The bindings are copied, so a later `ContextVar.set` on either side is invisible to the other, but the values themselves are the same objects. If a ContextVar holds a mutable object such as a dict, code running in the snapshot mutates the very same dict the original context sees. That is the usual way ambient isolation is accidentally defeated.
saying these in an interview costs you the question
- Thinks copy_context deep-copies the values it holds
- Passes a coroutine object to Context.run expecting it to await
- Believes a Context can be entered recursively
- Treats a Context as a dict you write into directly
- Shares one Context object across concurrent runners