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.
myproject/
├── myproject/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── models.py
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── test_utils.py
├── pyproject.toml
└── README.mdThe 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:
myproject/
├── src/
│ └── myproject/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
│ ├── test_core.py
│ └── test_utils.py
├── pyproject.toml
└── README.mdThe 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.
[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:
[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:
[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.
python -m venv .venv
source .venv/bin/activate # Linux and macOS
.venv\Scripts\activate # WindowsThe .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:
uv venv # create .venv
uv pip install -e ".[dev]" # install project in editable modeThe -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:
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
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.
Frequently Asked Questions
What is the best directory structure for a Python project?
Should I use the src-layout or the flat layout?
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.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.