Imports at a Distance

Sun 25 June 2017 by Moshe Zadka

(Thanks to Mark Williams for feedback and research)

Imagine the following code:

## mymodule.py
import toplevel.nextlevel.lowmodule

def _func():
    toplevel.nextlevel.lowmodule.dosomething(1)

def main():
    _func()

Assuming the toplevel.nextlevel.module does define a function dosomething, this code seems to work just fine.

However, imagine that later we decide to move _func to a different module: : .. code:

# utilmodule.py
import toplevel

def _func():
    toplevel.nextlevel.lowmodule.dosomething(1)

This code will probably still work, as long as at some point, before calling _func, we import mymodule.

This introduces a subtle action-at-a-distance: the code will only stop working when we remove the import from mymodule -- or any other modules which import lowmodule.

Even unit tests will not necessarily catch the problem, depending on the order of imports of the unit tests. Static analyzers, like pylint and pyflakes, also cannot catch it.

The only safe thing to do is to eschew this import style completely, and always do from toplevel.nextlevel import lowmodule.

Addendum

Why is this happening?

Python package imports are a little subtle.

import toplevel

Does three things:

  • (Once only) Creates a toplevel entry in sys.modules
  • (Once only) Executes toplevel/__init__.py inside the namespace of toplevel
  • Creates a variable called toplevel and assigns it the module.

The things marked "once only" will only happen the first time toplevel is imported.

import toplevel.nextlevel

Does the same three things (with :code:`toplevel`) as well as:

  • (Once only) Creates a toplevel.nextlevel entry in sys.modules
  • (Once only) Executes:code:toplevel/nextlevel/__init__.py inside the namespace of toplevel.nextlevel
  • (Once only) Creates a variable nextlevel in the namespace of toplevel, and binds the toplevel.nextlevel module to it.

The third one is the most interesting one -- note that the first time toplevel.nextlevel is imported, a nextlevel variable is created in the namespace of toplevel, so that every subsequent place that imports toplevel can access nextlevel for "free".