Title: Python Imports 101
Slug: python-imports-101
Date: 2023-06-08 13:15:00
Author: Kartones
Lang: en
Tags: Development, Python, Architecture, Patterns & Practices
og_image:
Description: Explains Python import basics, relative vs. absolute imports, import resolution, and provides examples to clarify common import scenarios.


Python imports are not that hard once you understand how they work internally. I needed to revisit the topic recently, so not being my daily programming language any more, I think it would be interesting to write a short summary for my future self (and potential visitors).


### Basics

The most common import scenarios are:

- Module import, all module: `import os`
- Module import, a submodule: `import os.path as path` or `from os import path`
- Class/Function imports: `from os.path import (abspath, dirname)`

You can also see that using `... as ...` you can alias imports.

Importing from a file follows the same syntax:

Given the example:
```
/a.py
/folder/b.py
/c.py
```

From `c.py` you can do the following:
```python
import folder.b
import a
```

Clear and simple, no problems so far.

### Relative vs absolute imports

Given the structure:
```
/src/config_folder/config.py
/src/a.py
/src/run.py
```

You can reference your current package/module via `.` (as in `from . import a`), and add additional dots to traverse up to parent folders. However, the reference point can vary, you need to have a parent module, and things can get complicated as codebases grow and you move code around.

You can also reference your modules via absolute imports, with either `from config_folder import config` or by referencing a package path,  `from src.config_folder import config`. But we will see that this can also be a bit complex at times, (hint: you probably don't want that `src.` prefix in the import statement).

### Import resolution

The path to module imports is resolved with the following logic:

- Python's `sys.path` value (read from the `PATH` environment variable)
- The script location (where the script is run from)
- `PYTHONPATH` environment variable ([+ info](https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH))

Modifying `sys.path` is not the best approach, but in certain scenarios, like when you are working in local scripts just for yourself, it can be an option. For example, if I want to keep an API key available to multiple scripts all around my hard drive, I can do:

```
import sys
sys.path.append("/a/path/containing/my/config/file")
from my_config import AN_API_KEY
```

But in general, we shouldn't mess around with `sys.path`. So that leaves us two choices for more production-like scenarios:

- Always use relative imports: This will help with the second case, and most IDEs support updating import paths
- Use `PYTHONPATH`, always run the code from a few entry points, and use absolute imports

The single most critical point is that the import resolution (or "root") is calculated by default from **the launched script location**. If you run `python3 /a/b/c.py`, the root folder to search for import modules is going to be `/a/b/`; But if you run `cd /a; python3 b/c.py`, the root folder is also going to be `/a/b/`, because the location from which you run does not matter.

Using the previous section example again:
```
/src/config_folder/config.py
/src/a.py
/src/run.py
```

Contents of `run.py`:
```
from config_folder import config
# ...
```

- to import `config.py` from `run.py`
- if we are going to run `python3 run.py` from inside `/src`
- then we should do `from config_folder import config`, omitting the `src` package, because we're already inside it

If we want to namespace each subproject (common practice for example in Django projects), we'd need to arrange our code to have an additional package level, for example like:

```
/src/myapp/config_folder/config.py
/src/myapp/a.py
/src/myapp/run.py
```

Contents of `run.py`:
```
from myapp.config_folder import config
# ...
```


And we should run `python3 myapp/run.py` from the `src` folder... But if we try, it will still give you an `ModuleNotFoundError: No module named 'myapp'` error, why? It errors because, if you remember, it will switch to `myapp` as the root folder to execute `run.py`. And so, this is why using `PYTHONPATH` always is a good approach. The following will work if run from the `src` folder:
```bash
$ PYTHONPATH=. python3 myapp/run.py
```

Alternative with absolute path (can be run from anywhere):
```bash
$ PYTHONPATH=/src/ python3 /src/myapp/run.py
```

### Examples & Conclusion

I've created examples of the three most common scenarios for absolute imports and uploaded them to [my GitHub's Python miscellaneous repository](https://github.com/Kartones/python/tree/master/imports):

- Import from a file in the same folder
- Import from a file in a sub-folder
- Import from a file in a sibling folder

The third one is often the source of headaches.

Note that I didn't created relative import examples, because a) I find absolute imports more clear, and b) I'm used to running things almost always specifying `PYTHONPATH`, and very often from a container (where the entry points are also very clearly defined).