No, not really. The behaviour of the first code snippet is an extremely well known issue that generates frequent duplicate links on Stack Overflow:
The error isn’t a “warning mechanism”; it’s a consequence of how the code was understood.
And of course using exec
as a workaround causes the code to run without error - because the code being exec
’d is just a string, so the compiler has no reason to consider what effect it could potentially have at runtime. And indeed, if we disassemble it (this disassembly from 3.8):
>>> x = 1
>>> def foo():
... exec('print(x)')
... x = 2
...
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_GLOBAL 0 (exec)
2 LOAD_CONST 1 ('print(x)')
4 CALL_FUNCTION 1
6 POP_TOP
3 8 LOAD_CONST 2 (2)
10 STORE_FAST 0 (x)
12 LOAD_CONST 0 (None)
14 RETURN_VALUE
We can clearly see that x
is still understood to mean a local variable - since there is still an x = 2
assignment, and no global
declaration.
And, of course, when the code print(x)
is exec
d, it’s compiled at that moment, independently of the function where the exec
call was made. Because that’s what exec
means, and what it’s for. Its purpose is to interpret a string as if it were Python code, in the current context - i.e., in isolation, at the point when and where it’s called.
No; it’s executing in the scope that was passed:
Help on built-in function exec in module builtins:
exec(source, globals=None, locals=None, /)
Execute the given source in the context of globals and locals.
The source may be a string representing one or more Python statements
or a code object as returned by compile().
The globals must be a dictionary and locals can be any mapping,
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
So, eval("print(x)")
will use the function’s locals as locals, and the current globals as globals. But because there is no assignment in the code string, it will compile such that x
is looked up via LOAD_NAME
- i.e., it will check globals after failing to find x
in locals.
I don’t really see why. It’s following the same rules that led to UnboundLocalError
in the first place.
This, on the other hand, is interesting. It does make sense that the separate exec
calls should be “isolated” that way.