Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 15 – Async programming with asyncio

Context variables and thread-local data

Keep a connection per thread with threading.local, carry a request id through asyncio tasks with ContextVar, and see how tasks and threads inherit context.

  • Advanced
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 10 of 25 minutes free
Free: A global breaks as soon as two requests overlap · Every task runs in a copy of the context · threading.local() follows threads, a ContextVar follows tasks · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

What you will learn

  • Store per-thread data such as a database connection with threading.local
  • Use a ContextVar for per-task values such as a request id in async code
  • Restore a context variable with a token used as a context manager
  • Explain how new tasks, threads and executor jobs inherit context
  • Add the current request id to every log record with a logging filter

Before you start

What you’ll learn · Before you start

What you will learn

  • Store per-thread data such as a database connection with threading.local
  • Use a ContextVar for per-task values such as a request id in async code
  • Restore a context variable with a token used as a context manager
  • Explain how new tasks, threads and executor jobs inherit context
  • Add the current request id to every log record with a logging filter

Before you start

On this page

Two requests, r-17 and r-42, are served at the same time as asyncio task: A coroutine wrapped by asyncio.create_task() and scheduled on the running event loop. By default it starts the next time the code that is running pauses, runs alongside other tasks, and keeps the coroutine's result or exception for whoever awaits it. More in “Tasks, gather and running coroutines”, and both of their log lines say r-42. The id sat in one module-level variable, and the second request overwrote it while the first was waiting for its database. Values like this describe what a program is doing right now: the request being served, the signed-in user, an open transaction. Passing them through every call is noisy.

Python has two tools for them. threading.local() keeps a separate set of attributes for each thread, which suits code that serves one request per thread. A contextvars.ContextVar keeps one value per context (contextvars): In contextvars, a mapping from context variables to their values. Each thread has its own, asyncio gives every task a copy taken when the task is created, and copy_context() returns a copy of the current one., and asyncio runs every task in its own copy of the context, so a value set in one task is invisible to the others even though they all share one thread. Use thread-local data: Attributes of a threading.local() object, which each thread sees separately: a value one thread assigns is invisible to the others. The tasks of one event loop share its thread, and so share these attributes. for resources that belong to a thread, such as a database connection, and context variable: A contextvars.ContextVar: a variable whose value is kept per context rather than once for the program. asyncio runs every task in its own copy of the context, so a value one task sets is invisible to the others, even on the same thread. for per-request values in async code, or in any code that may become async.

A global breaks as soon as two requests overlap

Two requests, handled as two asyncio tasks, store their id first in a module-level variable and then in a ContextVar. Each handler awaits once, the way it would await a database call:

One global, two requests Python · shared_global.py
import asyncio
from contextvars import ContextVar

current_request = "-"                                   # one global for the whole program
request_id = ContextVar("request_id", default="-")      # one value per context


def log_global(message):
    print(f"[{current_request}] {message}")


def log_context(message):
    print(f"[{request_id.get()}] {message}")


async def handle_with_global(rid, item):
    global current_request
    current_request = rid
    await asyncio.sleep(0)              # a database call: other tasks run meanwhile
    log_global(f"loaded {item}")


async def handle_with_contextvar(rid, item):
    request_id.set(rid)
    await asyncio.sleep(0)
    log_context(f"loaded {item}")


async def main():
    print("with a global:")
    async with asyncio.TaskGroup() as tg:
        tg.create_task(handle_with_global("r-17", "cart"))
        tg.create_task(handle_with_global("r-42", "invoice"))
    print("with a ContextVar:")
    async with asyncio.TaskGroup() as tg:
        tg.create_task(handle_with_contextvar("r-17", "cart"))
        tg.create_task(handle_with_contextvar("r-42", "invoice"))
    print("main still sees:", repr(request_id.get()))


asyncio.run(main())

Output

with a global:
[r-42] loaded cart
[r-42] loaded invoice
with a ContextVar:
[r-17] loaded cart
[r-42] loaded invoice
main still sees: '-'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shared_global.py

With the global, both lines claim to belong to request r-42. Task A stored r-17, paused at its await, and task B overwrote the global before A wrote its log line. Nothing raised an error; the log was simply wrong, which is how this bug appears in production, rarely and under load. With the ContextVar, each task logged its own id, and main() still saw the default "-" afterwards, because request_id.set() changed the value only in the context of the task that called it. Step through both runs and watch the value in the middle, written by both tasks and then copied for each:

Interactive · message by message Two requests at once, a global and then a ContextVar Step through shared_global.py. Task A and task B take turns at their awaits; watch the value in the middle.
Two requests at once, a global and then a ContextVar, step 9 of 9. Messages so far: task A to the shared value: current_request = "r-17"; task B to the shared value: current_request = "r-42"; the shared value to task A: reads "r-42"; the shared value to task B: reads "r-42"; task A to the shared value: request_id.set("r-17"); task B to the shared value: request_id.set("r-42").task A[r-17] loaded cartthe shared valuemain() still sees "-"task B[r-42] loaded invoicecurrent_request = "r-17"current_request = "r-42"reads "r-42"reads "r-42"request_id.set("r-17")request_id.set("r-42")

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Two requests at once. main() starts two handlers in a TaskGroup. Task A serves request r-17 and loads a cart; task B serves r-42 and loads an invoice. Both keep the id in one module-level variable, current_request.
  2. A stores r-17, then awaits. Task A writes r-17 to the global and awaits asyncio.sleep(0), the stand-in for a database call. The await gives the event loop to the next ready task.
  3. B overwrites it. Task B runs while A waits and writes r-42 to the same variable. Nothing complains, because there is one name and B may rebind it.
  4. A logs the wrong id. A resumes and logs. log_global() reads the global, which now holds r-42, so the line says [r-42] loaded cart, although the cart belongs to r-17.
  5. B is right by luck. B logs [r-42] loaded invoice, right only because it wrote last. Both lines claim r-42 and nothing raised an error, which is how the bug shows up in production, rarely and under load.
  6. The same with a ContextVar. The second run keeps the id in request_id, a ContextVar. The TaskGroup gave each new task its own copy of the context, so a set() changes only the copy of the task that calls it.
  7. A sets its own copy. Task A calls request_id.set("r-17") and awaits. The value goes into A's copy of the context and nowhere else.
  8. B sets its own copy. Task B calls request_id.set("r-42") while A waits. This time the write lands in B's copy, so A's value stays r-17.
  9. Each logs its own id. After the same awaits, A logs [r-17] loaded cart and B logs [r-42] loaded invoice. main() still reads the default "-" afterwards, because a value set inside a task never reaches the code that created it.

Checkpoint

Check: A global shared by overlapping requests

  1. Question 1 of 2 In shared_global.py, what lets task B overwrite the global before task A writes its log line?

    Choose one answer.

    A hint

    Look for the line where task A gives the event loop away.

    Show the answer: Question 1 of 2

    Answer: Task A's await asyncio.sleep(0) between storing the id and logging it

    Task A stored r-17 and awaited. While it waited, the loop ran task B, which stored r-42 in the same global, so when A resumed and logged, it read r-42.

  2. Question 2 of 2 In a real service, the same bug mislabels about one log line in a thousand and never shows up in tests that send one request at a time. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It needs two requests in flight at once, and one of them must log after an await that let the other one run

    The window opens only when two handlers overlap and one of them awaits between writing the global and reading it. A test that sends one request at a time never opens it; under load it opens now and then.

Every task runs in a copy of the context

asyncio.create_task() copies the current context when it creates a task, and the task runs in that copy for its whole life. The figure follows the second run above: main()’s context at the top, a copy for each task in the middle, and main()’s value unchanged at the bottom.

main() has request_id "-"; create_task() gives task A and task B a copy each; each sets its own id; main() still sees "-" afterwards.main()'s contextrequest_id = -create_task() takes a copy for each taskmain() after both tasksrequest_id = -Task A's copyset('r-17')request_id = r-17Task B's copyset('r-42')request_id = r-42a snapshot at creationa task's set() neverreaches its parent

Every task runs in its own copy of the context

Text description of the diagram

The diagram runs from top to bottom.

1. At the top, main() runs in a context where request_id has no value, so get() returns the default, "-". 2. create_task() takes a snapshot of that context for each new task. Task A calls set("r-17") and task B calls set("r-42"); each call changes only the task's own copy, so A reads r-17 and B reads r-42, whatever order they run in. 3. At the bottom, after both tasks have finished, main() still reads "-": a value set inside a task never reaches the task that created it.

Three rules follow from that. A task sees the values its creator had set before creating it. Whatever the task sets stays in its own copy and never reaches its parent. And the copy is A copy that holds the same objects as the original, not copies of them: a list in a copied context is the one list every copy reaches.: a list stored in a context variable is the same list object in every copy.

Key idea

A task’s copy of the context is taken when the task is created: it starts with what its creator had set, and what the task sets stays in the copy.

Checkpoint

Check: Each task's copy of the context

  1. Question 1 of 2 A task created with asyncio.create_task() calls request_id.set("r-9"). What does the code that created the task read from request_id afterwards?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The value it had before, because the task changed only its own copy

    create_task() copies the current context, and the task runs in that copy for its whole life. Its set() writes to the copy, so its creator reads its own value afterwards, as main() read "-" in the first program.

  2. Question 2 of 2 Before creating two tasks, main() sets a context variable to an empty list, and each task appends its own items to var.get(). What does each task find in the list?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The items of both tasks, because both copies hold the same list

    A copy of the context holds the same values as the original, and a list is one object, so appending in one task changes what every copy sees. Only set() is per copy.

threading.local() follows threads, a ContextVar follows tasks

Thread-local data is tied to the thread, and every task of an event loop runs in the loop’s thread, so it cannot tell two concurrent requests apart in async code. Context variables cover threads too, because each thread has a context of its own. The standard library made the same move in Python 3.7: decimal keeps its active context in a context variable, so decimal arithmetic uses the right precision inside each asyncio task. Answer the two questions for a value of your own; the table marks the tool that fits:

Interactive · answer and compare Where does this value belong? Answer two questions about a value of your own. The table marks the tool that fits and says why the others do not.
Does the code run as asyncio tasks, or may it become async one day?
What does the value belong to?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

A global, threading.local() and a ContextVar, against the five things that decide between them
Tool Tasks sharing one thread Worker threads A new thread starts with Undoing a value Typical use
A module-level global Keeps one value per: the whole program Poor one value for all of them: both log lines said r-42 Poor every thread reads and writes the same value Mixed the same value as every other thread Mixed assign the old value back by hand Mixed values that are the same for every request
threading.local() Keeps one value per: thread Poor the tasks of a loop share its thread, so they share the attributes Good each thread sees only the attributes it assigned Mixed no attributes: nothing is passed on Good del the attribute Good a connection or buffer per worker thread
A ContextVar Keeps one value per: context: every asyncio task, and every thread Good each task sees its own value: r-17 and r-42 Good each thread has a context of its own Mixed on the default build an empty context, unless it is given one Good reset() with the token from set(), or a with block Good a request id, a user or a trace id
How the answers decide
  • “Does the code run as asyncio tasks, or may it become async one day?” Yes, asyncio tasks on an event loop: rules out threading.local() and A module-level global. Every task of an event loop runs in the loop's one thread, so thread-local attributes and a global are both shared by every request in flight.
  • “What does the value belong to?” A request: an id, a user, a trace: rules out A module-level global. A global holds one value for the whole program, so overlapping requests overwrite each other's ids without any error.
  • “Does the code run as asyncio tasks, or may it become async one day?” No, it serves one request per thread and “What does the value belong to?” A request: an id, a user, a trace: adds a caveat to threading.local(). Thread-local data separates requests only while each request has a thread of its own, and stops doing so the day the code becomes async.
  • “What does the value belong to?” A thread: a connection or a buffer: rules out A module-level global. A global is one value for every thread, so it cannot give each thread a resource of its own.
  • “What does the value belong to?” A thread: a connection or a buffer: adds a caveat to A ContextVar. A resource such as a connection stays with the thread that opened it, which is what threading.local() is for; a context variable follows tasks and requests.

A request id in threading.local() in async code

state.request_id = rid in an async def handler is shared by every task on the loop’s thread, so after an await the handler can read another request’s id. Keep the id in a module-level ContextVar and call request_id.set(rid) instead.

Checkpoint

Check: threading.local() or a ContextVar

  1. Question 1 of 2 Which of these keeps the current user per request in an asyncio service?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A ContextVar, set at the start of each request

    In asyncio the request is the task, not the thread, and a ContextVar is the tool that keeps a value per task. Thread-local data and globals are shared by every task on the loop's thread.

  2. Question 2 of 2 A web app serves each request on its own thread and keeps the request id in threading.local(). The team moves the app to asyncio. What happens to the ids?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Requests on the loop's thread share the attribute, and one can log another's id after an await

    threading.local() separates threads, and one request per thread made it separate requests too. On an event loop many requests share one thread, so the id moves to a ContextVar, which works in threaded code as well.

A token puts the old value back

set() returns a token (contextvars): The object ContextVar.set() returns. It remembers the value the variable had before, and var.reset(token) puts that value back, once. Since Python 3.14 a token is also a context manager, so a with block resets the variable at its end. that remembers the previous value. Since Python 3.14 the token is also a context manager: An object with __enter__() and __exit__() methods. The with statement calls __enter__() before its block and __exit__() after it, however the block ends, so a file is closed or a lock released even after an error. More in “Context managers and the with statement”, so a with block sets a value and puts the old one back at its end, even when an exception leaves the block. Step through the program; each show() prints the value of that moment:

Interactive · line by line A value for one block, then the old one again Step through the program. The lit line has just run, and each show() prints the value request_id holds at that moment.

Program · token_scope.py

from contextvars import ContextVar, Token

request_id = ContextVar("request_id", default="-")


def show(where):
    print(f"{where:<22} request_id = {request_id.get()}")


show("before")
with request_id.set("r-7") as token:        # a Token is a context manager (3.14)
    show("inside")
    print(f"{'token.old_value':<22} is Token.MISSING: {token.old_value is Token.MISSING}")
    with request_id.set("r-7/retry"):
        show("nested")
    show("after the nested with")
show("after")

token = request_id.set("r-8")               # the same, written out by hand
try:
    show("inside try")
finally:
    request_id.reset(token)
show("after reset")
try:
    request_id.reset(token)
except RuntimeError as e:
    print("resetting with it again raises", type(e).__name__)

Variables

After line 28: ContextVar is ContextVar; Token is Token; request_id is <ContextVar object>; show is show(where); token is <Token object>.ContextVarContextVarTokenTokenrequest_id<ContextVar object>showshow(where)token<Token object>
  • changed in this step

Printed so far

before                 request_id = -
inside                 request_id = r-7
token.old_value        is Token.MISSING: True
nested                 request_id = r-7/retry
after the nested with  request_id = r-7
after                  request_id = -
inside try             request_id = r-8
after reset            request_id = -
resetting with it again raises RuntimeError

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 23 steps are listed below.

Line run 28
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. from contextvars import ContextVar, Token loads ContextVar and Token.
  3. Line 3. request_id = ContextVar("request_id", default="-") sets request_id to <ContextVar object>.
  4. Line 6. def show(where): makes the function show. Its body runs only when it is called.
  5. Line 10. show("before") prints before request_id = -.
  6. Line 11. with request_id.set("r-7") as token: runs. set() returned a token, and the with block holds it as token. Until the block ends, request_id is r-7.
  7. Line 12. show("inside") prints inside request_id = r-7.
  8. Line 13. print(f"{'token.old_value':<22} is Token.MISSING: {token.old_value is Token.MISSING}") prints token.old_value is Token.MISSING: True. Key point The token remembers the value from before its set(). The variable had none, so token.old_value is the marker Token.MISSING.
  9. Line 14. with request_id.set("r-7/retry"): runs.
  10. Line 15. show("nested") prints nested request_id = r-7/retry. The nested block set r-7/retry over r-7. Its own token remembers r-7, so the end of this block puts r-7 back.
  11. Line 14. with request_id.set("r-7/retry"): runs.
  12. Line 16. show("after the nested with") prints after the nested with request_id = r-7. The nested block has ended and r-7 is back. Each block restores the value it found, so blocks can nest.
  13. Line 11. with request_id.set("r-7") as token: runs. The outer block ends here, and its token puts back what was there before its set(): no value at all.
  14. Line 17. show("after") prints after request_id = -. The outer block has ended too. The variable has no value again, so get() returns the default, "-".
  15. Line 19. token = request_id.set("r-8") sets token to <Token object> (it was <Token object>).
  16. Line 20. try: runs.
  17. Line 21. show("inside try") prints inside try request_id = r-8.
  18. Line 23. request_id.reset(token) leaves request_id as it was. Key point reset(token) in a finally block is what the with statement does for you, and the way to write it on Python 3.13 and older.
  19. Line 24. show("after reset") prints after reset request_id = -.
  20. Line 25. try: runs.
  21. Line 26. request_id.reset(token) leaves request_id as it was.
  22. Line 27. Line 27 runs.
  23. Line 28. print("resetting with it again raises", type(e).__name__) prints resetting with it again raises RuntimeError. e no longer exists. That was the last line, so the program ends. A token works once: the second reset() with the same token raised RuntimeError.

Inside the outer block the id was r-7; the nested block replaced it, and the end of that block put r-7 back. token.old_value is Token.MISSING because the variable had no value before the first set(). The try and finally form at the end is what the with block does for you, and the form to use on Python 3.13 and older. A token works once: resetting with it a second time raises RuntimeError.

Four more tools complete the picture. contextvars.copy_context() takes a snapshot of the current context, and Context.run() calls a function inside such a snapshot. threading.Thread(context=...), new in 3.14, sets the context a thread starts with. asyncio.to_thread() passes the caller’s context to its thread, while loop.run_in_executor() passes none. A logging.Filter can copy a context variable into every log record.

Checkpoint

Check: Undoing a value with its token

  1. Question 1 of 2 request_id has the default "-" and no value yet. What does request_id.get() return right after a with request_id.set("r-1"): block ends?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "-"

    After the block the variable has no value again, as before the set(), so get() returns the default, "-", as the program's line "after" showed.

  2. Question 2 of 2 A program runs token = var.set(5), then calls var.reset(token) twice. What happens on the second call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It raises RuntimeError, because a token works once

    A token remembers one previous value and can put it back once. The program's last line tried a second reset() with the same token and caught RuntimeError.

Check yourself

One question at a time on the four ideas above, the one you know least first.

Practice that adapts to you

Practise this lesson

10 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 What does shared_global.py print?

    What does this program print? Choose one answer.

    import asyncio
    from contextvars import ContextVar
    
    current_request = "-"                                   # one global for the whole program
    request_id = ContextVar("request_id", default="-")      # one value per context
    
    
    def log_global(message):
        print(f"[{current_request}] {message}")
    
    
    def log_context(message):
        print(f"[{request_id.get()}] {message}")
    
    
    async def handle_with_global(rid, item):
        global current_request
        current_request = rid
        await asyncio.sleep(0)              # a database call: other tasks run meanwhile
        log_global(f"loaded {item}")
    
    
    async def handle_with_contextvar(rid, item):
        request_id.set(rid)
        await asyncio.sleep(0)
        log_context(f"loaded {item}")
    
    
    async def main():
        print("with a global:")
        async with asyncio.TaskGroup() as tg:
            tg.create_task(handle_with_global("r-17", "cart"))
            tg.create_task(handle_with_global("r-42", "invoice"))
        print("with a ContextVar:")
        async with asyncio.TaskGroup() as tg:
            tg.create_task(handle_with_contextvar("r-17", "cart"))
            tg.create_task(handle_with_contextvar("r-42", "invoice"))
        print("main still sees:", repr(request_id.get()))
    
    
    asyncio.run(main())
    Show the answer: Question 1 of 10

    Answer: it prints

    with a global:
    [r-42] loaded cart
    [r-42] loaded invoice
    with a ContextVar:
    [r-17] loaded cart
    [r-42] loaded invoice
    main still sees: '-'

    Task A stores r-17 in the global and pauses at its await; task B stores r-42 before A logs, so both lines say r-42. With the ContextVar, each set() changes only the copy of the context that its own task runs in, so each task logs its own id and main() still reads the default.

  2. Question 2 of 10 A handler runs current_request = rid, logs a line at once, and only then awaits the database. Which of its log lines are safe?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: The line before the first await; any line after an await can carry another request's id

    Tasks switch only at await points. Until this handler's first await, nobody else can write the global; after it, any line may read the id of whichever request wrote last.

  3. Question 3 of 10 When does a new asyncio task get its copy of the context?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: When asyncio.create_task() creates it

    asyncio.create_task() copies the current context when it creates the task. From then on the task reads and writes only that copy.

  4. Question 4 of 10 A handler sets request_id, starts a background task that sends an email, and returns at once. The email task logs a second later, after the handler's own task has finished. Which id does its line carry?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: The handler's request id, held in the task's own copy

    A task sees the values its creator had set before creating it and keeps its copy for its whole life, so the email task logs the right id even after the handler has returned.

  5. Question 5 of 10 What does a threading.local() object keep apart?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: Its attributes, one set per thread

    threading.local() gives every thread its own attributes on the same object. It separates threads, not tasks.

  6. Question 6 of 10 A program runs jobs on the eight threads of a ThreadPoolExecutor, and each thread needs its own database connection, opened once and reused for every job that thread runs. Where do you keep the connection?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: In an attribute of a threading.local() object

    The connection belongs to the thread, which is exactly what thread-local data is for: each worker thread sees its own attribute, and a job finds the connection its thread opened earlier. A global would be shared by every thread, and a value the main thread puts in a ContextVar does not reach the worker threads, which start with an empty context in the default build.

  7. Question 7 of 10 Two asyncio tasks run on the same event loop. Each stores the current user in state.user, where state = threading.local(), then awaits a database call and reads state.user again. What can go wrong?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Both tasks run in the same thread, so the second task's assignment can overwrite the first task's user

    Thread-local data is kept per thread, and every task of one event loop runs in the loop's thread. While the first task waits, the second one can replace the shared attribute, so the first task reads the wrong user after its await. A ContextVar keeps a value per task instead.

  8. Question 8 of 10 A context variable has never been set. What is token.old_value for the token its first set() returns?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: Token.MISSING

    A token remembers the previous value, or the marker Token.MISSING when there was none, as the program printed: token.old_value is Token.MISSING: True.

  9. Question 9 of 10 What does this print on Python 3.14?

    Read the code, then choose one answer.

    from contextvars import ContextVar
    
    user = ContextVar("user", default="guest")
    with user.set("asha"):
        with user.set("ravi"):
            pass
        print(user.get(), end=" ")
    print(user.get())
    Show the answer: Question 9 of 10

    Answer: asha guest

    Each with block resets the variable to the value it had before its own set(). The inner block ends first and puts back "asha"; the outer block then puts back the default, "guest".

  10. Question 10 of 10 Library code must also run on Python 3.13. How does it set a request id for one block of work and restore the old value afterwards?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: token = request_id.set(rid), the work in a try block, and request_id.reset(token) in finally

    On 3.13 and older a token is not a context manager, so the try and finally form restores the old value whether the work succeeds or raises.

How context flows into tasks, threads and executors, with four failures reproduced Pro: opens with a Pro, Premium or Ultimate pass

Context reaches a task when the task is created, reaches a thread only when someone passes it, and reaches an executor job only by hand. This part runs every case, reproduces the four failures these rules cause and shows the fix for each.

In this part:

  • When a task takes its copy, traced step by step
  • Threads and executors: three failures reproduced and fixed
  • A mutable value shared by every copy, reproduced

3 diagrams · 4 runnable examples (Python) · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Per-thread resources and request ids in every log line, measured Pro: opens with a Pro, Premium or Ultimate pass

threading.local() still has a job in threaded code, and a context variable is the natural home of a request id in logs. This part builds both, measures what a lookup of each costs and ends with an exercise.

In this part:

  • One database connection per worker thread, counted
  • Request ids in every log line, from any library
  • What a lookup costs, measured
  • A worker that serves many requests must reset the context, reproduced
  • Exercise: a request context and a log line that knows it
    • A request context and a log line that knows it

2 diagrams · 4 runnable examples (Python) · 1 exercise with sample tests · about 12 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between threading.local() and a contextvars.ContextVar, and which one should asyncio code use?

    threading.local() gives each thread its own set of attributes: a value assigned in one thread is invisible in the others. A ContextVar stores a value in the current context, and Python keeps a context per thread and, in asyncio, a copy per task, taken when the task is created. In threaded code the two behave alike. In asyncio they differ, because all the tasks of an event loop run in the same thread: with threading.local(), two concurrent requests overwrite each other's values, while with a ContextVar each task sees only what it, or the code that created it, set. So asyncio code keeps per-request values such as a request id or the current user in context variables, created once at module level, and keeps threading.local() for resources that belong to a thread, such as a database connection in a thread pool.

  2. Mid to senior

    Under load, about one log line in a thousand from an async web service carries another request's id. Each handler stores the id in a module-level variable as its first step, and the log formatter reads it. Explain the bug, and fix it so that every line, including lines that libraries log, carries the right id.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A tenant id kept in a ContextVar is visible inside a function run with asyncio.to_thread(), but the same function run with loop.run_in_executor() sees the default value. A teammate proposes a global dict keyed by thread id. Explain the difference, and what you do instead.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A ContextVar holds a dict of settings, set once at start-up; handlers change it with settings.get()["locale"] = the user's locale. Concurrent requests now render pages in each other's languages, though every request runs in its own task. Why does copying the context not protect them, and how do you redesign it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A global holds one value for the whole program, so concurrent requests overwrite each other’s values without any error.
  • Every asyncio task runs in a copy of the context taken when it is created, so what it sets never reaches its creator.
  • threading.local() keeps attributes per thread: right for per-thread resources, wrong for asyncio tasks, which share their loop’s thread.
  • with var.set(value): sets a value for one block and restores the old one afterwards, on Python 3.14 and newer.

Terms in this lesson

context (contextvars)
In contextvars, a mapping from context variables to their values. Each thread has its own, asyncio gives every task a copy taken when the task is created, and copy_context() returns a copy of the current one.
context variable
A contextvars.ContextVar: a variable whose value is kept per context rather than once for the program. asyncio runs every task in its own copy of the context, so a value one task sets is invisible to the others, even on the same thread.
thread-local data
Attributes of a threading.local() object, which each thread sees separately: a value one thread assigns is invisible to the others. The tasks of one event loop share its thread, and so share these attributes.
token (contextvars)
The object ContextVar.set() returns. It remembers the value the variable had before, and var.reset(token) puts that value back, once. Since Python 3.14 a token is also a context manager, so a with block resets the variable at its end.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.