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, which we’ll continue to reference for the next section:

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 not actually the case. import bar is an absolute import, and absolute imports need to follow sys.path to import modules.

How does sys.path affect imports?#

sys.path is a list of directories that Python searches when resolving imports. You can see what sys.path looks like by printing it out, or by using the command python -m site:

# Example output of 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',
]
...

When Python sees import bar, it iterates through each of the above directories to find a 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.

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, they can import each other with import foo and 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 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.

In this situation, the correct way to import the submodules would be using their fully qualified names, pkg.foo and pkg.bar:

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

These absolute imports will work anywhere you write them, whether it be pkg/foo.py or main.py, as long as the package can be found in sys.path.

What about relative imports?#

If you’ve seen any project that uses imports like from . import mod, where the import always starts with from and is followed by one or more leading . periods, those are known as relative imports. They work inside any submodule where you need to import a sibling or parent module, and can be used in place of their equivalent absolute imports.

For example, the absolute imports in the previous section could be re-written using relative imports like so:

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

Here, Python will assume that your relative imports start from each submodule’s parent package, pkg, meaning you don’t have to write out their fully qualified names.

Beware, relative imports aren’t a general form of import that you can use to replace all absolute imports. For example, writing from . import pkg in main.py results in the following ImportError:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
    from . import pkg
ImportError: attempted relative import with no known parent package

Relative imports are only allowed in submodules where you had to import them through a parent package, like pkg/foo.py and pkg/bar.py.

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.