-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
locals() does not include local variables set in exec() statement in 3.13b2 #120708
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
Comments
Confirmed this change:
Possibly related to PEP-668; cc @markshannon @gaogaotiantian. Note that the backward compatibility section of the PEP says that certain corner cases may change: https://peps.python.org/pep-0667/#id1. |
This is due to PEP 667, but the part that matters is not the In short - we never encourage the users to use the side effects on the current scope with |
Can you close this then? |
Let me know if you have more questions @jpe . |
I think I see what's going on and can work around it (I've already rewritten the code where I first ran into it). I'm not convinced that it's ok for exec() to work with a copy of locals because there's probably a fair amount of code that relies on the changes being propagated to the frame locals. The following seems to work, though will it fail if the function is optimized? import sys
imp = 'import sys'
assign = 'b = 2'
def f():
frame = sys._getframe()
exec(imp, globals(), frame.f_locals)
exec(assign, globals(), frame.f_locals)
print(locals())
f() |
The function is optimized in your example. This should work. As for the code relying on this, yes there will be some. However, that's a fragile behavior from the beginning and we have warned the users not to use it for a long time.
This is an explicitly discouraged behavior and it should not be a big surprise that it stopped working one day. The current implementation provides a clearer boundary and semantics. |
I see; thanks |
Notice that assign1 = 'a = 2'
assign2 = 'b = 2'
def f():
a = 1
exec(assign1)
exec(assign2)
print(locals())
f() will print |
Actually, let me discuss this with more core devs. We'll see if this is the best behavior. It won't be the same as before - that's for sure, but maybe the |
Okay I made a mistake here. |
The original code is not code that I think is particularly well written and may have worked by accident, but it did work all the way to 3.13. I've already changed it. The code was getting a reference to the imported module via locals() and then returned it. Again, this was't the best way to do it and I would not recommend writing it that way. There's been at least 1 other bug report on this so this might not be a rarely used code pattern. I guess I don't follow why locals() is only returning a dict with the fast variables. If extra variables are added to the frame, shouldn't they be included? |
It should, but no variable is added to the frame. When you do assign = 'b = 2'
def f():
exec(assign)
print(b)
f() Because This is what actually happened on 3.13: assign = 'b = 2'
def f():
d = locals()
exec(assign, globals(), d)
exec('print(b)', globals(), d)
f()
|
After some digging, it looks like the way to add extra variables to the frame is through frame.f_locals. We have other code that does that and I'm glad I won't need to change it. Is the fact that extra variables can be set documented anywhere? It's implied in PEP 667, but PEPs are proposals and not necessarily reference documentation. I'm hoping that the extra variable storage won't disappear from the frame in some future version, though we may be able to work around that if it were to happen. |
Technically, it's impossible to add extra variables in the optimized frames (function scope frames) in CPython. It's impossible to make the following code work even with import sys
def f():
exec("a = 1", locals=sys._getframe().f_locals)
print(a)
f() You should not expect that there is a long-term support for the "extra variable"-like feature. What you might feel like the extra variable might be: import sys
def f():
exec("a = 1", locals=sys._getframe().f_locals)
exec("print(a)", locals=sys._getframe().f_locals)
f() This is a completely different story because both I'm not sure what you are trying to do (or have done with some workaround), but under the current mechanism of Python frames, really adding a variable is impossible. |
Why does |
Because the debugger needs it, otherwise it would make the life of the developers of the debugger miserable. |
I maintain a debugger which sets & retrieves extra variables when users execute arbitrary code in a stack frame. I imagine this is what pdb does as well, though it's been years since I looked at pdb. I think the extra variables in a frame were originally added way-back-when to support exec or from ... import *, maybe when fast locals were introduce (I've worked with the python core code since the 1.5.2 days). Now that exec no longer even appears to work in a function frame, I worry that the extra variables dict may be removed at some point. |
So wildcard import (
Again, your In 3.12 and before, that's a cached dict of the frame local variables. You can write anything to it, and in some cases (FastToLocals, LocalsToFast) it writes back to the frame fast variables or vice versa. Every call to Now
That being said, I don't think it's promised that this mechanism will live forever. The thing about working on a debugger is that you had to use some black magic that could be removed in the future. For now, I don't think there's any plan to remove the extra dict in |
from ... import * once worked in function frames, class scopes, and other places -- I brought it up since I think it was one of the motivations for adding a dictionary to the frame object in addition to the fast locals array. You are correct that when using exec(), a new frame is created. It is the frame object that I was referring to; the frame object is what contains the "PyObject *f_extra_locals;" field. Anyway, I think my questions are pretty much answered. Thanks for your help. |
Not really. I hope that I answered all of your questions. |
I suppose the alternative would be to operate on a copy of |
Yes, with f = sys._getframe()
exec(code, globals(), f.f_locals) a new frame (and c frame object) is created, but the locals (both the fast locals and f_extra_locals) are retrieved from and stored in the frame object f. It's likely that the frame that the code is exec'd in has no local variables stored. I think I confused the issue by switching from talking about the exec(" ") with no globals or locals arguments use case to the use case of running arbitrary code is a given stack frame in a debugger (or at least appearing to by using the locals from the frame). Thanks again. |
@eryksun Yes, the workaround for a debugger if support for f_extra_locals is dropped from the frame object is for the debugger to essentially manage the extra locals, though this complicates thing a bit -- the debugger probably wants to treat functions differently than module scopes. I'm fairly sure there were other uses for f_extra_locals in older python versions (from ... import *, exec, maybe others) so it wasn't only for debuggers. |
I hightly doubt that considering |
There has been support for storing the equivalent of extra locals back as far as I can remember. The dictionary may have been moved from somewhere else recently. |
This is basically exactly how it works before PEP 667 (converting fast locals to dict and vice versa), and that's a pretty bad experience. |
The extra locals were stored in |
Yes, that sounds right. So the extra locals were in f_locals along with the value of fast locals as of the last FastToLocals call. I just checked and in python 2.7, the following worked: def f():
exec('b = 2')
print b Note that I'm not trying to argue that this should work now or that it's good code, only that there once were other uses for the equivalent of extra locals. |
Why doesn't >>> def f():
... x = 42
... exec('del x', locals=sys._getframe().f_locals)
...
>>> f()
Traceback (most recent call last):
File "<python-input-3>", line 1, in <module>
f()
~^^
File "<python-input-2>", line 3, in f
exec('del x', locals=sys._getframe().f_locals)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
NameError: name 'x' is not defined >>> def f():
... exec('x = 42; del x', locals=sys._getframe().f_locals)
...
>>> f()
Traceback (most recent call last):
File "<python-input-6>", line 1, in <module>
f()
~^^
File "<python-input-5>", line 2, in f
exec('x = 42; del x', locals=sys._getframe().f_locals)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
NameError: name 'x' is not defined |
The inability to del is a change in behavior from prior versions. The following raises an UnboundLocalError. on the print() line in 3.11, sets n to None in 3.12, and raises a NameError on the exec() line in 3.13: import ctypes
import sys
PyFrame_LocalsToFast = ctypes.pythonapi.PyFrame_LocalsToFast
PyFrame_LocalsToFast.restype = ctypes.c_int
PyFrame_LocalsToFast.argtypes = [ctypes.py_object, ctypes.c_int]
def func():
frame = sys._getframe()
n = 1
exec('del n', globals(), frame.f_locals)
PyFrame_LocalsToFast(frame, 1)
print(n)
func() Should I open a new bug for this? |
No this is intentional. First of all,
The reason we chose to disallow users to delete from The difference between 3.11 and 3.12 is that we don't check if the local value is So for the new proxy, we simply disallow users to delete variables, because there's no reasonable result coming out of it. Maybe deleting "extra variable" would make sense, because they are not there anyway. But at least for now, for consistency, we simply don't allow that - so the users don't need to care about whether the variable is "extra". |
Sigh. 😞 |
I understand. Thanks for the explanation. |
Bug report
Bug description:
The f() function above prints {} when run with 3.13b2 (debug build on macOS) instead of the expected {'sys': <module 'sys' (built-in)>, 'b': 2}
CPython versions tested on:
3.13
Operating systems tested on:
macOS
The text was updated successfully, but these errors were encountered: