Iron Law
USE SOURCE LAYOUT (src/) FOR ALL SERVICES AND INSTALLABLE TOOLS — flat layout works for scripts but causes import confusion in larger projects and CI environments
Python Packaging
Project structure patterns for Python 3.14 services, internal tools, and CLI applications.
Scope
This skill covers the workspace-relevant subset of Python packaging:
pyproject.tomlstructure for FastAPI services and internal tools- Source layout vs flat layout decision
- CLI entry points (Click, argparse) for internal tooling
- Dynamic versioning
- Editable installs for development
Out of scope for this workspace: PyPI publishing, wheels, namespace packages, C extensions, private package index. The workspace builds services and internal tools, not distributable libraries.
Layout Decision
New project?
│
├── FastAPI service / internal service
│ └── Source layout: src/<package>/ ← default for all services
│
├── Internal CLI tool (deployment scripts, data tools)
│ └── Source layout with [project.scripts] entry point
│
└── Quick one-off script
└── Flat layout (single .py file or simple directory)
Reference File
Load resources/implementation-playbook.md for:
- Source layout pattern (Pattern 1) — full directory structure with init.py placement
- Flat layout pattern (Pattern 2) — when appropriate
- Full pyproject.toml template (Pattern 4) — with all modern metadata fields
- Dynamic versioning with
setuptools-scm(Pattern 5) - CLI with Click (Pattern 6) — entry points, commands, options
- CLI with argparse (Pattern 7) — subcommands, arguments
- Editable install (Pattern 16) —
uv pip install -e .for development
Key pyproject.toml Sections
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-service"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"fastapi>=0.135.2",
"pydantic>=2.0.0",
]
[project.scripts]
my-cli = "my_package.cli:main" # CLI entry point
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
CLI Entry Point Pattern
# src/my_tool/cli.py
import click
@click.group()
def main():
"""Internal deployment tool."""
pass
@main.command()
@click.argument("environment")
@click.option("--dry-run", is_flag=True)
def deploy(environment: str, dry_run: bool):
"""Deploy to environment."""
click.echo(f"Deploying to {environment} (dry_run={dry_run})")
if __name__ == "__main__":
main()
Install and run: uv pip install -e . then my-cli deploy staging --dry-run