Building command-line interfaces in Python. Many utilities earn their keep as scripts you invoke from cron, Make, or your own fingers.
argparse – batteries included
The standard library module argparse is enough for most small tools.
import argparse
from pathlib import Path
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Normalise emails in a CSV file")
parser.add_argument("input", type=Path, help="CSV file to read")
parser.add_argument("-o", "--output", type=Path, help="write result here")
parser.add_argument("--dry-run", action="store_true", help="show changes without writing")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
print(args.input, args.output, args.dry_run)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Return exit codes
Return 0 on success, non-zero on failure. Shell scripts and CI depend on it.
try:
run_import(args)
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
return 0
Click for nicer UX
pip install click
import click
from pathlib import Path
@click.command()
@click.argument("input", type=click.Path(exists=True, path_type=Path))
@click.option("--output", "-o", type=click.Path(path_type=Path))
@click.option("--dry-run", is_flag=True)
def main(input: Path, output: Path | None, dry_run: bool) -> None:
click.echo(f"Reading {input}")
if dry_run:
click.echo("Dry run - no files changed")
if __name__ == "__main__":
main()
Entry points from packaging
Wire your CLI through pyproject.toml so users run mytool after pip install.
[project.scripts]
mytool = "mytool.cli:main"
Keep business logic out of the parser
Parse arguments in the CLI layer; put work in importable functions you can test with pytest without spawning a subprocess.
# mytool/importer.py
def run_import(path: Path, dry_run: bool = False) -> int:
...
# mytool/cli.py
def main(argv=None) -> int:
args = build_parser().parse_args(argv)
return run_import(args.input, dry_run=args.dry_run)
Next: mini project – a CLI utility that ties packaging, config, HTTP or file I/O, and tests together.

