Search code examples
pythonpython-importlib

Why undeclared variables are present in `globals()` after a reload and is it safe to use them to identify a reload?


I've found that the snippet below when reloading a module test unexpectedly has all variables already defined in globals()/locals().

Why this happens?

I've noticed this "xxx" in locals() pattern a lot in Blender Python scripts as typically people use it to check whether module was reloaded before (you typically reload those scripts a lot during the development). Since everyone's using it, I guess it should work most of the times.

But is it really safe to use this caveat to identify whether module was already loaded before (any ideas of cases when it wouldn't work?)? I mean not just for the development and testing stuff as it does looks like more of an implementation detail that could break in the production.

# prints False, False, False
import test
import importlib

# prints True, True, True
importlib.reload(test)

test.py:

print("a" in globals(), "b" in globals(), "c" in globals())

# NameError: name 'a' is not defined
# print(a)

a = 25
b = 35
c = 45

Solution

  • As stated by @jasonharper in the comments: yes, the __dict__ of the module is retained when importlib.reload is used. (From the importlib.reload() documentation: "The names in the module namespace are updated to point to any new or changed objects" and "When a module is reloaded, its dictionary (containing the module’s global variables) is retained". Quoted from the docs by @jasonharper)

    For regular code that is never an issue, since all names that a module will use have to be defined first - either as an assignemtn of as a function or class declaration. This assignment will update the dict and replace the previous references that is there.

    But checking if names as strings are present in globals() like you are doing will detect the pre-existing names, and can be used to know if the module is in the middle of a reloading operation.

    However, if the module is maually "deleted" from sys.modules prior to reloading, then it will run with a fresh globals() dictionary. This can only be done intentionally - so if you plan to base any behavior on whether an item is present in globals() it is ok.

    To see the behavior I mean, try this snippet:

    # prints False, False, False
    import test
    import importlib
    
    # prints True, True, True
    importlib.reload(test)
    
    import sys
    del sys.modules("test")
    importlib.reload(test)
    
    # or even:
    del sys.modules("test")
    # module is reloaded, even without a `.reload()` call,
    # as it is not in sys.modules anymore
    import test