From 1d1685f570804656351ea9511962654db8277b54 Mon Sep 17 00:00:00 2001 From: Sohel2309 Date: Sun, 6 Sep 2026 20:36:54 +0530 Subject: [PATCH 1/3] Fix threading local fallback import --- Lib/_threading_local.py | 8 ++++---- Lib/test/test_threading.py | 21 +++++++++++++++++++++ Lib/threading.py | 23 ++++++++++++++++++----- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Lib/_threading_local.py b/Lib/_threading_local.py index 2af3885458b54f..4b592b0a3030b8 100644 --- a/Lib/_threading_local.py +++ b/Lib/_threading_local.py @@ -36,14 +36,14 @@ def __init__(self): def get_dict(self): """Return the dict for the current thread. Raises KeyError if none defined.""" - thread = current_thread() + thread = threading.current_thread() return self.dicts[id(thread)][1] def create_dict(self): """Create a new dict for the current thread, and return it.""" localdict = {} key = self.key - thread = current_thread() + thread = threading.current_thread() idt = id(thread) def local_deleted(_, key=key): # When the localimpl is deleted, remove the thread attribute. @@ -88,7 +88,7 @@ def __new__(cls, /, *args, **kw): self = object.__new__(cls) impl = _localimpl() impl.localargs = (args, kw) - impl.locallock = RLock() + impl.locallock = threading.RLock() object.__setattr__(self, '_local__impl', impl) # We need to create the thread dict in anticipation of # __init__ being called, to make sure we don't call it @@ -117,4 +117,4 @@ def __delattr__(self, name): return object.__delattr__(self, name) -from threading import current_thread, RLock +import threading diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 96b43936be92cd..8d09d2481aa08f 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -2608,6 +2608,27 @@ def test_concurrent_tee_negative_n(self): class MiscTestCase(unittest.TestCase): + def test_threading_local_fallback_import(self): + # gh-156341: threading must still be importable when the platform's + # `_thread` module doesn't provide the `_local` type, forcing the + # pure Python fallback implementation in `_threading_local`. This + # used to raise ImportError due to a circular import between the + # two modules. + rc, out, err = assert_python_ok("-c", """if 1: + import _thread + del _thread._local + import threading + import _threading_local + # Sanity check: the pure Python fallback is actually in use, + # and it still behaves like a normal thread-local object. + assert threading.local is _threading_local.local + tlocal = threading.local() + tlocal.x = 1 + assert tlocal.x == 1 + print("ok") + """) + self.assertEqual(out.strip(), b"ok") + def test__all__(self): restore_default_excepthook(self) diff --git a/Lib/threading.py b/Lib/threading.py index abac31e25886fa..1a3e1d0a0bdb9f 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1537,11 +1537,6 @@ def __init__(self): _active[self._ident] = self -# Helper thread-local instance to detect when a _DummyThread -# is collected. Not a part of the public API. -_thread_local_info = local() - - class _DeleteDummyThreadOnDel: ''' Helper class to remove a dummy thread from threading._active on __del__. @@ -1692,6 +1687,24 @@ def _register_atexit(func, *arg, **kwargs): _main_thread = _MainThread() +# Helper thread-local instance to detect when a _DummyThread is collected. +# Not a part of the public API. +# +# This must be created only after _main_thread has been constructed and +# registered in `_active` above (rather than immediately after the +# _MainThread class, where it conceptually belongs). Constructing a +# local() instance immediately populates the thread-local dict for the +# creating thread, which calls current_thread(). When the platform's +# `_thread` module doesn't provide `_local` (see the `local` import near +# the top of this file), that population is done by the pure Python +# fallback implementation in `_threading_local`. Creating this any earlier +# either reintroduces the circular import described in gh-156341 (if +# current_thread isn't defined yet), or -- if current_thread is defined +# but the main thread hasn't been registered in `_active` yet -- causes +# current_thread() to construct a _DummyThread, whose __init__ itself +# needs _thread_local_info to already exist. +_thread_local_info = local() + def _shutdown(): """ Wait until the Python thread state of all non-daemon threads get deleted. From 788da2d7d79be8676726901da87411dfa49aa099 Mon Sep 17 00:00:00 2001 From: Sohel2309 Date: Sun, 6 Sep 2026 20:44:01 +0530 Subject: [PATCH 2/3] Add NEWS entry for threading local fallback --- .../2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst b/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst new file mode 100644 index 00000000000000..bb5ff40488994e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst @@ -0,0 +1,2 @@ +Fix fallback importing of the :mod:`_threading_local` module when the +standard library `threading` module has not yet been fully initialized. From 3c449adea61e054cc0ca82dedf79b8376584f6cc Mon Sep 17 00:00:00 2001 From: Sohel2309 Date: Tue, 8 Sep 2026 09:09:24 +0530 Subject: [PATCH 3/3] Fix NEWS entry for threading local fallback --- .../2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst | 2 -- .../2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst b/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst deleted file mode 100644 index bb5ff40488994e..00000000000000 --- a/Misc/NEWS.d/next/Core and Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix fallback importing of the :mod:`_threading_local` module when the -standard library `threading` module has not yet been fully initialized. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst new file mode 100644 index 00000000000000..493e6af2e9f1c1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-06-18-00-00.gh-issue-156341.Sl7kQ2.rst @@ -0,0 +1,2 @@ +Fix fallback importing of ``_threading_local`` when the standard +library ``threading`` module has not yet been fully initialized.