Good project structure makes code easier to navigate and maintain. Here's how to organize Python projects.

Modules vs Packages

Module: A single .py file

# utils.py is a module
import utils

Package: A directory with __init__.py

mypackage/
├── __init__.py
├── core.py
└── helpers.py
import mypackage
from mypackage import core

Basic Project Structure

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

The src Layout

Putting code in src/ prevents accidental imports from the project root:

# Without src/
myproject/
├── mypackage/     # Might import local instead of installed
└── tests/

# With src/
myproject/
├── src/
│   └── mypackage/  # Must be installed to import
└── tests/

init.py

Controls what's exposed when importing the package:

# mypackage/__init__.py
 
# Make these available at package level
from .core import process
from .utils import helper
 
# Package metadata
__version__ = "1.0.0"
__all__ = ["process", "helper"]

Now users can:

from mypackage import process  # Instead of mypackage.core.process

Subpackages

Organize large projects into subpackages:

mypackage/
├── __init__.py
├── api/
│   ├── __init__.py
│   ├── routes.py
│   └── middleware.py
├── models/
│   ├── __init__.py
│   ├── user.py
│   └── order.py
└── utils/
    ├── __init__.py
    └── helpers.py

Import paths:

from mypackage.api import routes
from mypackage.models.user import User

Relative Imports

Within a package, use relative imports:

# mypackage/api/routes.py
 
# Relative imports (preferred within package)
from ..models import User
from ..utils.helpers import format_date
 
# Absolute imports (also work)
from mypackage.models import User

pyproject.toml

Modern Python projects use pyproject.toml:

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
 
[project]
name = "mypackage"
version = "1.0.0"
description = "My awesome package"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
    "requests>=2.28.0",
    "click>=8.0.0",
]
 
[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=22.0.0",
    "mypy>=0.990",
]
 
[project.scripts]
mycommand = "mypackage.cli:main"
 
[tool.setuptools.packages.find]
where = ["src"]

Installing for Development

Install in editable mode:

pip install -e ".[dev]"

Changes to your code are immediately available without reinstalling.

Entry Points (CLI)

Create command-line tools:

# src/mypackage/cli.py
import click
 
@click.command()
@click.argument("name")
def main(name):
    """Greet someone."""
    click.echo(f"Hello, {name}!")
 
if __name__ == "__main__":
    main()
# pyproject.toml
[project.scripts]
greet = "mypackage.cli:main"

After installing:

greet World  # Hello, World!

main.py

Make packages runnable with python -m:

# src/mypackage/__main__.py
from .cli import main
 
if __name__ == "__main__":
    main()
python -m mypackage World  # Hello, World!

Organizing by Feature

For larger projects, organize by feature instead of type:

mypackage/
├── __init__.py
├── users/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   └── services.py
├── orders/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   └── services.py
└── shared/
    ├── __init__.py
    └── database.py

Configuration

Keep config separate:

mypackage/
├── __init__.py
├── config.py      # Configuration classes
├── settings.py    # Load from environment
└── core.py
# config.py
from pydantic import BaseSettings
 
class Settings(BaseSettings):
    database_url: str
    api_key: str
    debug: bool = False
 
    class Config:
        env_file = ".env"

What Goes Where

TypeLocation
Package codesrc/mypackage/
Teststests/
Documentationdocs/
Configurationpyproject.toml, .env
Scriptsscripts/
Type stubssrc/mypackage/py.typed

Quick Reference

myproject/
├── src/
│   └── mypackage/
│       ├── __init__.py      # Package exports
│       ├── __main__.py      # python -m entry
│       ├── core.py          # Main functionality
│       ├── cli.py           # Command-line interface
│       └── utils.py         # Helpers
├── tests/
│   └── test_core.py
├── pyproject.toml           # Package metadata
├── README.md
└── .gitignore

Good structure scales. Start simple, add subpackages as complexity grows, and keep related code together.

React to this post: