If you don't know why something works in Python, it can often help put a behavior that confuses you into functions, and then parse it from Python bytecode using the dis module.
Let's start with a simpler version of your code:
def foo(): exec("K = 89") print(K)
If you run foo() , you will get the same exception that you see with your more complex function:
>>> foo() Traceback (most recent call last): File "<pyshell#167>", line 1, in <module> foo() File "<pyshell#166>", line 3, in foo print(K) NameError: name 'K' is not defined
Let's take it apart and see why:
>>> import dis >>> dis.dis(foo) 2 0 LOAD_GLOBAL 0 (exec) 3 LOAD_CONST 1 ('K = 89') 6 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 9 POP_TOP 3 10 LOAD_GLOBAL 1 (print) 13 LOAD_GLOBAL 2 (K) 16 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 19 POP_TOP 20 LOAD_CONST 0 (None) 23 RETURN_VALUE
The operation you need to pay attention to is the number indicated by "13". Here, the compiler processes the search for K in the last line of the function ( print(K) ). It uses the LOAD_GLOBAL , which fails because "K" is not a global variable name, but rather a value in our locals() dict (added by exec ).
What if we persuaded the compiler to see K as a local variable (giving it a value before running exec ), so that it would know that it would not look for a global variable that did not exist?
def bar(): K = None exec("K = 89") print(K)
This function will not give you an error if you run it, but you do not get the expected value:
>>> bar() None
Let's take it apart to understand why:
>>> dis.dis(bar) 2 0 LOAD_CONST 0 (None) 3 STORE_FAST 0 (K) 3 6 LOAD_GLOBAL 0 (exec) 9 LOAD_CONST 1 ('K = 89') 12 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 15 POP_TOP 4 16 LOAD_GLOBAL 1 (print) 19 LOAD_FAST 0 (K) 22 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 25 POP_TOP 26 LOAD_CONST 0 (None) 29 RETURN_VALUE
Pay attention to the operation codes used in "3" and "19". The Python compiler uses STORE_FAST and LOAD_FAST to put the value for the local variable K in slot 0 and later to return it. Using numbered slots is significantly faster than inserting and fetching values ββfrom a dictionary like locals() , so the Python compiler does this for all local access variables in a function. You cannot overwrite a local variable in a slot by changing the dictionary returned by locals() (like exec , unless you pass it a dict for use in your namespace).
In fact, let's try the third version of our function, where we again look at locals when we have K , defined as a regular local variable:
def baz(): K = None exec("K = 89") print(locals())
You won't see 89 in the output this time either!
>>> baz() {"K": None}
The reason you see the old K value in locals() is explained in the function documentation :
Refresh and return a dictionary representing the current local character table.
The slot in which the local variable K is stored has not been modified by the exec statement, which only modifies the locals() dict. When you call locals() again, Python "updates [s]" the dictionary with the value from the slot, replacing the value stored there with exec .
That's why the docs say:
Note: The contents of this dictionary should not be changed; changes may not affect the values ββof local and free variables used by the interpreter.
Your call to exec modifies the locals() dict and you will learn how its changes are not always visible by your later code.