Skip to content
AstroPaper
Go back

Python Project Folder Structure — The Modern Standard

Edit page

Modern Python projects follow a well-established layout that balances import safety, installability, and tooling support. This note covers the standard structure for applications and explains the reasoning behind each layer.

The Structure at a Glance

my-app/
├── src/
│   └── my_app/
│       ├── __init__.py
│       ├── main.py
│       ├── api/
│       ├── models/
│       └── services/
├── tests/
│   └── conftest.py
├── pyproject.toml
└── README.md

Naming Convention: Hyphens vs Underscores

The project root directory uses my-app (hyphens), while the Python package folder uses my_app (underscores). This is intentional:

Why src/my_app/ and Not Just src/?

This is the most common question. The answer is about namespacing.

If code lives directly in src/:

src/
├── main.py
└── utils.py

Imports are flat and ambiguous:

import main
import utils

These names can clash with any other installed library that exports main or utils.

With src/my_app/:

src/
└── my_app/
    ├── main.py
    └── utils.py

Every import is namespaced under the package:

from my_app import main
from my_app.utils import something

There is no ambiguity about which package my_app.utils belongs to.

Benefits Summary

ConcernWhy it matters
No name clashesmy_app.utils won’t conflict with another library’s utils
Clear ownershipAny import reveals which package it comes from
Installable as a packagepyproject.toml maps to src/my_app and installs cleanly
Multiple packagessrc/ can hold my_app/ and my_app_cli/ side by side

Why the src/ Layer at All?

src/ is a container that keeps packages out of the project root. Without it, Python can accidentally import code directly from the project root during development — before it is installed — which can mask packaging bugs and import errors.

The src/ layout forces the package to be installed (even in editable mode via pip install -e .) before it can be imported in tests and scripts. This means your test environment matches what users of the package actually get.

Package vs Application Layout

The src/my_app/ convention applies to both libraries and applications, but the internal structure differs:

Library — exposes a public API, keeps internals flat:

src/my_lib/
├── __init__.py    ← public API re-exports
├── core.py
└── utils.py

Application — groups by feature or layer:

src/my_app/
├── __init__.py
├── main.py        ← entry point
├── api/           ← HTTP handlers or CLI commands
├── models/        ← data models
└── services/      ← business logic

Modern Tooling

graph TD
    A[pyproject.toml] --> B[uv / poetry]
    A --> C[ruff]
    A --> D[mypy / pyright]
    A --> E[pytest]
    B --> F[uv.lock / poetry.lock]
ToolRole
pyproject.tomlSingle config file for metadata, deps, and tool settings
uvFast package manager and project scaffolder
ruffFormatter + linter (replaces black, flake8, isort)
mypy / pyrightStatic type checking
pytestTest runner with tests/conftest.py for shared fixtures

Running uv init --app scaffolds exactly this layout automatically — the src/my_app/ pattern is baked into the tool.

Quick Reference

my-app/               ← repo root (hyphens OK)
├── src/              ← isolates packages from project root
│   └── my_app/       ← the actual Python package (underscores)
│       └── __init__.py
├── tests/            ← sits at project root, not inside src/
├── pyproject.toml    ← replaces setup.py; all config lives here
└── .python-version   ← pins Python version for pyenv/uv

The src/ layer provides import isolation. The named package folder provides namespacing. Together they form the de facto standard for new Python projects in 2024 and beyond.


Edit page
Share this post on:

Previous Post
SQLite Explained — Engine, File, and the Whole Picture
Next Post
ECMAScript, JavaScript, and the Node.js Package Manager Ecosystem