Why can’t I import my submodules?#

Are you trying to write a package but you can’t figure out how to import your submodules correctly, and it keeps failing with ModuleNotFoundError? This shortened guide from my Using python -m to invoke modules as scripts article should hopefully help you understand why you’re getting those errors, and how to fix it.

Before you read…

This guide requires some pre-requisite knowledge of using Python. If you can answer the following questions with at least some level of confidence, you can continue ahead:

  • What is a terminal? What can you use it for?

  • What is a current working directory?

  • How do you run Python scripts (.py files) from the terminal?

  • How do you make a Python script import another script?

How do Python imports work?#

In case you’re not sure about the distinctions between a script, module, and a package, let’s assume that (1) a script is a .py file you can run with python script.py, (2) a module is something you can import, and (3) a package is a specific kind of module consisting of a directory with an __init__.py.

You might already know that scripts can import other scripts as modules and access their functions and classes, allowing you to organize your program across separate files and re-use your code in a modular way. However, when you start getting into writing packages and putting submodules inside them, importing those submodules is no longer as intuitive as you might think.

Take for example the following layout:

CWD/
├── pkg/
   ├── __init__.py
   ├── foo.py
   └── bar.py
└── main.py

If you were to write import bar in foo.py, what do you think would happen? Presumably Python would find bar.py and import its contents because it’s next to foo.py, right? This is actually not always the case. import bar is an absolute import, which means it’s going to follow Python’s sys.path to find a bar module to be imported.

How does sys.path affect imports?#

sys.path is a list of directories that Python searches when resolving imports. When Python sees import bar, it iterates through each directory to find the first module that matches the name bar before importing it. This includes your Python’s standard library and the site-packages directory where your pip-installed modules go to.

You can see for yourself what sys.path looks like by printing it out, or by using the command python -m site:

sys.path = [
    '/home/thegamecracks/thegamecracks.github.io',
    '/home/thegamecracks/.pyenv/versions/3.11.9/lib/python311.zip',
    '/home/thegamecracks/.pyenv/versions/3.11.9/lib/python3.11',
    '/home/thegamecracks/.pyenv/versions/3.11.9/lib/python3.11/lib-dynload',
    '/home/thegamecracks/thegamecracks.github.io/.venv/lib/python3.11/site-packages',
]
USER_BASE: '/home/thegamecracks/.local' (exists)
USER_SITE: '/home/thegamecracks/.local/lib/python3.11/site-packages' (doesn't exist)
ENABLE_USER_SITE: False

Now, here’s the important thing to know: All absolute imports rely on sys.path.

It’s a common mistake to think that because pkg/foo.py and pkg/bar.py are next to each other, either of them can use import foo or import bar. This is false. Absolute imports don’t care about what modules are next to your script, only modules that can be found in sys.path.

What affects sys.path then? The most important consideration here is how you run Python in the terminal. When you run a command like python path/to/script.py, Python adds the directory containing the script, path/to/, to sys.path. So in the previous layout, if you ran python main.py, CWD/ would be in sys.path. This means Python would only be able to find the package pkg, and not its inner modules foo and bar. Therefore, to import either submodule, you must refer to them by their fully qualified names, pkg.foo and pkg.bar, rather than simply writing import bar.

Hint

This is where you might use relative imports over absolute imports!

from . import foo
from . import bar
from .foo import ham, spam

Python will assume that your relative imports start from each module’s parent package, pkg, meaning you don’t have to write out their fully qualified names. In other words, the above relative imports become equivalent to the following absolute imports:

from pkg import foo
from pkg import bar
from pkg.foo import ham, spam

But beware, relative imports can’t be used outside of submodules. You’ll get an ImportError if you try to do so.

How should I structure my project?#

Because of these quirks, it’s important to organize and run your scripts in a consistent format to avoid stumbling into import problems and having to re-write your imports. For example, you might put modules and scripts in the same directory and then run your scripts with python path/to/script.py:

my_project/
└── app/
    ├── layouts/
       ├── __init__.py
              from parser import Body, Footer, Header
       └── ...
    ├── parser/
       ├── __init__.py
       └── ...
    ├── compile.py
           from layouts import create_layout
           from parser import Body, Footer, Header
    ├── generate.py
    └── validate.py
/my_project $ python app/generate.py
/my_project $ python app/validate.py
/my_project $ python app/compile.py

Or you might organize all of your scripts into a package and use python -m package.submodule:

my_project/
└── my_package/
    ├── sub_package/
       └── __init__.py
               from my_package import submodule
    ├── __init__.py
           from . import sub_package
    ├── __main__.py
    ├── migrate.py
    └── submodule.py
/my_project $ python -m my_package --help
/my_project $ python -m my_package.migrate --input foo.csv --input bar.csv

The use of -m in python -m path.to.mod makes Python prepend your current working directory to sys.path, unlike python path/to/main.py which prepends the script’s parent directory.

However you organize your scripts, the one thing I recommend is setting your project root as the current working directory. cd ing around to run different scripts for one project is cumbersome, can unintentionally change your sys.path, and can be confusing for other users which have to contend with the same file structure and might assume that your project root is where they should run your commands from. However, if you think your way makes your project easier to work with, feel free to stick to it! Just make sure to keep your procedures documented for others and/or for your future self.