From 9440f8f8662994e4435d18e3b0d40a126859db4c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 18 Feb 2026 21:45:52 +0100 Subject: [PATCH] gh-145055: Accept frozendict for globals in exec() and eval() --- Doc/library/functions.rst | 18 +++++++++++++++++- Include/internal/pycore_dict.h | 4 ++-- Lib/test/test_builtin.py | 18 ++++++++++++++++++ ...6-02-21-12-16-46.gh-issue-145055.VyT-zI.rst | 2 ++ Objects/codeobject.c | 2 +- Objects/dictobject.c | 2 +- Objects/funcobject.c | 2 +- Python/_warnings.c | 4 ++-- Python/bltinmodule.c | 14 ++++++++------ Python/ceval.c | 4 ++-- Python/import.c | 2 +- Python/pythonrun.c | 2 +- 12 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-12-16-46.gh-issue-145055.VyT-zI.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 65b8ffdb23111d..9e66fe9d4697b0 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -594,7 +594,7 @@ are always available. They are listed here in alphabetical order. :param globals: The global namespace (default: ``None``). - :type globals: :class:`dict` | ``None`` + :type globals: :class:`dict` | :class:`frozendict` | ``None`` :param locals: The local namespace (default: ``None``). @@ -643,6 +643,10 @@ are always available. They are listed here in alphabetical order. If the given source is a string, then leading and trailing spaces and tabs are stripped. + It's possible to pass :class:`frozendict` to *globals* if the + :class:`!frozendict` has a ``__builtins__`` item. In this case, it's not + possible to assign or reassign global variables. + See :func:`ast.literal_eval` for a function that can safely evaluate strings with expressions containing only literals. @@ -660,6 +664,10 @@ are always available. They are listed here in alphabetical order. The semantics of the default *locals* namespace have been adjusted as described for the :func:`locals` builtin. + .. versionchanged:: next + + *globals* can now be a :class:`frozendict`. + .. index:: pair: built-in function; exec .. function:: exec(source, /, globals=None, locals=None, *, closure=None) @@ -688,6 +696,10 @@ are always available. They are listed here in alphabetical order. respectively. If provided, *locals* can be any mapping object. Remember that at the module level, globals and locals are the same dictionary. + It's possible to pass :class:`frozendict` to *globals* if the + :class:`!frozendict` has a ``__builtins__`` item. In this case, it's not + possible to assign or reassign global variables. + .. note:: When ``exec`` gets two separate objects as *globals* and *locals*, the @@ -737,6 +749,10 @@ are always available. They are listed here in alphabetical order. The semantics of the default *locals* namespace have been adjusted as described for the :func:`locals` builtin. + .. versionchanged:: next + + *globals* can now be a :class:`frozendict`. + .. function:: filter(function, iterable, /) diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 59e88be6aeec12..08166b10468367 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -370,7 +370,7 @@ _PyDict_UniqueId(PyDictObject *mp) static inline void _Py_INCREF_DICT(PyObject *op) { - assert(PyDict_Check(op)); + assert(PyAnyDict_Check(op)); Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op); _Py_THREAD_INCREF_OBJECT(op, id); } @@ -378,7 +378,7 @@ _Py_INCREF_DICT(PyObject *op) static inline void _Py_DECREF_DICT(PyObject *op) { - assert(PyDict_Check(op)); + assert(PyAnyDict_Check(op)); Py_ssize_t id = _PyDict_UniqueId((PyDictObject *)op); _Py_THREAD_DECREF_OBJECT(op, id); } diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 7b69374b1868d1..ce47431dfd6b5d 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -784,6 +784,12 @@ def __getitem__(self, key): raise ValueError self.assertRaises(ValueError, eval, "foo", {}, X()) + # Pass frozenset to globals + ns = frozendict(x=1, data=[], __builtins__=__builtins__) + code = "data.append(x)" + eval(code, ns, ns) + self.assertEqual(ns['data'], [1]) + def test_eval_kwargs(self): data = {"A_GLOBAL_VALUE": 456} self.assertEqual(eval("globals()['A_GLOBAL_VALUE']", globals=data), 456) @@ -882,6 +888,18 @@ def test_exec(self): del l['__builtins__'] self.assertEqual((g, l), ({'a': 1}, {'b': 2})) + # Pass frozenset to globals + ns = frozendict(x=1, data=[], __builtins__=__builtins__) + code = "data.append(x)" + exec(code, ns, ns) + self.assertEqual(ns['data'], [1]) + + ns = frozendict(__builtins__=__builtins__) + code = "x = 1" + errmsg = "'frozendict' object does not support item assignment" + with self.assertRaisesRegex(TypeError, errmsg): + exec(code, ns, ns) + def test_exec_kwargs(self): g = {} exec('global z\nz = 1', globals=g) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-12-16-46.gh-issue-145055.VyT-zI.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-12-16-46.gh-issue-145055.VyT-zI.rst new file mode 100644 index 00000000000000..c9daaa27717ed0 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-12-16-46.gh-issue-145055.VyT-zI.rst @@ -0,0 +1,2 @@ +:func:`exec` and :func:`eval` now accept :class:`frozendict` for *globals*. +Patch by Victor Stinner. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 520190824fbf1a..d26516f7c2ff66 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -1830,7 +1830,7 @@ identify_unbound_names(PyThreadState *tstate, PyCodeObject *co, assert(attrnames != NULL); assert(PySet_Check(attrnames)); assert(PySet_GET_SIZE(attrnames) == 0 || counts != NULL); - assert(globalsns == NULL || PyDict_Check(globalsns)); + assert(globalsns == NULL || PyAnyDict_Check(globalsns)); assert(builtinsns == NULL || PyDict_Check(builtinsns)); assert(counts == NULL || counts->total == 0); struct co_unbound_counts unbound = {0}; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 8f960352fa4824..808afdf48dd492 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2661,7 +2661,7 @@ _PyDict_LoadGlobalStackRef(PyDictObject *globals, PyDictObject *builtins, PyObje PyObject * _PyDict_LoadBuiltinsFromGlobals(PyObject *globals) { - if (!PyDict_Check(globals)) { + if (!PyAnyDict_Check(globals)) { PyErr_BadInternalCall(); return NULL; } diff --git a/Objects/funcobject.c b/Objects/funcobject.c index efe27a2b70c4de..fc32826fb3a861 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -150,7 +150,7 @@ PyObject * PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname) { assert(globals != NULL); - assert(PyDict_Check(globals)); + assert(PyAnyDict_Check(globals)); _Py_INCREF_DICT(globals); PyCodeObject *code_obj = (PyCodeObject *)code; diff --git a/Python/_warnings.c b/Python/_warnings.c index d44d414bc93a04..424a6984087b87 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -1045,7 +1045,7 @@ setup_context(Py_ssize_t stack_level, /* Setup registry. */ assert(globals != NULL); - assert(PyDict_Check(globals)); + assert(PyAnyDict_Check(globals)); int rc = PyDict_GetItemRef(globals, &_Py_ID(__warningregistry__), registry); if (rc < 0) { @@ -1269,7 +1269,7 @@ warnings_warn_explicit_impl(PyObject *module, PyObject *message, } if (module_globals && module_globals != Py_None) { - if (!PyDict_Check(module_globals)) { + if (!PyAnyDict_Check(module_globals)) { PyErr_Format(PyExc_TypeError, "module_globals must be a dict, not '%.200s'", Py_TYPE(module_globals)->tp_name); diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 493a6e0413d8eb..d26357e6c1b44b 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -1034,10 +1034,11 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, PyErr_SetString(PyExc_TypeError, "locals must be a mapping"); return NULL; } - if (globals != Py_None && !PyDict_Check(globals)) { + if (globals != Py_None && !PyAnyDict_Check(globals)) { PyErr_SetString(PyExc_TypeError, PyMapping_Check(globals) ? - "globals must be a real dict; try eval(expr, {}, mapping)" - : "globals must be a dict"); + "globals must be a real dict or a frozendict; " + "try eval(expr, {}, mapping)" + : "globals must be a dict or a frozendict"); return NULL; } @@ -1191,9 +1192,10 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, locals = Py_NewRef(globals); } - if (!PyDict_Check(globals)) { - PyErr_Format(PyExc_TypeError, "exec() globals must be a dict, not %.100s", - Py_TYPE(globals)->tp_name); + if (!PyAnyDict_Check(globals)) { + PyErr_Format(PyExc_TypeError, + "exec() globals must be a dict or a frozendict, not %T", + globals); goto error; } if (!PyMapping_Check(locals)) { diff --git a/Python/ceval.c b/Python/ceval.c index 2cd7c7bfd28d09..aa2c9701283508 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2717,7 +2717,7 @@ static PyObject * get_globals_builtins(PyObject *globals) { PyObject *builtins = NULL; - if (PyDict_Check(globals)) { + if (PyAnyDict_Check(globals)) { if (PyDict_GetItemRef(globals, &_Py_ID(__builtins__), &builtins) < 0) { return NULL; } @@ -3572,7 +3572,7 @@ _PyEval_GetANext(PyObject *aiter) void _PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name, _PyStackRef *writeto) { - if (PyDict_CheckExact(globals) && PyDict_CheckExact(builtins)) { + if (PyAnyDict_CheckExact(globals) && PyAnyDict_CheckExact(builtins)) { _PyDict_LoadGlobalStackRef((PyDictObject *)globals, (PyDictObject *)builtins, name, writeto); diff --git a/Python/import.c b/Python/import.c index c20c55727d2f94..85529906918027 100644 --- a/Python/import.c +++ b/Python/import.c @@ -3728,7 +3728,7 @@ resolve_name(PyThreadState *tstate, PyObject *name, PyObject *globals, int level _PyErr_SetString(tstate, PyExc_KeyError, "'__name__' not in globals"); goto error; } - if (!PyDict_Check(globals)) { + if (!PyAnyDict_Check(globals)) { _PyErr_SetString(tstate, PyExc_TypeError, "globals must be a dict"); goto error; } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index ec8c2d12ab27fc..3b2c0136fba52d 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1347,7 +1347,7 @@ static PyObject * run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, PyObject *locals) { /* Set globals['__builtins__'] if it doesn't exist */ - if (!globals || !PyDict_Check(globals)) { + if (!globals || !PyAnyDict_Check(globals)) { PyErr_SetString(PyExc_SystemError, "globals must be a real dict"); return NULL; }