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:
- The src layout requires installing the project to run its code. The flat layout does not.
- 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.
- 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.pyandnoxfile.pyon 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.
| Step | Flat layout | src layout |
|---|---|---|
python -m pytest with nothing installed | Passes: the working copy is on the import path | ModuleNotFoundError: No module named 'mypkg' |
Plain pytest with nothing installed | Collection error (no import path for the root) | ModuleNotFoundError |
Wheel installed in a fresh venv, pytest run from the project root | Passes: the working copy shadows the broken install | Fails: No module named 'mypkg.sub' |
| Same venv, importing from another directory | ModuleNotFoundError: No module named 'mypkg.sub' | ModuleNotFoundError: No module named 'mypkg.sub' |
Editable install, then pytest | Passes | Passes |
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:
| Command | Files created |
|---|---|
uv init demo | pyproject.toml, README.md, .python-version, src/demo/__init__.py |
uv init --package demo | the same layout |
uv init --lib demo | the same, plus src/demo/py.typed |
uv init --no-package demo | pyproject.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
| Flat | src | |
|---|---|---|
| Needs install before import | No | Yes |
| Catches packaging mistakes | Not when run from the project root | Yes, if you test a regular install |
| Extra files importable | Possible, in some editable installs | No |
| Typical use | Apps, scripts | Libraries, published packages |
Related reading
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.