diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1724beb8ce69517..1c5ab4ef73da2fb 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -1949,6 +1949,17 @@ def filter(*args): def test_set_bad_filter(self): self.assertRaises(ValueError, _testcapi.PyImport_SetLazyImportsFilter, 42) + def test_dunder_lazy_import_without_frame(self): + # gh-151510: __lazy_import__() called with no globals and no running + # Python frame must raise TypeError instead of crashing. + with self.assertRaisesRegex( + TypeError, + r"__lazy_import__\(\) missing globals when called without a frame", + ): + _testcapi.lazy_import_without_frame( + "test.test_lazy_import.data.basic2" + ) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst new file mode 100644 index 000000000000000..cfa5ee8d3839c1b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-16-00-45-42.gh-issue-151510.HJ-kGn.rst @@ -0,0 +1,2 @@ +Fix a crash in :func:`!__lazy_import__` when called without an explicit +``globals`` argument and without a current Python frame. diff --git a/Modules/_testcapi/import.c b/Modules/_testcapi/import.c index 384a8f52da4b984..11d0e6acaebe1f2 100644 --- a/Modules/_testcapi/import.c +++ b/Modules/_testcapi/import.c @@ -1,6 +1,27 @@ #include "parts.h" #include "util.h" +static PyObject * +pyimport_lazyimportwithoutframe(PyObject *self, PyObject *name) +{ + PyObject *lazy_import = PyImport_ImportModuleAttrString("builtins", + "__lazy_import__"); + if (lazy_import == NULL) { + return NULL; + } + + // Simulate being called with no running Python frame (e.g. from a freshly + // attached C thread), so that PyEval_GetGlobals() returns NULL. + PyThreadState *tstate = PyThreadState_Get(); + struct _PyInterpreterFrame *saved = tstate->current_frame; + tstate->current_frame = NULL; + PyObject *res = PyObject_CallOneArg(lazy_import, name); + tstate->current_frame = saved; + + Py_DECREF(lazy_import); + return res; +} + // Test PyImport_ImportModuleAttr() static PyObject * pyimport_importmoduleattr(PyObject *self, PyObject *args) @@ -95,6 +116,7 @@ static PyMethodDef test_methods[] = { {"PyImport_GetLazyImportsMode", pyimport_getlazyimportsmode, METH_NOARGS}, {"PyImport_SetLazyImportsFilter", pyimport_setlazyimportsfilter, METH_VARARGS}, {"PyImport_GetLazyImportsFilter", pyimport_getlazyimportsfilter, METH_NOARGS}, + {"lazy_import_without_frame", pyimport_lazyimportwithoutframe, METH_O}, {NULL}, }; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index d5129bf6a5a6bc0..fa64255be00e75d 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -313,6 +313,12 @@ builtin___lazy_import___impl(PyObject *module, PyObject *name, PyThreadState *tstate = PyThreadState_GET(); if (globals == NULL) { globals = PyEval_GetGlobals(); + if (globals == NULL) { + PyErr_SetString(PyExc_TypeError, + "__lazy_import__() missing globals " + "when called without a frame"); + return NULL; + } } if (locals == NULL) { locals = globals;