How do I structure a Python project so my imports work?
Short answer
Put your code in a package folder, add a pyproject.toml, and install it in editable mode with pip install -e. This makes imports work from anywhere instead of depending on which directory you ran from.
The short version
Put your code in a named package folder, describe the project in a pyproject.toml, and install it in editable mode with pip install -e .. Imports then work from any directory. The alternative, relying on the current working directory, is the cause of most "ModuleNotFoundError" frustration.
A workable layout
myproject/
pyproject.toml
src/
analysis/
__init__.py
cleaning.py
reporting.py
tests/
test_cleaning.py
The analysis folder is the package. The __init__.py marks it as one (it can be empty). Tests live outside the package, alongside it.
The minimal pyproject.toml
[project]
name = "analysis"
version = "0.1.0"
dependencies = ["pandas", "numpy"]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
This is enough to describe the project and its dependencies.
Install it in editable mode
From the project root, inside your virtual environment:
pip install -e .
Editable mode links the package into the environment rather than copying it, so your edits take effect immediately with no reinstall. Now from analysis.cleaning import normalise works from anywhere, including from tests and notebooks.
Why this fixes the import errors
Without installation, Python only finds your modules if it happens to be looking in the right directory, which depends on where you ran the command from. That is why a script works in the terminal but fails in a notebook, or works for you but not for a colleague. Installing the package puts it on the import path properly, so the location you run from stops mattering.
The hacks to stop using
Two workarounds are common and both are bad.
import sys
sys.path.append("../src") # fragile, depends on the working directory
And relying on relative paths to data files, which break the moment someone runs the script from elsewhere. Installing the package removes the need for both.
Absolute imports inside the package
Within your own code, import by the full package path rather than with bare relative names, which is clearer and less fragile.
from analysis.cleaning import normalise # explicit and unambiguous
Dependencies belong in the project file
Listing dependencies in pyproject.toml means anyone can recreate the environment from the project itself, rather than guessing from import statements. This is what makes the project reproducible for the next person, which is really the point of the whole exercise.
Tooling
Modern tools such as uv, poetry and hatch build on this same pyproject.toml standard and add lock files and faster installs. They are worth exploring once the basic structure makes sense, but plain pip install -e . with a pyproject.toml is enough to solve the import problem and is the foundation the others build on.
Enjoying these?
Go from piecing together answers to real fluency.
Our courses take you from looking things up to doing the work with confidence, step by step on real data.
Browse the courses