Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Python Import From Another Directory: Why It Fails and What to Do

Sean

Platform Writer

Aug 10, 2026
8 min read

Python looks for modules in sys.path, which contains the directory of the script you ran, anything in PYTHONPATH, and the installed packages of your environment. A sibling folder is in none of those, which is why the import fails. sys.path.append makes it work; installing your project as a package makes it work correctly.

Python Import From Another Directory: Why It Fails and What to Do

This is the single most common structural confusion in Python, and the popular answer — appending to sys.path — is a workaround that quietly causes problems later. Here is why it fails, what the workaround costs, and the small amount of setup that removes the question permanently.

Table of contents

Why the import fails

project/
├── src/
│   └── app.py
└── utils/
    └── helpers.py
# src/app.py
from utils.helpers import clean   # ModuleNotFoundError: No module named 'utils'

Running python src/app.py puts src/ on sys.path — not the project root. From src/, there is no utils to find.

import sys
for p in sys.path:
    print(p)

# ''  (or the script's directory)
# /usr/lib/python312.zip
# /usr/lib/python3.12
# /path/to/.venv/lib/python3.12/site-packages

sys.path[0] is the directory of the script you invoked, not the directory you invoked it from. That distinction explains most of the confusion — moving to the project root and running the same command changes nothing, because the script’s location is what counts.

One exception worth knowing: python -m package.module puts the current working directory on the path instead. That single difference resolves a surprising number of these problems on its own.

The sys.path workaround, and what it costs

# The answer you will find everywhere
import sys
from pathlib import Path

sys.path.append(str(Path(__file__).resolve().parent.parent))

from utils.helpers import clean   # now works

It works. The problems are real but deferred, which is why it stays popular:

  • Import order becomes load-bearing. The sys.path line must execute before the import, which violates every style guide and breaks when a formatter reorders imports.
  • Linters and type checkers cannot follow it. Your editor shows unresolved imports; mypy and pyright cannot analyse the module.
  • It is per-entry-point. Every script needs the same incantation, and they drift.
  • Shadowing risk. Adding a directory to the front of sys.path can shadow a standard library or installed module with a local file. A local logging.py breaks things in ways that take a long time to diagnose.
  • It does not survive packaging. The relative path is wrong once the code is installed or containerised.

PYTHONPATH is the same workaround at the environment level — no code change, same limitations, plus it is invisible to anyone reading the source:

PYTHONPATH=/path/to/project python src/app.py
export PYTHONPATH="${PYTHONPATH}:/path/to/project"

It is acceptable in a container where you control the environment completely. It is a poor default for a project other people will run.

The correct fix: make it a package

The proper answer is to declare your project as a package and install it into your environment in editable mode. It takes one file and one command.

project/
├── pyproject.toml
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── app.py
│       └── utils/
│           ├── __init__.py
│           └── helpers.py
└── tests/
    └── test_helpers.py
# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = []

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["src"]
python -m venv .venv
source .venv/bin/activate
pip install -e .        # editable: edits take effect immediately

Now imports are absolute and work identically from anywhere — a script, a test, a notebook, a container:

from myapp.utils.helpers import clean

Editable installs do not copy your code. They add a path entry pointing at your source, so changes take effect without reinstalling. You get the correctness of an installed package with the convenience of working in place.

The src/ layout is deliberate. Because the package is not at the project root, you cannot accidentally import it from the working directory — which means your tests exercise the installed package, the same thing your users get.

Relative imports, and when they work

Within a package, relative imports express intent clearly:

# src/myapp/app.py
from .utils.helpers import clean       # sibling subpackage
from . import config                   # sibling module
from ..other import thing              # parent package

The rule that trips everyone: relative imports only work when the module is being run as part of a package. Executing the file directly makes it __main__, which has no package context.

python src/myapp/app.py
# ImportError: attempted relative import with no known parent package

python -m myapp.app
# works -- module is run within its package

Use python -m package.module to run code inside a package. It is the answer to a large share of these errors and it is barely mentioned in most tutorials.

For a script users will run, define a console entry point and stop worrying about invocation:

# pyproject.toml
[project.scripts]
myapp = "myapp.app:main"
pip install -e .
myapp          # runs myapp.app:main from anywhere

Making tests find your code

The second-most-common version of this problem is tests/ not being able to import the application.

With the package installed via pip install -e ., it simply works — pytest imports myapp the same way anything else does, with no configuration.

# tests/test_helpers.py
from myapp.utils.helpers import clean

def test_clean():
    assert clean('  hi  ') == 'hi'
# pyproject.toml -- keeps pytest predictable
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]     # only needed if you skip the editable install

conftest.py with sys.path manipulation is the workaround people reach for and it should not be necessary. If your tests need path surgery to find your code, the packaging is not right, and the same problem will reappear in your container build.

Add __init__.py to test directories only if you need test modules with colliding names across directories. Otherwise leave them out — pytest handles it.

What this looks like in a deployment

The packaging question becomes concrete at deploy time, because a container has none of the assumptions your laptop had.

FROM python:3.12-slim
WORKDIR /app

# Dependencies first, so this layer caches
COPY pyproject.toml ./
COPY src/ ./src/
RUN pip install --no-cache-dir .

# Absolute imports work because the package is installed
CMD ["python", "-m", "myapp.app"]

Compare that with the sys.path.append version, where the relative path from __file__ no longer describes the container layout and the application fails at startup with an import error that worked fine locally.

This is the practical argument for doing it properly. A project that imports correctly runs the same way on a laptop, in CI, and in production. On RunxBuild, Python services build from the repository and the build log shows the install step, so a packaging mistake surfaces at build time rather than as a container that starts and immediately exits — the Python services documentation covers that stage.

How this fits the rest of the stack

The import fails because Python searches sys.path and your sibling directory is not in it. sys.path.append fixes the symptom and breaks your linters, your tests, and your container build. A pyproject.toml and pip install -e . take five minutes and remove the question permanently. If you are deploying a Python service and want the build and runtime costs separately, the RunxBuild hosting calculator shows them apart.

Useful related references:

FAQ

Why can Python not import from a sibling directory?

Python only searches sys.path, which contains the directory of the script you ran, PYTHONPATH entries, and installed packages. A sibling folder is in none of those. Note that sys.path[0] is the script’s directory, not your working directory.

Is sys.path.append a bad practice?

It works but has real costs: linters and type checkers cannot follow it, import order becomes significant, every entry point needs its own copy, and the relative path breaks once the code is containerised. Prefer installing the project as a package.

How do I make my project importable properly?

Add a pyproject.toml declaring the package, use a src/ layout, and run pip install -e . in your virtual environment. Absolute imports then work identically from scripts, tests, notebooks, and containers.

Why do relative imports fail when I run the file directly?

Running a file directly makes it main, which has no parent package, so there is nothing for a relative import to be relative to. Use python -m package.module instead.

How do I make pytest find my source code?

Install the package with pip install -e . and imports just work with no configuration. If tests need sys.path manipulation in conftest.py, the packaging is wrong and the same problem will reappear in your deployment.

#python import from another directory#sys.path#python packages#pyproject.toml#editable install