PythonProject Structure

Project Structure

As a Python project grows past a single script, how you lay out files on disk starts to matter. A good structure separates application code from tests and configuration, makes imports behave predictably, and makes the project installable the same way whether you're developing it locally or shipping it to a user.

src-layout vs flat-layout

There are two common ways to lay out a package:

Layout

Structure

Trade-off

Flat layout

mypackage/ sits directly at the repository root, next to pyproject.toml

Simple, but Python can accidentally import the local uninstalled copy instead of the installed one, hiding packaging bugs

src layout

mypackage/ lives inside src/, i.e. src/mypackage/

Forces you to install the package (even in editable mode) before it can be imported, which catches missing-dependency and packaging mistakes early

The src-layout is generally preferred for anything you intend to install or distribute, precisely because it removes the ambiguity: there is no version of your package importable by just running Python from the repo root, so the only way to run your code is the same way your users eventually will — through an installed package. Many popular open-source libraries and the official Python Packaging Authority guides now recommend it as the default.

An example layout

Recommended src-layout

Text
my-project/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       ├── core.py
│       └── utils.py
├── tests/
│   ├── test_core.py
│   └── test_utils.py
├── pyproject.toml
├── README.md
└── .gitignore

Application code lives under src/mypackage/, entirely separate from tests/, which mirrors the package's module names so it's obvious what each test file covers. pyproject.toml describes the package and its dependencies, README.md documents it for humans, and .gitignore keeps build artifacts (__pycache__/, dist/, .venv/) out of version control.

The role of __init__.py

An __init__.py file marks a directory as a regular Python package, letting you write from mypackage.core import thing. It can simply be empty — its presence alone is enough — or you can use it to control what a package exposes at its top level, re-exporting selected names so callers get a clean public interface:

src/mypackage/__init__.py

Python
from mypackage.core import process_data
from mypackage.utils import slugify

__all__ = ["process_data", "slugify"]

With that in place, users can write from mypackage import process_data instead of reaching into mypackage.core directly, which gives you room to reorganize internal modules later without breaking anyone's imports.

Tests live alongside, not inside
  • Keep tests/ as a sibling of src/, never inside the installable package, so test code is never accidentally shipped or imported by consumers.

  • Mirror module names in test file names (core.pytest_core.py) so it is obvious what is covered.

  • Use pytest's test discovery (files named test_*.py or *_test.py) rather than inventing a custom runner.

Start simple, grow as needed
You do not need `src/`, a `tests/` folder, and a full `pyproject.toml` for a 30-line script — a single `.py` file is the right structure for that. Introduce a package layout, a `tests/` directory, and packaging metadata when the project actually grows enough to need them: multiple modules, other people depending on it, or a real install/distribution story. Over-structuring a small script adds ceremony without adding value.