Currently, context variables and warnings behave differently when used with threads on the free-threaded build and the GIL-enabled build: on the free-threaded build, a new thread inherits the creating thread’s context by default, while on the GIL-enabled build it starts with an empty context. This is linked with warnings because of the warnings.catch_warnings state.
This is controlled via the thread_inherit_context and context_aware_warnings Python startup options, which default to on in free-threaded builds and off in GIL-enabled builds. See the startup option docs and the Python 3.14 release notes for more detail about this.
To make what thread_inherit_context does concrete, consider the following example using numpy.printoptions, which stores state in a context variable. The printoptions context manager gives users control over how NumPy prints arrays.
$ python3.14
>>> import numpy, threading
>>> def print_printoptions():
... print(numpy.get_printoptions()['precision'])
>>> with numpy.printoptions(precision=2):
... threading.Thread(target=print_printoptions).start()
8
The spawned thread sees the default 'precision' print option, 8, instead of the value specified in the with statement: 2.
With Python 3.14 and thread_inherit_context enabled, the spawned thread sees the state that was active where it was started, as the structure of the code suggests:
$ python3.14 -X thread_inherit_context=1
>>> import numpy, threading
>>> def print_printoptions():
... print(numpy.get_printoptions()['precision'])
>>> with numpy.printoptions(precision=2):
... threading.Thread(target=print_printoptions).start()
2
The current default on the GIL-enabled build is documented but I think it’s a surprising default, and everywhere else we’ve already decided that propagation is the right behavior: asyncio tasks copy the creating context, asyncio.to_thread() explicitly propagates it, and the free-threaded build has thread_inheric_context on by default. Plain threading.Thread is the odd one out.
I want to create a future where thread_inherit_context is turned on by default on both builds.
I spoke with @yselivanov, @thomas, and @hugovk at the CPython sprints on July 18th. They agreed the current behavior is surprising, and liked the idea of introducing the warning in Python 3.15 if we can get it done before rc1 on August 4th. Otherwise we can do it for 3.16. If the warning lands in 3.15, my rough hope would be to flip the defaults in 3.16; if the warning slips to 3.16, then 3.17.
Yury had an idea for implementing the warning without making every Thread(...) call noisy. When a thread is created without the flag being explicitly set (as opposed to explicitly disabled by the user), we’d capture a snapshot of which context variables were set in the creating thread, without making that context active in the new thread. If the new thread later reads a context variable and falls back to its default even though the variable was set in the enclosing scope when the thread was spawned, Python would issue a warning that the value was not inherited from the creating thread. Code that never reads context variables in threads and code that reads the same value as the value defined in the enclosing scope when the tread was spawned would never see the warning.
If a user explicitly sets thread_inherit_context to 0 there would also be no warning, as that indicates an explicit user request to use the current behavior. Code that wants a clean context for a particular thread can opt out explicitly by passing context=contextvars.Context() to threading.Thread.
The context_aware_warnings option makes warnings.catch_warnings() store its state in a context variable instead of mutating process-global state. IMO it should also be turned on by default on both builds: combined with thread_inherit_context, a catch_warnings block around thread creation would finally apply to warnings raised inside that thread, closing a long-standing gap in the warnings module.
One subtlety: with context_aware_warnings enabled, emitting a warning itself reads a context variable to find the active filters, so the check will need to exempt the warnings machinery’s own context variable and guard against re-entrancy.
I’m posting here to raise awareness and to start discussion. If you think such a warning would be unavoidably noisy, or you see other downsides, I’d appreciate hearing about it. Maybe @nas has other ideas — I know he thought about this quite a bit for Python 3.14 last year.