Stage 7 · Master
Automation Scripts
Building CLI Tools
Click, argparse, Typer — build polished CLIs with help text and subcommands.
argparse Fundamentals
argparse is the standard library CLI framework. It handles arguments, flags, help text, and subcommands. Use it for scripts where adding a dependency is not worth it.
import argparse
import sys
def main():
parser = argparse.ArgumentParser(
description="Check health of services",
prog="healthcheck",
)
parser.add_argument("hosts", nargs="+", help="Hostnames to check")
parser.add_argument("-p", "--port", type=int, default=8080, help="Port to connect to (default: 8080)")
parser.add_argument("-t", "--timeout", type=int, default=5, help="Timeout in seconds (default: 5)")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
for host in args.hosts:
status = check_host(host, args.port, args.timeout)
if args.json:
import json
print(json.dumps({"host": host, "status": status}))
else:
print(f"{host}: {status}")
def check_host(host: str, port: int, timeout: int) -> str:
return "healthy" # placeholder
if __name__ == "__main__":
main()argparse generates help text automatically. Run with --help to see all options. nargs='+' accepts one or more positional arguments.
Click Framework
Click is the most popular third-party CLI framework. It uses decorators, handles complex argument types, and supports subcommands cleanly.
import click
@click.group()
@click.version_option(version="1.0.0")
def cli():
"""DevOps toolkit — manage services, deployments, and monitoring."""
pass
@cli.command()
@click.argument("hosts", nargs=-1, required=True)
@click.option("--port", "-p", default=8080, help="Port to check")
@click.option("--timeout", "-t", default=5, help="Timeout in seconds")
@click.option("--output", "-o", type=click.Choice(["text", "json"]), default="text")
def check(hosts: tuple[str], port: int, timeout: int, output: str):
"""Check health of one or more services."""
for host in hosts:
click.echo(f"Checking {host}:{port}...")
@cli.command()
@click.argument("service")
@click.option("--replicas", "-r", default=1, type=int)
def deploy(service: str, replicas: int):
"""Deploy a service with the specified number of replicas."""
click.echo(f"Deploying {service} with {replicas} replicas...")
if __name__ == "__main__":
cli()Click groups let you build multi-command CLIs like git or docker. Each @cli.command() becomes a subcommand. Click handles type conversion, validation, and help text automatically.
Typer — Modern CLI
import typer
from typing import Optional
app = typer.Typer(help="Deployment automation tool")
@app.command()
def deploy(
service: str = typer.Argument(help="Service name"),
env: str = typer.Option("--env", "-e", help="Target environment"),
replicas: int = typer.Option(1, "--replicas", "-r", help="Number of replicas"),
dry_run: bool = typer.Option(False, "--dry-run", help="Preview without applying"),
):
"""Deploy a service to the target environment."""
if dry_run:
typer.echo(f"[DRY RUN] Would deploy {service} to {env} with {replicas} replicas")
else:
typer.echo(f"Deploying {service} to {env} with {replicas} replicas")
@app.command()
def rollback(service: str, revision: Optional[int] = None):
"""Roll back a service to a previous revision."""
typer.echo(f"Rolling back {service} to revision {revision or 'previous'}")
if __name__ == "__main__":
app()Typer uses type hints and default values to define CLI arguments. It generates help text, validates inputs, and supports shell completion. It is built on Click but with less boilerplate.
Rich Output
from rich.console import Console
from rich.table import Table
from rich.progress import track
import time
console = Console()
# Styled text
console.print("[bold green]Deployment successful![/bold green]")
console.print("[red]Error:[/red] Service unreachable")
# Tables
table = Table(title="Service Status")
table.add_column("Service", style="cyan")
table.add_column("Status", style="green")
table.add_column("Replicas", justify="right")
table.add_row("web-01", "Running", "3")
table.add_row("api-01", "Running", "2")
table.add_row("db-01", "Degraded", "1")
console.print(table)
# Progress bar
for step in track(range(100), description="Deploying..."):
time.sleep(0.01) # Simulate workrich replaces print() with styled, structured output. Tables, progress bars, and colored text make your CLI output readable and professional.
Progress Bars
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
def deploy_with_progress(services: list[str]) -> None:
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
) as progress:
task = progress.add_task("Deploying...", total=len(services))
for service in services:
progress.update(task, description=f"Deploying {service}...")
deploy_service(service)
progress.advance(task)
deploy_with_progress(["web-01", "web-02", "api-01"])rich.progress supports spinners, bars, and custom renderers. Use it for any operation that takes more than 2 seconds. Users should never wonder if the script is stuck.
Packaging CLI Tools
[project]
name = "mydevops"
version = "1.0.0"
dependencies = ["click", "rich", "httpx"]
[project.scripts]
mydevops = "mydevops.cli:main"
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.backends._legacy:_Backend"The [project.scripts] section creates a command-line entry point. After pip install, the 'mydevops' command is available everywhere. This is how tools like black, ruff, and httpx ship as CLI commands.
argparse works but is verbose. Click and Typer give you better ergonomics, automatic help formatting, and shell completion. Typer is best for new projects; Click is best if you need full control.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.