How do you structure a Python project for a distributable package?
Quick Answer
The modern recommended layout is the **`src` layout**: application code lives under `src/mypackage/`, separate from tests, docs, and config — this forces tests to import the package as it would actually be *installed*, rather than accidentally picking up the working directory's uninstalled source (a common bug with the older "flat" layout where the package sits directly next to `setup.py`). Combine it with `pyproject.toml` for metadata and `[project.scripts]` for CLI entry points.
Detailed Answer
The src layout
myproject/
├── pyproject.toml
├── README.md
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── core.py
│ └── cli.py
└── tests/
├── test_core.py
└── test_cli.py
[project]
name = "mypackage"
[project.scripts]
mycli = "mypackage.cli:main"
mypackage lives under src/, not at the project root next to
pyproject.toml — this is deliberate, not just tidiness.
Why src layout beats the flat layout
# Flat layout (older convention) -- package sits at the project root
myproject/
├── setup.py
├── mypackage/ <- right next to setup.py
│ └── __init__.py
└── tests/
With a flat layout, running tests from the project root can accidentally
import the uninstalled source directly (since the current directory
is often on sys.path), even if the package was never properly
pip install-ed — tests can pass locally while the actual built and
installed package is broken (e.g., missing a data file that wasn't
included in the package manifest). The src layout physically prevents
this: mypackage isn't importable at all unless it's actually installed
(even via pip install -e . for local development), so tests exercise
the real installed package, matching what end users will actually get.
Editable installs for local development
pip install -e . # "editable install" -- installs the package, but pointing at src/
An editable install lets you edit src/mypackage/*.py and immediately
see changes reflected without reinstalling, while still going through the
proper import mechanism (not a sys.path accident) — the standard way to
develop against the src layout locally.
Entry points for CLI tools
[project.scripts]
mycli = "mypackage.cli:main"
After installation, this makes mycli available as a real shell command
that calls mypackage.cli.main() — the standard way to ship a
command-line tool as part of a package, rather than telling users to run
python -m mypackage.cli manually.
Keeping tests, docs, and config out of the shipped package
tests/ <- not shipped to end users, only needed for development
docs/ <- likewise
pyproject.toml <- build/dev config, not shipped as importable code
Separating these from src/mypackage/ keeps the actual distributed
package (the wheel end users pip install) minimal — containing only
what's needed to run the library/application, not the development
tooling around it.
Interview-ready summary: The src layout puts the actual package
under src/mypackage/, separate from tests/docs/config, specifically to
prevent tests from accidentally importing uninstalled source instead of
the real installed package — combined with pyproject.toml for metadata
and [project.scripts] for CLI entry points, it's the standard modern
structure for a distributable Python project.