diff --git a/Lib/_threading_local.py b/Lib/_threading_local.py index 2af3885458b54f2..4b592b0a3030b8d 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 96b43936be92cda..8d09d2481aa08f6 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 abac31e25886fae..1a3e1d0a0bdb9ff 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. 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 000000000000000..493e6af2e9f1c1c --- /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.