classification
Title: global declarations affect too much inside exec or compile
Type: behavior Stage: needs patch
Components: Interpreter Core Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: Kevin Shweh, gvanrossum, lys.nikolaou, pablogsal, terry.reedy
Priority: normal Keywords:

Created on 2020-10-29 05:07 by Kevin Shweh, last changed 2020-10-31 01:01 by pablogsal.

Messages (4)
msg379855 - (view) Author: Kevin Shweh (Kevin Shweh) Date: 2020-10-29 05:07
A global declaration inside a function is only supposed to affect assignments inside that function, but in code executed with exec, a global declaration affects assignments outside the function:

>>> gdict = {}
>>> ldict = {}
>>> exec('x = 1', gdict, ldict)
>>> 'x' in gdict
False
>>> 'x' in ldict
True
>>> 
>>> gdict = {}
>>> ldict = {}
>>> exec('''
... x = 1
... def f(): global x''', gdict, ldict)
>>> 'x' in gdict
True
>>> 'x' in ldict
False

Here, we can see that the presence of a "global x" declaration inside f causes the "x = 1" outside of f to assign to globals instead of locals. This also affects code objects compiled with compile().
msg380024 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2020-10-31 00:37
exec with different global and local dicts is most like executing code in a class definition, where locals is different from globals.  But mimicking your exec with an actual class statement does not produce the same result.

>>> gdict = {}
>>> class C:
	x = 1
	def f(): global x
	
>>> gdict
{}
>>> C.x
1

# To continue, I reproduced the behavior in 3.10.
>>> ldict = {}
>>> exec('''
x = 1
def f(): global x''', gdict, ldict)
>>> 'x' in gdict
True

# And that putting x in gdict required the global declaration.
>>> gdict = {}
>>> exec('''
x = 1
def f(): pass''', gdict, ldict)

>>> 'x' in gdict
False
>>> 'x' in ldict
True

This seems like a bug to me too.  The change is in the bytecode, not the subsequent execution thereof.

>>> dis.dis('x = 1')
  1           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (x)
              ...

# Ditto if add '\ndef f(): pass', but here is the test case.
>>> dis.dis('x = 1\ndef f(): global x')
  1           0 LOAD_CONST               0 (1)
              2 STORE_GLOBAL             0 (x)
              ...

# Same result for 'global x; x = 1', but 'x = 1; global x' raises
SyntaxError: name 'x' is assigned to before global declaration


The change is inconsequential when locals is globals, but not when not.
msg380028 - (view) Author: Guido van Rossum (gvanrossum) * (Python committer) Date: 2020-10-31 00:54
Looks like a bug. Maybe someone can bisect and find when this started happening?
msg380029 - (view) Author: Pablo Galindo Salgado (pablogsal) * (Python committer) Date: 2020-10-31 01:01
> Maybe someone can bisect and find when this started happening?

As far as I understand, this has been the case since Python 2 at least:

$cat code.py
gdict = {}
ldict = {}
exec('''
x = 1
def f(): global x''', gdict, ldict)
print(('x' in gdict, 'x' in ldict))


gdict = {}
ldict = {}
exec('''
x = 1
def f(): pass''', gdict, ldict)
print(('x' in gdict, 'x' in ldict))


$ python2 code.py
(True, False)
(False, True)

$ python3.9 code.py
(True, False)
(False, True)
History
Date User Action Args
2020-10-31 01:01:46pablogsalsetmessages: + msg380029
2020-10-31 00:54:12gvanrossumsetmessages: + msg380028
2020-10-31 00:37:01terry.reedysetversions: + Python 3.9, Python 3.10
nosy: + terry.reedy, pablogsal, gvanrossum, lys.nikolaou

messages: + msg380024

stage: needs patch
2020-10-29 05:07:31Kevin Shwehcreate