Search code examples
pythonimportscopenamespacesglobal-variables

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


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?


Solution

  • 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.