Organize Python Projects

Learn how to structure a Python project from a single script to a maintainable package with clear directory layout, configuration, and dependency management.

7 min read

Organizing a Python project means deciding where every file lives, how imports work, how dependencies are declared, and how tests fit into the structure. A single script needs no organization, but as soon as you split code across multiple files, you need a deliberate layout.

The goal is a structure that any Python developer can understand quickly.

When someone clones your project, they should immediately know where the source lives.

This article builds on concepts from Python modules and packages explained.

The single-script project

Every Python project starts as one file. A script that renames files, processes a CSV, or scrapes a web page does not need a package structure. Keep the script focused, and when it grows past 200 lines or when you need to reuse logic across multiple scripts, it is time to add structure.

The multi-file project

The first step beyond a single script is splitting code into modules. A module is a .py file. When you have a few related modules, group them in a package directory with an init.py file.

texttext
myproject/
├── myproject/
│   ├── __init__.py
│   ├── core.py
│   ├── utils.py
│   └── models.py
├── tests/
│   ├── __init__.py
│   ├── test_core.py
│   └── test_utils.py
├── pyproject.toml
└── README.md

The inner myproject/ directory is the Python package. The outer myproject/ directory is the project root. This distinction matters because Python imports search from sys.path, not from the directory your terminal happens to be in.

The init.py file can be empty. Its presence alone tells Python that the directory is a package.

The src-layout for installable packages

For a project meant to be installed with pip, use the src-layout. Instead of putting the package directory directly in the project root, put it inside a src/ directory:

texttext
myproject/
├── src/
│   └── myproject/
│       ├── __init__.py
│       ├── core.py
│       └── utils.py
├── tests/
│   ├── test_core.py
│   └── test_utils.py
├── pyproject.toml
└── README.md

The src-layout has one critical benefit: when you run tests, Python imports the installed version of your package, not the source directory. This catches packaging mistakes early. Without the src-layout, tests can pass during development but fail after installation because a file was not included in the package.

pyproject.toml: the single config file

Modern Python projects use pyproject.toml as the central configuration file. It replaces the old setup.py, setup.cfg, and individual tool config files.

tomltoml
[build-system]
requires = ["setuptools>=75", "wheel"]
build-backend = "setuptools.build_meta"
 
[project]
name = "myapp"
version = "0.1.0"
description = "A short description of the project"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
dependencies = [
    "requests>=2.32",
    "pydantic>=2.10",
]

The [project] table defines your package metadata: name, version, Python requirement, and runtime dependencies. This is the minimum information pip needs to install your package.

Separate optional-dependencies keeps development tools out of production installs. Add entry points under project.scripts for command-line tools:

tomltoml
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.11"]
test = ["pytest>=8", "pytest-cov>=6"]
 
[project.scripts]
myapp = "myapp.cli:main"

Tool configuration for linters and test runners is also centralized in the same pyproject.toml file. This removes the need for separate .flake8 or .isort.cfg files:

tomltoml
[tool.ruff]
line-length = 88
target-version = "py311"
 
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
 
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]

Virtual environments

Each project gets its own virtual environment. A virtual environment is an isolated Python installation that contains the project's dependencies without affecting the system Python or other projects.

texttext
python -m venv .venv
source .venv/bin/activate      # Linux and macOS
.venv\Scripts\activate         # Windows

The .venv/ directory should be added to .gitignore. Never commit it to version control.

Modern tools like uv combine virtual environment management with fast dependency resolution:

texttext
uv venv                        # create .venv
uv pip install -e ".[dev]"     # install project in editable mode

The -e flag means editable mode. Source changes take effect immediately.

Tests: mirror the package structure

Put tests in a top-level tests/ directory that mirrors the package layout. Each source module gets a corresponding test file. This one-to-one mapping makes it easy to find the tests for any piece of code.

Configuration and secrets

Application configuration should live in a dedicated module, not scattered through the codebase. Use a config.py that reads from environment variables. Never hardcode secrets, API keys, or database URLs:

pythonpython
import os
 
DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://localhost/myapp")
SECRET_KEY = os.environ["SECRET_KEY"]        # required, fails fast if missing
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"

Use a .env file for local development and add .env to .gitignore. For production, set real environment variables through your deployment platform.

Documentation and README

Every project needs a README.md at the root. A good README answers: what does this project do, how do I install it, how do I run it, how do I run the tests, and how do I contribute. Keep the README short enough to scan in 30 seconds.

Detailed documentation belongs in a docs/ directory. For habits that keep the code itself easy to follow as the project grows, see Write Clean and Readable Python Code.

Project growth: when to add what

Not every project needs the full structure from day one. Add pieces as the project grows.

A single script stays as one file. A few related modules get a package directory with pyproject.toml and a tests/ directory. An installable library adopts the src-layout with docs.

The key principle is to start simple and add structure only when the current layout becomes a problem. Avoid creating directories for future features. The utils/ directory full of random helper functions is the classic trap. If a function is useful enough to be shared, put it in a focused module with a descriptive name.

Rune AI

Rune AI

Key Insights

  • Use pyproject.toml as the single configuration file for modern Python projects.
  • Adopt the src-layout for installable packages to prevent import confusion.
  • Keep tests in a top-level tests/ directory that mirrors the package structure.
  • Use virtual environments to isolate dependencies per project.
  • Start simple and add structure only when the project needs it.
RunePowered by Rune AI

Frequently Asked Questions

What is the best directory structure for a Python project?

For libraries and larger applications, use the src-layout with a src/ directory containing your package, a tests/ directory at the top level, and a pyproject.toml file for configuration. For simple scripts, a single .py file is fine.

Should I use the src-layout or the flat layout?

The src-layout (with a src/ directory) is recommended for installable packages because it prevents accidentally importing the package from the project root during development. The flat layout is simpler but can cause subtle import bugs.

Conclusion

A well-organized Python project is easier to navigate, easier to test, and easier for other developers to contribute to. Start simple and add structure as the project grows. A single script does not need a full package layout, but by the time you have three or four files, it is time to organize.