diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst
index f183f3f535c4cb..91af9bff615d03 100644
--- a/Doc/library/threading.rst
+++ b/Doc/library/threading.rst
@@ -334,7 +334,7 @@ since it is impossible to detect the termination of alien threads.
.. class:: Thread(group=None, target=None, name=None, args=(), kwargs={}, *, \
- daemon=None)
+ daemon=None, context=None)
This constructor should always be called with keyword arguments. Arguments
are:
@@ -359,6 +359,11 @@ since it is impossible to detect the termination of alien threads.
If ``None`` (the default), the daemonic property is inherited from the
current thread.
+ *context* is the :class:`~contextvars.Context` value to use while running
+ the thread. The default value is ``None`` which means to use a copy
+ of the context of the caller of :meth:`~Thread.start`. To start with
+ an empty context, pass a new instance of :class:`~contextvars.Context`
+
If the subclass overrides the constructor, it must make sure to invoke the
base class constructor (``Thread.__init__()``) before doing anything else to
the thread.
@@ -369,6 +374,12 @@ since it is impossible to detect the termination of alien threads.
.. versionchanged:: 3.10
Use the *target* name if *name* argument is omitted.
+ .. versionchanged:: 3.14
+ Threads now inherit the context of the caller of :meth:`~Thread.start`
+ instead of starting with an empty context. The *context* parameter
+ was added. Pass a new :class:`~contextvars.Context()` if an empty context
+ is required.
+
.. method:: start()
Start the thread's activity.
diff --git a/Lib/test/test_context.py b/Lib/test/test_context.py
index 82d1797ab3b79e..55704f20afcba4 100644
--- a/Lib/test/test_context.py
+++ b/Lib/test/test_context.py
@@ -383,6 +383,54 @@ def sub(num):
tp.shutdown()
self.assertEqual(results, list(range(10)))
+ @isolated_context
+ @threading_helper.requires_working_threading()
+ def test_context_thread_inherit(self):
+ import threading
+
+ cvar = contextvars.ContextVar('cvar')
+
+ # By default, the context of the caller is inheritied
+ def run_inherit():
+ self.assertEqual(cvar.get(), 1)
+
+ cvar.set(1)
+ thread = threading.Thread(target=run_inherit)
+ thread.start()
+ thread.join()
+
+ # If context=None is passed, behaviour is to inherit
+ thread = threading.Thread(target=run_inherit, context=None)
+ thread.start()
+ thread.join()
+
+ # An explicit Context value can also be passed
+ custom_ctx = contextvars.Context()
+ custom_var = None
+
+ def setup_context():
+ nonlocal custom_var
+ custom_var = contextvars.ContextVar('custom')
+ custom_var.set(2)
+
+ custom_ctx.run(setup_context)
+
+ def run_custom():
+ self.assertEqual(custom_var.get(), 2)
+
+ thread = threading.Thread(target=run_custom, context=custom_ctx)
+ thread.start()
+ thread.join()
+
+ # You can also pass a new Context() object to start with an empty context
+ def run_empty():
+ with self.assertRaises(LookupError):
+ cvar.get()
+
+ thread = threading.Thread(target=run_empty, context=contextvars.Context())
+ thread.start()
+ thread.join()
+
# HAMT Tests
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index bc6c6427740949..2e38bbfd4b93ea 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -44,6 +44,7 @@
import random
import inspect
import threading
+import contextvars
if sys.platform == 'darwin':
@@ -1725,8 +1726,10 @@ def test_threading(self):
self.finish1 = threading.Event()
self.finish2 = threading.Event()
- th1 = threading.Thread(target=thfunc1, args=(self,))
- th2 = threading.Thread(target=thfunc2, args=(self,))
+ th1 = threading.Thread(target=thfunc1, args=(self,),
+ context=contextvars.Context())
+ th2 = threading.Thread(target=thfunc2, args=(self,),
+ context=contextvars.Context())
th1.start()
th2.start()
diff --git a/Lib/threading.py b/Lib/threading.py
index 78e591124278fc..b9c7492fdbadef 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -4,6 +4,8 @@
import sys as _sys
import _thread
import warnings
+import contextvars as _contextvars
+
from time import monotonic as _time
from _weakrefset import WeakSet
@@ -871,7 +873,7 @@ class Thread:
_initialized = False
def __init__(self, group=None, target=None, name=None,
- args=(), kwargs=None, *, daemon=None):
+ args=(), kwargs=None, *, daemon=None, context=None):
"""This constructor should always be called with keyword arguments. Arguments are:
*group* should be None; reserved for future extension when a ThreadGroup
@@ -888,6 +890,11 @@ class is implemented.
*kwargs* is a dictionary of keyword arguments for the target
invocation. Defaults to {}.
+ *context* is the contextvars.Context value to use for the thread.
+ The default value is None, which means to use a copy of the context
+ of the caller. To start with an empty context, pass a new instance
+ of contextvars.Context().
+
If a subclass overrides the constructor, it must make sure to invoke
the base class constructor (Thread.__init__()) before doing anything
else to the thread.
@@ -917,6 +924,7 @@ class is implemented.
self._daemonic = daemon
else:
self._daemonic = current_thread().daemon
+ self._context = context
self._ident = None
if _HAVE_THREAD_NATIVE_ID:
self._native_id = None
@@ -972,9 +980,15 @@ def start(self):
with _active_limbo_lock:
_limbo[self] = self
+
+ if self._context is None:
+ # No context provided, inherit a copy of the context of the caller.
+ self._context = _contextvars.copy_context()
+
try:
# Start joinable thread
- _start_joinable_thread(self._bootstrap, handle=self._handle,
+ _start_joinable_thread(self._bootstrap,
+ handle=self._handle,
daemon=self.daemon)
except Exception:
with _active_limbo_lock:
@@ -1051,7 +1065,13 @@ def _bootstrap_inner(self):
_sys.setprofile(_profile_hook)
try:
- self.run()
+ if self._context is None:
+ # Run with empty context, matching behaviour of
+ # threading.local and older versions of Python.
+ self.run()
+ else:
+ # Run with the provided or the inherited context.
+ self._context.run(self.run)
except:
self._invoke_excepthook(self)
finally:
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 67acf0fc520087..18484a42abb6e8 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -420,6 +420,7 @@ PARSER_HEADERS= \
# Python
PYTHON_OBJS= \
+ Python/_contextvars.o \
Python/_warnings.o \
Python/Python-ast.o \
Python/Python-tokenize.o \
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst
new file mode 100644
index 00000000000000..f9d00cfbc9bcf6
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-01-06-10-55-41.gh-issue-128555.tAK_AY.rst
@@ -0,0 +1,4 @@
+Starting a new thread using :class:`threading.Thread` will now, by default,
+use a copy of the :class:`contextvars.Context` from the caller of
+:meth:`threading.Thread.start` rather than using an empty context. The
+``_contextvars`` module is now built-in.
diff --git a/Modules/Setup b/Modules/Setup
index ddf39e0b966610..e01c7bb1a8a45e 100644
--- a/Modules/Setup
+++ b/Modules/Setup
@@ -132,7 +132,6 @@ PYTHONPATH=$(COREPYTHONPATH)
#_asyncio _asynciomodule.c
#_bisect _bisectmodule.c
-#_contextvars _contextvarsmodule.c
#_csv _csv.c
#_datetime _datetimemodule.c
#_decimal _decimal/_decimal.c
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index 52c0f883d383db..189c30c558336e 100644
--- a/Modules/Setup.stdlib.in
+++ b/Modules/Setup.stdlib.in
@@ -31,7 +31,6 @@
@MODULE_ARRAY_TRUE@array arraymodule.c
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
-@MODULE__CONTEXTVARS_TRUE@_contextvars _contextvarsmodule.c
@MODULE__CSV_TRUE@_csv _csv.c
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
@MODULE__JSON_TRUE@_json _json.c
diff --git a/Modules/config.c.in b/Modules/config.c.in
index 53b4fb285498d0..41e42228c1ffd1 100644
--- a/Modules/config.c.in
+++ b/Modules/config.c.in
@@ -29,6 +29,7 @@ extern PyObject* PyInit__imp(void);
extern PyObject* PyInit_gc(void);
extern PyObject* PyInit__ast(void);
extern PyObject* PyInit__tokenize(void);
+extern PyObject* PyInit__contextvars(void);
extern PyObject* _PyWarnings_Init(void);
extern PyObject* PyInit__string(void);
@@ -55,6 +56,9 @@ struct _inittab _PyImport_Inittab[] = {
/* This lives in gcmodule.c */
{"gc", PyInit_gc},
+ /* This lives in Python/_contextvars.c */
+ {"_contextvars", PyInit__contextvars},
+
/* This lives in _warnings.c */
{"_warnings", _PyWarnings_Init},
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 9ebf58ae8a9bc4..ef6dbf9f8e4222 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -423,7 +423,6 @@
-
@@ -570,6 +569,7 @@
+
diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters
index 6c76a6ab592a84..b661aad2019454 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -1262,6 +1262,9 @@
PC
+
+ Python
+
Python
@@ -1526,9 +1529,6 @@
Objects
-
- Modules
-
Modules\zlib
diff --git a/Modules/_contextvarsmodule.c b/Python/_contextvars.c
similarity index 97%
rename from Modules/_contextvarsmodule.c
rename to Python/_contextvars.c
index 3f96f07909b69a..0f8b8004c1af22 100644
--- a/Modules/_contextvarsmodule.c
+++ b/Python/_contextvars.c
@@ -1,6 +1,6 @@
#include "Python.h"
-#include "clinic/_contextvarsmodule.c.h"
+#include "clinic/_contextvars.c.h"
/*[clinic input]
module _contextvars
diff --git a/Modules/clinic/_contextvarsmodule.c.h b/Python/clinic/_contextvars.c.h
similarity index 100%
rename from Modules/clinic/_contextvarsmodule.c.h
rename to Python/clinic/_contextvars.c.h
diff --git a/configure.ac b/configure.ac
index badb19d55895de..3717cb44314609 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7709,7 +7709,6 @@ dnl always enabled extension modules
PY_STDLIB_MOD_SIMPLE([array])
PY_STDLIB_MOD_SIMPLE([_asyncio])
PY_STDLIB_MOD_SIMPLE([_bisect])
-PY_STDLIB_MOD_SIMPLE([_contextvars])
PY_STDLIB_MOD_SIMPLE([_csv])
PY_STDLIB_MOD_SIMPLE([_heapq])
PY_STDLIB_MOD_SIMPLE([_json])