How to structure a large Python CLI project
Adopt the Strict src/ Layout
Large CLI applications fail when package imports collide with local module names. Isolate your codebase using a strict src/ layout to prevent accidental shadowing during testing. This architecture aligns with established Modern Python CLI Frameworks & Architecture standards. Place all business logic inside src/your_tool/. Keep the repository root strictly for configuration and CI pipelines.
your_tool/
├── pyproject.toml
├── README.md
├── tests/
└── src/
└── your_tool/
├── __init__.py
├── cli.py
├── commands/
│ ├── __init__.py
│ ├── deploy.py
│ └── analyze.py
└── core/
├── __init__.py
└── config.py
Configure Entry Points in pyproject.toml
Avoid manual if __name__ == '__main__' blocks in production tooling. Define console_scripts in your build configuration to delegate command parsing to the packaging system. This approach is mandatory when Structuring Multi-Command Python CLIs. It guarantees correct sys.path resolution and enables seamless virtual environment activation.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "your-tool"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = ["typer>=0.9.0", "rich>=13.0.0"]
[project.scripts]
your-tool = "your_tool.cli:app"
Minimal Command Router (Python 3.10+)
Use modern type hints and a centralized router to manage execution flow. The following pattern leverages typer for automatic help generation and deterministic subcommand dispatch. Keep cli.py intentionally thin. Delegate heavy logic to commands/ modules to maintain strict separation of concerns. This simplifies isolated unit testing across your codebase.
import typer
from your_tool.commands import deploy, analyze
app = typer.Typer(add_completion=False)
app.command()(deploy.main)
app.command()(analyze.main)
if __name__ == "__main__":
app()
Resolve ModuleNotFoundError During Local Execution
Developers frequently encounter ModuleNotFoundError when executing scripts directly via python src/your_tool/cli.py. Python's import system does not recognize src/ as a package root in this context. Run pip install -e . at the repository root to resolve this. Invoke the CLI exclusively via its registered entry point: your-tool deploy --env prod. This forces Python to resolve imports through installed package metadata.