From 526dfe849fa22b7f5882d798aa0036cb4235eeb5 Mon Sep 17 00:00:00 2001 From: methatp0 <269683727+methatp0@users.noreply.github.com> Date: Wed, 21 Oct 2026 07:24:56 +0200 Subject: [PATCH] gh-159964: Propagate contextvars to ThreadPoolExecutor tasks --- Doc/library/concurrent.futures.rst | 8 +++++ Lib/concurrent/futures/thread.py | 7 +++-- .../test_thread_pool.py | 29 +++++++++++++++++++ ...-10-21-07-31-00.gh-issue-159964.K7mP2x.rst | 1 + 4 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-21-07-31-00.gh-issue-159964.K7mP2x.rst diff --git a/Doc/library/concurrent.futures.rst b/Doc/library/concurrent.futures.rst index 845a9199ef7cf0..45c7b3964c2653 100644 --- a/Doc/library/concurrent.futures.rst +++ b/Doc/library/concurrent.futures.rst @@ -190,6 +190,11 @@ And:: pending jobs will raise a :exc:`~concurrent.futures.thread.BrokenThreadPool`, as well as any attempt to submit more jobs to the pool. + Each callable submitted with :meth:`submit` runs in a copy of the + :mod:`contextvars` context of the thread that submits it. Changes to + context-variable bindings made by a task do not affect the submitting + thread's context or the context used by later tasks. + .. versionchanged:: 3.5 If *max_workers* is ``None`` or not given, it will default to the number of processors on the machine, @@ -219,6 +224,9 @@ And:: Default value of *max_workers* is changed to ``min(32, (os.process_cpu_count() or 1) + 4)``. + .. versionchanged:: 3.16 + Tasks run in a copy of the submitting thread's context. + .. _threadpoolexecutor-example: diff --git a/Lib/concurrent/futures/thread.py b/Lib/concurrent/futures/thread.py index 909359b648709f..1e5ea5f783f751 100644 --- a/Lib/concurrent/futures/thread.py +++ b/Lib/concurrent/futures/thread.py @@ -6,6 +6,7 @@ __author__ = 'Brian Quinlan (brian@sweetapp.com)' from concurrent.futures import _base +import contextvars import itertools import queue import threading @@ -54,7 +55,7 @@ def prepare(cls, initializer, initargs): def create_context(): return cls(initializer, initargs) def resolve_task(fn, args, kwargs): - return (fn, args, kwargs) + return (contextvars.copy_context(), fn, args, kwargs) return create_context, resolve_task def __init__(self, initializer, initargs): @@ -69,8 +70,8 @@ def finalize(self): pass def run(self, task): - fn, args, kwargs = task - return fn(*args, **kwargs) + context, fn, args, kwargs = task + return context.run(fn, *args, **kwargs) class _WorkItem: diff --git a/Lib/test/test_concurrent_futures/test_thread_pool.py b/Lib/test/test_concurrent_futures/test_thread_pool.py index 226b7acb64fb29..1c10e21e27aaf7 100644 --- a/Lib/test/test_concurrent_futures/test_thread_pool.py +++ b/Lib/test/test_concurrent_futures/test_thread_pool.py @@ -1,4 +1,5 @@ import contextlib +import contextvars import multiprocessing as mp import multiprocessing.process import multiprocessing.util @@ -14,6 +15,34 @@ class ThreadPoolExecutorTest(ThreadPoolMixin, ExecutorTest, BaseTestCase): + def test_contextvars_are_copied_for_each_task(self): + value = contextvars.ContextVar("value", default="default") + + def read_value(): + return value.get() + + def change_value(): + value.set("worker") + + with self.executor_type(max_workers=1) as executor: + token = value.set("first") + try: + self.assertEqual(executor.submit(read_value).result(), "first") + finally: + value.reset(token) + + self.assertEqual(executor.submit(read_value).result(), "default") + + token = value.set("third") + try: + self.assertEqual(executor.submit(read_value).result(), "third") + finally: + value.reset(token) + + # Changes made by one task must not leak into the next task. + executor.submit(change_value).result() + self.assertEqual(executor.submit(read_value).result(), "default") + def test_map_submits_without_iteration(self): """Tests verifying issue 11777.""" finished = [] diff --git a/Misc/NEWS.d/next/Library/2026-10-21-07-31-00.gh-issue-159964.K7mP2x.rst b/Misc/NEWS.d/next/Library/2026-10-21-07-31-00.gh-issue-159964.K7mP2x.rst new file mode 100644 index 00000000000000..324b66b3a5ad69 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-21-07-31-00.gh-issue-159964.K7mP2x.rst @@ -0,0 +1 @@ +Fix :class:`concurrent.futures.ThreadPoolExecutor` to run each submitted callable in a copy of the submitting thread's :mod:`contextvars` context.