This is due to the way Python converts your code into bytecode (compilation step).
When compiling a function, Python processes the entire variable, which is assigned as a local variable, and performs optimization to reduce the number of name searches that it would have to perform. Each local variable is assigned an index, and when the function is called, their value will be stored in the local stack array addressed by the index. The compiler will issue the code LOAD_FAST and STORE_FAST to access the variable.
The global syntax instead indicates to the compiler that even if a variable is assigned a value, it cannot be considered as a local variable, it should not be assigned an index. Instead, it will use the code LOAD_GLOBAL and STORE_GLOBAL to access the variable. These opcodes are slower, since they use the name to search in as many dictionaries as possible (locals, globals).
If the variable is read-only, the compiler always allocates LOAD_GLOBAL , because it does not know if it should be a local or global variable, and therefore, assume that it is global.
So, in your first function, global x tells the compiler that you want it to treat write access to x as a global variable record instead of a local variable. The operation codes for the function make it clear:
>>> dis.dis(changeXto1) 3 0 LOAD_CONST 1 (1) 3 STORE_GLOBAL 0 (x) 6 LOAD_CONST 0 (None) 9 RETURN_VALUE
In the third example, you import the __main__ module into a local variable named __main__ , and then assign its field x . Since the module is an object that saves all the top-level mapping as fields, you assign the variable x in the __main__ module. And, as you discovered, the __main__ module fields directly display the values ββin the globals() dictionary, because your code is defined in the __main__ module. The operation codes show that you do not have direct access to x :
>>> dis.dis(changeXto3) 2 0 LOAD_CONST 1 (-1) 3 LOAD_CONST 0 (None) 6 IMPORT_NAME 0 (__main__) 9 STORE_FAST 0 (__main__) 3 12 LOAD_CONST 2 (3) 15 LOAD_FAST 0 (__main__) 18 STORE_ATTR 1 (x) 21 LOAD_CONST 0 (None) 24 RETURN_VALUE
The second example is interesting. Since you assign the value to the variable x , the compiler assumes that it is a local variable and performs the optimization. Then from __main__ import x imports the __main__ module and creates a new binding of the value x in the __main__ module to a local variable named x . This is always the case, from ${module} import ${name} just create a new binding of the current namespace. When you assign a new value to the variable x , you simply change the current binding, not the binding in the __main__ module, which is not connected (although if the value is changed and you mutate it, the change will be visible through all the bindings). Here are the operation codes:
>>> dis.dis(f2) 2 0 LOAD_CONST 1 (-1) 3 LOAD_CONST 2 (('x',)) 6 IMPORT_NAME 0 (__main__) 9 IMPORT_FROM 1 (x) 12 STORE_FAST 0 (x) 15 POP_TOP 3 16 LOAD_CONST 3 (2) 19 STORE_FAST 0 (x) 22 LOAD_CONST 0 (None) 25 RETURN_VALUE
A good way to think about this is that in Python all assignments bind a name to a value in a dictionary, and dereferencing just searches the dictionary (this is an approximate approximation, but pretty close to the conceptual model). By executing obj.field , you are viewing a hidden dictionary obj (accessible via obj.__dict__ ) for the "field" key.
When you have a bare variable name, it is looked up in the locals() dictionary, and then the globals() dictionary if it is different (they are the same when the code is executed at the module level). For assignment, it always places the binding in the locals() dictionary, unless you have declared that you want to access globally by executing global ${name} (this syntax also works at the top level).
So, translating your function, it's almost like you wrote: