diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index e0c5552fe11c53..53d5cbaf17e6b4 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -2807,5 +2807,60 @@ def test_fails_without_lazy(self): self.assertIn("ImportError", result.stderr) +@support.requires_subprocess() +class LazyImportFinalizationTests(unittest.TestCase): + """Destructors that use lazy imports after finalization freed their state.""" + + def test_declare(self): + code = textwrap.dedent(""" + import builtins, os + + class Canary: + def __del__(self, write=os.write, exec=exec, + builtins={"__lazy_import__": builtins.__lazy_import__}): + exec("lazy import a.b", {"__builtins__": builtins}) + write(1, b"ok") + + # Only this placeholder keeps the canary alive. + exec("lazy import pkg.mod", {"__builtins__": { + "__lazy_import__": builtins.__lazy_import__, "c": Canary()}}) + """) + self.assertEqual(assert_python_ok("-c", code).out, b"ok") + + def test_resolve(self): + code = textwrap.dedent(""" + import contextvars, json, os, types + + def safe_import(name, *args, allowed={"json": json}): + return allowed[name] + + # Not in sys.modules, with builtins that still work at shutdown. + ns = types.ModuleType("ns") + ns.json = __lazy_import__("json", {"__builtins__": {"__import__": safe_import}}) + + class Canary: + def __del__(self, write=os.write, type=type, ns=ns): + write(1, type(ns.json).__name__.encode()) + + contextvars.ContextVar("v").set(Canary()) + """) + self.assertEqual(assert_python_ok("-c", code).out, b"module") + + def test_set_lazy_attributes(self): + code = textwrap.dedent(""" + import _imp, os, sys + + class Canary: + def __del__(self, write=os.write, + set_lazy=_imp._set_lazy_attributes): + set_lazy(None, "mod") + write(1, b"ok") + + # Freed while sys.lazy_modules is being cleared. + sys.lazy_modules.add(Canary()) + """) + self.assertEqual(assert_python_ok("-c", code).out, b"ok") + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst new file mode 100644 index 00000000000000..9eb11205e452fa --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-05-01-59-22.gh-issue-158820.3KLyOT.rst @@ -0,0 +1,2 @@ +Fix a crash when a :ref:`lazy import ` is declared or resolved +by a finalizer running during interpreter shutdown. diff --git a/Python/import.c b/Python/import.c index 4d3e74f45e5e36..7538575fbf7520 100644 --- a/Python/import.c +++ b/Python/import.c @@ -287,6 +287,9 @@ _PyImport_ClearLazyModules(PyInterpreterState *interp) int _PyImport_DiscardLazyModule(PyInterpreterState *interp, PyObject *name) { + if (LAZY_MODULES(interp) == NULL) { + return 0; + } return PySet_Discard(LAZY_MODULES(interp), name); } @@ -4179,7 +4182,10 @@ lazy_modules_add(PyThreadState *tstate, PyObject *name, else { Py_XDECREF(mod); } - return loaded ? 0 : PySet_Add(LAZY_MODULES(tstate->interp), name); + if (loaded || LAZY_MODULES(tstate->interp) == NULL) { + return 0; + } + return PySet_Add(LAZY_MODULES(tstate->interp), name); } // Ensure a dict of pending submodule names exists for the parent. @@ -4213,7 +4219,10 @@ register_lazy_on_parent(PyThreadState *tstate, PyObject *name, PyObject *source) { PyDictObject *pending = (PyDictObject *)LAZY_PENDING_SUBMODULES(tstate->interp); - assert(pending != NULL); + // Finalizers can still run after finalize_modules() cleared the dict. + if (pending == NULL) { + return 0; + } Py_ssize_t end = PyUnicode_GET_LENGTH(name); while (true) { Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, -1); @@ -5575,7 +5584,7 @@ _imp__set_lazy_attributes_impl(PyObject *module, PyObject *modobj, /*[clinic end generated code: output=3369bb3242b1f043 input=900339e013ab2b82]*/ { PyInterpreterState *interp = _PyInterpreterState_GET(); - if (PySet_Discard(LAZY_MODULES(interp), name) < 0) { + if (_PyImport_DiscardLazyModule(interp, name) < 0) { return NULL; } if (_PyImport_ClearLazySubmodule(_PyThreadState_GET(), name, 0) < 0) {