-
-
Notifications
You must be signed in to change notification settings - Fork 31.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
exec
and eval
incorrectly accepting subclass of dict
as globals
#131811
Comments
I don't think we should change the behavior. I agree the documentation might suggest that passing a subclass of |
Maybe we should make the documentation clearer on that point? |
Updating documentation (and removing or improving I personally favor updating /* Current code */
if (!PyDict_Check(globals)) {
PyErr_Format(PyExc_TypeError, "exec() globals must be a dict, not %.100s",
Py_TYPE(globals)->tp_name);
goto error;
}
/* Added code */
if (!PyDict_CheckExact(globals)) {
if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1,
"globals should be an exact dict, not %.200s. "
"The ability to pass a subclass of dict is deprecated, "
"and may be removed in a future version of Python.",
Py_TYPE(globals)->tp_name)) {
goto error;
}
} Edit: See this commit |
If we decided to change the behavior, a deprecation phase would probably be required. If we kept the behavior, I would keep the "not-fully-legit" tests to remind us of this in the future (I vaguely remember that I found this issue before, but can't find the PR). I'm leaning towards keeping things as it is but I'm not super against deprecating it. This is a builtin function that is widely used, we probably need inputs from a few other core devs. |
I tried to search for similar issues before, but failed. I have now found this previous dicussion: This also seems relevant: |
cc @rhettinger |
If I'm not misread docs, strict type checking should be enforced iff only |
Research notes: The global argument type check goes back to before 1997 (I stopped looking at 79f25d9). The dict subtype check goes back to at least 2001 (I stopped looking at cbfc855). The eval/exec type check code wasn't updated when builtin types became subclassable in Python 2.2. In 2011, I was the first to report that the C-API would bypass methods defined in subclasses of builtin types. This current report is the first to focus on There was a surprised user in 2017. But after the resulting doc edit, 059b9ea, the eval/exec issue doesn't appear to have been a problem in practice. Likely this is either because testing promptly reveals that dict subclass methods have been bypassed or because users have read the docs which tell them specifically, "it must be a dictionary (and not a subclass of dictionary)" Related PRs but not a direct cause: In 2006 7cae87c , Recommendation: I don't have a strong opinion on what if anything should be done. But I'm reluctant to make a breaking change by raising an exception — we're here to fix bugs that people have rather than to cause bugs in their long stable working code. The current docs are helpful and only wrong in using the word "must" instead of "should". The docs do not cover the specific case what happens if you don't follow the rules, leaving this as an undefined behavior. We could elaborate, "it must be a dictionary (and not a subclass of dictionary because subclass methods are bypassed)". |
Bug report
Bug description:
According to Python docs,
exec
andeval
take as theirglobals
argument an exactdict
, and not a subclass of it.However,
builtin_exec_impl
allows subclasses ofdict
because it checks the argument withPyDict_Check
instead ofPyDict_CheckExact
(see here).builtin_eval_impl
makes the same, wrong, validation, with a slightly different error message. (see here).This is a problem because it allows developers to pass a dictionary with a custom
__getitem__
method which will never actually get called. It will not get called because when opcode LOAD_NAME is executed, it assumesglobals
is adict
, and accesses it usingPyDict_GetItemRef
instead ofPyMapping_GetOptionalItem
(see here).Furthermore, opcode LOAD_GLOBAL behaves slightly different, first checking if
globals
is an exactdict
and then choosing how to access it.Here is an example of the inconsistent and unexpected behavior:
There even is a test (
test_builtin.py
, see here) that says "customglobals
orbuiltins
can raise errors on item access". The test passes only because alocals
is not provided toexec
, soexec
copies it fromglobals
. A customglobals
cannot raise errors on item access.If one applies the change I think must be made (Changing
PyDict_Check
toPyDict_CheckExact
inexec
andeval
's implementations) 4 more tests break.test_getpath
passes custom dictionaries toexec
(which, remember, the documentation forbids), and should be rewritten but does not seem to be very complex to do.test_descrtut
should changeexec("x = 3; print(x)", a)
toexec("x = 3; print(x)", {}, a)
, and then not expect 'builtins' to appear ina.keys()
test_dynamic
'stest_load_global_specialization_failure_keeps_oparg
should be rewritten to, instead of creating a class with__missing__
, create a dictionary with data (for i in range(variables): my_globals[f"_number_{i}"] = i
)test_type_aliases
.test_exec_with_unusual_globals
fails, but I do not understand how to fix it.Alternatively,
_PyEval_LoadName
could be modified (and the documentation updated) so subclasses of dictionaries are supported asglobals
, but this is a hot path and I have not measured any possible performance implications. Modifyingexec
to comply with the docs makes more sense, IMHO.CPython versions tested on:
CPython main branch, 3.12, 3.10, 3.9
Operating systems tested on:
Linux
The text was updated successfully, but these errors were encountered: