src Layout vs Flat Layout for Python Projects

src layout vs flat layout for Python packages: how each behaves, the shadowing problem src avoids, a tested packaging mistake, and how uv init chooses.

Use the src layout when you build or publish a package, and a flat layout for small applications and scripts that are never installed. The difference is only where the importable code lives, but it changes what your tests actually test.

The two layouts

# flat layout                      # src layout
project/                           project/
├── pyproject.toml                 ├── pyproject.toml
├── mypkg/                         ├── src/
│   ├── __init__.py                │   └── mypkg/
│   └── core.py                    │       ├── __init__.py
└── tests/                         │       └── core.py
    └── test_core.py               └── tests/
                                       └── test_core.py

The Python Packaging User Guide lists three behaviour differences:

  1. The src layout requires installing the project to run its code. The flat layout does not.
  2. The src layout helps prevent accidental use of the in-development copy. The interpreter puts the current working directory first on the import path, so a package in the working directory wins over an installed one with the same name.
  3. The src layout stops an editable install importing files that were not meant to be importable. A flat layout puts files such as README.md, tox.ini, setup.py and noxfile.py on the import path of some editable installs.

The problem, reproduced

Here is the packaging mistake the guide describes. A package has a subpackage, mypkg/sub, and pyproject.toml lists packages = ["mypkg"], forgetting mypkg.sub. The wheel built from either layout contains only mypkg/__init__.py and mypkg/core.py, and core.py imports from the missing subpackage.

StepFlat layoutsrc layout
python -m pytest with nothing installedPasses: the working copy is on the import pathModuleNotFoundError: No module named 'mypkg'
Plain pytest with nothing installedCollection error (no import path for the root)ModuleNotFoundError
Wheel installed in a fresh venv, pytest run from the project rootPasses: the working copy shadows the broken installFails: No module named 'mypkg.sub'
Same venv, importing from another directoryModuleNotFoundError: No module named 'mypkg.sub'ModuleNotFoundError: No module named 'mypkg.sub'
Editable install, then pytestPassesPasses

The result in the third row is the point. With a flat layout the broken wheel looked fine because Python imported the source directory instead. With a src layout the tests run against the installed package and fail. The last row is a caveat: an editable install of the src layout still passes, because it points at your source tree. That is why the guide says an editable installation is typically used for development and a regular installation for testing.

Plain pytest behaves differently from python -m pytest in the flat layout here because only the python -m form adds the current directory to the import path.

Setting up a src layout

With the src directory in place, current build backends find the package without extra configuration. The current releases of Hatchling and setuptools both built a wheel containing mypkg/__init__.py, mypkg/core.py and the mypkg/sub subpackage from a src/mypkg tree with no [tool] configuration at all. The mistake above came from listing packages by hand. A minimal pyproject.toml:

[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"

[project]
name = "mypkg"
version = "0.1.0"
requires-python = ">=3.10"

For development, install the project in editable mode:

pip install -e .

To test what users will get, build the package and install it normally into a fresh environment:

python -m build --wheel
python -m venv test-env
test-env/bin/pip install dist/*.whl pytest
test-env/bin/python -m pytest

tox and nox automate that second step by installing the project into a fresh environment for every run. For choosing a build backend, see Hatch vs uv; the publishing steps are in how to publish a Python package to PyPI.

With uv

uv creates a src layout by default. Checked with uv 0.13.7:

CommandFiles created
uv init demopyproject.toml, README.md, .python-version, src/demo/__init__.py
uv init --package demothe same layout
uv init --lib demothe same, plus src/demo/py.typed
uv init --no-package demopyproject.toml, README.md, .python-version and a flat main.py

The first three define uv_build as the build backend. uv sync installs the project in editable mode: uv pip show reports an “Editable project location” pointing at the project directory. The uv documentation says a src layout “is particularly valuable when developing libraries”, because it isolates the library from any python invocation in the project root. Before uv 0.12 a new application had no build system by default.

Running a command-line tool from source

The src layout means a command-line interface cannot be run straight from the source tree. The PyPA guide suggests installing the package in development mode for testing, or, when that is impractical, prepending the package folder to sys.path from the package’s __main__.py so that python src/package works.

When a flat layout is fine

  • Scripts and applications that you run directly and never build or publish.
  • Notebooks and data projects with no installable package.
  • Small projects where the extra directory is only noise.

If an application grows, moving to a src layout later means moving the directory and updating pyproject.toml.

Trade-offs

Flatsrc
Needs install before importNoYes
Catches packaging mistakesNot when run from the project rootYes, if you test a regular install
Extra files importablePossible, in some editable installsNo
Typical useApps, scriptsLibraries, published packages

The layout decision goes with how you declare the project; see pyproject.toml explained and requirements.txt vs pyproject.toml. For code organisation inside the package, see how to write clean Python code and Python package managers compared.

Frequently Asked Questions

What is the src layout in Python?

In a src layout the code you want to be importable lives in a subdirectory, usually src/, for example src/mypkg/__init__.py, instead of the top-level directory next to pyproject.toml. The project must be installed before you can import it.

Which layout should I use for a Python package?

uv’s documentation says the src layout is particularly valuable when developing libraries, and uv init now creates a src layout by default. The Python Packaging User Guide describes the trade-offs rather than mandating one. A flat layout is fine for small scripts and applications that are never installed.

What does the src layout prevent?

It prevents your tests and scripts from importing the working copy from the current directory when you meant to test the installed package. Python puts the current directory first on the import path, so in a flat layout a packaging mistake such as a subpackage left out of the wheel can go unnoticed.

How do I install a src layout project for development?

Use an editable install: pip install -e . or uv sync, which installs the project in editable mode. The PyPA guide suggests an editable installation for development and a regular installation for testing.

Does uv create a src layout?

Yes. Since uv 0.12, uv init creates a src layout with a build system by default, and uv init —lib adds a py.typed marker. uv init —no-package creates the simpler flat layout with a main.py.