When a module import fails, other threads can receive a stale module reference that is no longer in sys.modules.
- Thread 1 starts importing module X, adds it to
sys.modules - Thread 2 checks
sys.modules, finds module X, takes the "skip lock" fast path - Thread 1's import fails, removes module X from
sys.modules - Thread 2 returns the stale module reference (not in
sys.modules)
Affected code paths
Lib/importlib/_bootstrap.py:_find_and_load()lines 1268-1282Python/import.c:PyImport_GetModule()Python/import.c:PyImport_ImportModuleLevelObject()
All three have an optimization that skips locking when a module appears already loaded, but none verify the module is still valid after checking _initializing.
Reproducer
import sys import threading import tempfile import os with tempfile.TemporaryDirectory() as tmpdir: sys.path.insert(0, tmpdir) # Module that partially initializes then fails with open(os.path.join(tmpdir, "failing_mod.py"), "w") as f: f.write("import time; time.sleep(0.05); raise RuntimeError('fail')") results = [] def do_import(delay): import time; time.sleep(delay) try: mod = __import__("failing_mod") if "failing_mod" not in sys.modules: results.append("RACE: got module not in sys.modules") except RuntimeError: results.append("ok") t1 = threading.Thread(target=do_import, args=(0,)) t2 = threading.Thread(target=do_import, args=(0.01,)) t1.start(); t2.start(); t1.join(); t2.join() print(results) # May show "RACE" on affected builds