Codeskill

Learn to code, step by step

CLI tools argparse/click

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.

PreviousPytest fundamentals