Python imports that work both when using a file as a module and when called directly

Viewed 74

I was reading today about absolute and relative imports in Python, and I was wondering if there is a pythonic way of making an import work when the script that contains the import is used as a module and when it is called directly. As an example:

└── project
      ├── main.py
      └── package
             ├── submoduleA.py
             └── submoduleB.py

main.py

from package.submoduleA import submoduleAfunction
submoduleAfunction() 

submoduleB.py

def submoduleBfunction():
    print('Hi from submoduleBfunction')

submoduleA.py

# Absolute import, works when called directly: python3 package/submoduleA.py 
from submoduleB import submoduleBfunction 

# Relative import, works when imported as module: python3 main.py
# from .submoduleB import submoduleBfunction 

def submoduleAfunction():
    submoduleBfunction()
    
if __name__=="__main__":
    submoduleAfunction()

As you see, I have there both absolute and relative imports. Each of them works in a specific situation. But is there a pythonic way of making it work for both cases? So far I've done the trick messing with sys.path, but I'm not sure that this will work in every situation (i.e. called from interpreter or in different OS's) or even if this goes against any recommendations.

import sys
import pathlib
sys.path.append(str(pathlib.Path(__file__).parent.resolve()))
3 Answers

Disclaimer:

  • Your mileage may vary and I realize that my answer does not fully answer the question (using relative imports). Mea maxima culpa (my bad).

  • In my defense: googling python avoid relative imports returns 540k hits.

I usually avoid relative imports because they seem to have too many special cases on just this type of things and I am a huge fan of running python files directly as scripts when appropriate.

For example, my constants.py file can be run as a script - it will breakpoint() and I can inspect its contents, a good deal of which come from environment variables.

Assuming that the parent directory for project is in your Python path, I would just use from project.package.submoduleA import submoduleAfunction. That works in all cases.

(One possible additional benefit I found with explicit imports is when I wanted to rename my top-level package (myproject to myproject2). Having everything explicit allowed me to quickly sed the changes. Note Wim's remarks in the comments below)

# Absolute import, works when called directly: python3 package/submoduleA.py 
from submoduleB import submoduleBfunction 

Since submoduleA is the "sibling" of submoduleB, this is actually an implicit relative import. The style guide says of them:

Implicit relative imports should never be used and have been removed in Python 3.

The only reason it even works when running python3 package/submoduleA.py as a script is that Python prepends the directory of the script to sys.path. From the docs:

As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter.

That is, the directory /path/to/package gets injected to sys.path, allowing submoduleB to be imported directly.

Implicit relative imports should never be used, so what should you use instead? It doesn't matter, you can use the correct form of the absolute import:

from package.submoduleB import submoduleBfunction

or a relative import:

from .submoduleB import submoduleBfunction

Either is fine as long as package has been installed into the environment (installing the package is what will put the code into site-packages, which is on sys.path).

If you absolute must run package/submoduleA.py directly as a script, which is considered an anti-pattern, then you will need to use the absolute import version. Relative imports do not work in this case. You would need to use instead python3 -m package.submoduleA in order for the relative-import version to work.

One possibility is to just enclose the absolute import in a try block and catch an ImportException. In case of an exception, you are presumably using it as a module so then do a relative import.

try:
    ​from submoduleB import submoduleBfunction 
except ImportException:
    from .submoduleB import submoduleBfunction 
Related