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 |
| Simple, but Python can accidentally import the local uninstalled copy instead of the installed one, hiding packaging bugs |
src layout |
| 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
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
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 ofsrc/, never inside the installable package, so test code is never accidentally shipped or imported by consumers.Mirror module names in test file names (
core.py→test_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.