Change of a global variable gets lost when importing the enclosing namespace

Viewed 36

I was playing with scopes and namespaces and I found a weird behaviour which I'm not sure how to explain. Say we have a file called new_script.py with inside

a = 0

def func():
    import new_script #import itself
    new_script.a += 1
    print(new_script.a)

func()
print(a)

when executing it prints

1 
1 
2 
0

I didn't expect the last print of the number 0. From what I understand, it prints the first two 1 executing the self-import statement incrementing the global a, then it prints 2 because it increments again the global a from inside the function, but then why the last print is 0 instead of 2?

2 Answers

TL;DR: you have two different variables __main__.a and new_script.a. You only change new_script.a.

To trace it through:

a = 0

defines a variable a in __main__ module.

def func(): ...

defines a function func in __main__ module.

func()

calls this function in __main__ module. In function:

import new_script

imports new_script module in which:

a = 0
def func(): ...

defines a and func in new_script module (new_script.a and new_script.func)

func()

calls func from new_script module. In function:

import new_script

well, we have it imported already so we don't import it again

new_script.a += 1
print(new_script.a)

increment new_script.a and print it (our first 1). Then

print(a)

printing a (a.k.a new_script.a) from new_script module (our second 1)

new_script is finished. Back to execution in the __main__:

new_script.a += 1
print(new_script.a)

incrementing new_script.a second time and printing it (our 2).

Finally:

print(a)

printing a (a.k.a. __main__.a) that has never been changed (so 0)

Well, this has lead down a very interesting rabit hole. So thanks you for that.

Here are the key points:

  • Imports will not recurse. If it's imported once, it will execute the module level code, but it will not execute again if it's imported again. Hence you only see 4 values.
  • Imports are singletons. If you try this code:
# singleton_test.py
import singleton_test

def func():
    import singleton_test #import itself
    print(singleton_test.singleton_test == singleton_test)

func()

It will print:

True
True
  • The imported singleton version of a module is different from the original ran version of the module

With this in mind, we can explore your code, by enriching it with a few more comments, particularly using __name__ which contains the name of the current module, which will be __main__ if the current module is what was ran originally:

a = 0

print("start", __name__)

def func():
    print("Do import", __name__)
    import new_script #import itself
    new_script.a += 1
    print(new_script.a, "func", __name__)

func()
print(a, "outr", __name__)

This will print

start __main__
Do import __main__
start new_script
Do import new_script
1 func new_script
1 outr new_script
2 func __main__
0 outr __main__

This shows you quite well, given that the imported module is a singleton (but not the module that was ran), that you

  • first print 1 in the function after you incremented value inside the function inside the module
  • then you print 1 at the end of the imported module
  • then you print 2 after incrementing the value on the singleton on the original run code
  • then finally you print 0 for the unchanged outer module that you originally ran, but have not touched.
Related