How do you structure a Python project for a distributable package?

6 minintermediatepackagingproject-structuresrc-layout

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.