You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ThreadPoolExecutor tasks see the context of whichever submit() call started their worker thread #159964
When sys.flags.thread_inherit_context is true (the default on the free-threaded build), ThreadPoolExecutor workers copy the context of the submit() call that happened to start them, and every later task on that worker sees that copy:
with precision=2: 3.14 3.14
no with block: 3.14159000 3.14
with precision=5: 3.14159 3.14
The pool column is stuck at precision=2 because its only worker was started inside the first with block. With more than one worker, the value a task sees depends on which worker runs it.
I think you could argue that this is a bug. For each task to see the submitting thread’s context, the pool would need to capture that context with contextvars.copy_context() at submission time and execute the task using that copy’s run() method. Currently, when thread_inherit_context is true, contextvar bindings are copied when the pool starts a worker thread, and that worker keeps using its context for subsequent tasks.
Edit: also note that copy_context() and using Context.run() would add a little overhead. However the ContextVars mechanism is designed specifically to be efficient to be used like this. Since asyncio already uses run() similarly, I think it's hard to argue it's too expensive for thread based work.
Today this prints "opened a connection" once, because each worker keeps a single context across tasks. With a naive fix for the reproducer in the issue, each task runs in a fresh copy of the submitter's context, and it prints three times. asgiref.local.Local is documented as thread-local in sync threads, and asgiref already works aroundthread_inherit_context for new threads (django/asgiref#564), but that can't help here. Django keeps its cache backends in a Local, so with this change Django would create a new cache backend for every pool task on the free-threaded build (or with -X thread_inherit_context=1).
So sadly the easy fix I think requires Guido's time machine. It's possible to fix this in asgiref but I'm not sure if they'd be open to that.
Activity
ngoldbaum commentedon Oct 20, 2026
ping @nascheme
nascheme commentedon Oct 20, 2026
I think you could argue that this is a bug. For each task to see the submitting thread’s context, the pool would need to capture that context with
contextvars.copy_context()at submission time and execute the task using that copy’s run() method. Currently, whenthread_inherit_contextis true, contextvar bindings are copied when the pool starts a worker thread, and that worker keeps using its context for subsequent tasks.Edit: also note that
copy_context()and usingContext.run()would add a little overhead. However the ContextVars mechanism is designed specifically to be efficient to be used like this. Since asyncio already usesrun()similarly, I think it's hard to argue it's too expensive for thread based work.pythongh-159964: Propagate contextvars to ThreadPoolExecutor tasks
ngoldbaum commentedon Oct 21, 2026
I’d like to fix this during the core dev sprint.
ngoldbaum commentedon Oct 21, 2026
Unfortunately there are significant downstream uses that making this change would break. In particular:
Today this prints "opened a connection" once, because each worker keeps a single context across tasks. With a naive fix for the reproducer in the issue, each task runs in a fresh copy of the submitter's context, and it prints three times.
asgiref.local.Localis documented as thread-local in sync threads, and asgiref already works aroundthread_inherit_contextfor new threads (django/asgiref#564), but that can't help here. Django keeps its cache backends in aLocal, so with this change Django would create a new cache backend for every pool task on the free-threaded build (or with-X thread_inherit_context=1).So sadly the easy fix I think requires Guido's time machine. It's possible to fix this in asgiref but I'm not sure if they'd be open to that.