Sys.path

Last edited

Understanding sys.path

Suppose you have a project that uses the common src/ layout:

/tmp/proj
├── pyproject.toml
└── src
    ├── fetcher
    │   └── __init__.py
    └── twatch
        ├── __init__.py
        └── __main__.py

fetcher and twatch aren’t two fully separate projects, but you want the code to act like they are, where twatch imports fetcher as a top-level package.

# twatch/__init__.py
import fetcher

This looks like it would be valid? However running:

python3 -m twatch
# /usr/bin/python3: No module named twatch

It fails on twatch before it even gets as far as import fetcher, but that’s the same problem twice, not two problems: neither one lives anywhere python looks. So the question is where does python look, and how do I get src/ into that list.

Let’s see where python is searching to find modules and packages

# cwd /tmp/proj
python3 -c "import sys; print(sys.path)"
[
    '', # cwd
    '/usr/lib/python313.zip',
    '/usr/lib/python3.13',
    '/usr/lib/python3.13/lib-dynload',
    '/usr/local/lib/python3.13/dist-packages',
    '/usr/lib/python3/dist-packages'
]
# As you can see there no way to find our packages/modules

Python is not smart, it doesn’t walk the file system, it JUST loops through this list.
When you run import fetcher it walks the sys.path list and in each entry tries to find (in this order):

  1. a folder named fetcher with __init__.py inside it
  2. a file named fetcher.py
# Here's the structre for this test:
.
├── pyproject.toml
└── src
    ├── fetcher.py # note the module
    ├── fetcher    # note the package
    │   └── __init__.py
    └── twatch
        ├── __init__.py
        └── __main__.py

Now let’s see it resolve

python3 -m twatch
ran fetcher hello from fetcher/__init__.py

# Moving fetcher package out, so we can hit the module
mv fetcher othername
python3 -m twatch   
ran fetcher hello from fetcher.py

Okay I get it. I see it’s dumb and I need to help it find folders (packages) and files (modules). What ways can I do this?

  1. sys accepts an environment variable PYTHONPATH, which will add it to the start of the list of sys.path.

    export PYTHONPATH="$PWD/src"  
    python3 -c "import sys; print(sys.path)"
    [
        '',
        '/tmp/proj/src',
        ...
    ]

    Great so now it will search for packages and modules in that folder as well

  2. Install the packages from source with something like pip, which will add either a copy or a symlink of the source files into a folder that’s already listed.

    source .venv/bin/activate 
    python3 -c "import sys; print(sys.path)"
    [
        '',
        '/usr/lib/python313.zip',
        '/usr/lib/python3.13',
        '/usr/lib/python3.13/lib-dynload',
        '/tmp/proj/.venv/lib/python3.13/site-packages' # replaced : the two dist-packages
    ]

    So now python will check the venv’s site-packages folder for packages and modules.

    We need to first tell our build tool (and in turn pip) where to find the folder to install.

    [project]
    name = "proj"
    version = "0.1.0"
    
    [build-system]
    requires = ["setuptools"]
    
    [tool.setuptools.packages.find]
    where = ["src"]         # this says build (copy) the folders/files from this folder.

    First I’ll do a non-editable install, which will just copy the current folders/files in the folder so they can be resolved:

    pip install . 
    ls .venv/lib/python3.13/site-packages/
    fetcher twatch
    
    ls .venv/lib/python3.13/site-packages/fetcher 
    __init__.py
    
    cat .venv/lib/python3.13/site-packages/fetcher/__init__.py 
    hello_from_fetcher = "hello from fetcher/__init__.py"

    Second I’ll show a editable install, note implementation differs on the build system

    pip uninstall proj # remove our last install (copy)
    pip install --editable . # read the pyproject.toml to find what we need to do
    
    # I'm using setuptools which just uses this .pth file to tell python to look in the folder for more packages (basically a link)
    (.venv) ➜  proj cat .venv/lib/python3.13/site-packages/__editable__.proj-0.1.0.pth 
    /tmp/proj/src
    (.venv) ➜  proj 

    All that work and now python can find it’s way to our package from anywhere since the .venv is active and thus the folders are in the sys.path list

    (.venv) ➜  proj pwd
    /tmp/proj
    (.venv) ➜  proj python -m twatch
    ran fetcher hello from fetcher.py

Commands

# pretty print the sys.path list
python3 -c "import sys; from pprint import pprint; pprint(sys.path)"