Build Command Line Tools with argparse, Click, and Rich - Python CLI Tutorial #37
Video: Build Command Line Tools with argparse, Click, and Rich - Python CLI Tutorial #37 by Taught by Celeste AI - AI Coding Coach
Python CLI Tools: argparse, Click, Rich
argparse(stdlib) handles command-line parsing.Click(third-party) is friendlier with decorators and subcommands.Richadds colors, tables, progress bars. Pick argparse for one-shot scripts, Click for multi-command tools, Rich for any output worth styling.
A good CLI tool is self-documenting (--help shows everything), gives clear error messages, and feels native. Python has three layers of the puzzle.
argparse: the standard library option
import argparse
parser = argparse.ArgumentParser(
description="Analyze a text file",
epilog="Example: python script.py sample.txt --lines"
)
parser.add_argument("filename", help="file to analyze")
parser.add_argument("--lines", action="store_true", help="count lines")
parser.add_argument("--top", type=int, default=5, help="top N words")
parser.add_argument("--search", type=str, help="search for a word")
args = parser.parse_args()
# Use the parsed arguments
print(args.filename, args.lines, args.top)
Run it:
python script.py data.txt --lines --top 10
python script.py --help # auto-generated usage
add_argument:
- Positional: just a name (
"filename"). - Optional: starts with
--("--lines"). action="store_true"for boolean flags.type=int,type=floatfor conversion.default=for fallback.required=Truefor mandatory options.choices=["a", "b"]to restrict values.
argparse: --help is free
$ python script.py --help
usage: script.py [-h] [--lines] [--top TOP] [--search SEARCH] filename
Analyze a text file
positional arguments:
filename file to analyze
options:
-h, --help show this help message and exit
--lines count lines
--top TOP top N words (default: 5)
--search SEARCH search for a word
Example: python script.py sample.txt --lines
Auto-generated from the add_argument calls. Add help= to each — your future users (including future you) will thank you.
Click: decorator-based CLIs
import click
@click.command()
@click.argument("filename")
@click.option("--lines", is_flag=True, help="count lines")
@click.option("--top", default=5, help="top N words")
@click.option("--search", help="search for a word")
def main(filename, lines, top, search):
"""Analyze a text file."""
click.echo(f"Analyzing {filename}")
if lines:
...
if __name__ == "__main__":
main()
pip install click. Define one function per command, decorate it with arguments and options, and Click handles parsing, help, and error messages.
@click.argument for positional, @click.option for ---style. The function parameters automatically receive the parsed values.
Click: subcommands
For tools with multiple actions (like git add, git commit):
@click.group()
def cli():
"""Employee directory tool."""
pass
@cli.command()
@click.option("--dept", help="Filter by department")
def list(dept):
"""List all employees."""
...
@cli.command()
@click.argument("name")
def search(name):
"""Search by name."""
...
@cli.command()
@click.argument("name")
@click.option("--format", "fmt", type=click.Choice(["short", "full"]), default="short")
def info(name, fmt):
"""Show employee info."""
...
if __name__ == "__main__":
cli()
Run:
python tool.py list --dept Engineering
python tool.py search Alice
python tool.py info alice --format full
@click.group() defines the parent command; @cli.command() registers subcommands. Each gets its own --help.
Click: validation and types
@click.option("--port", type=click.IntRange(1, 65535))
@click.option("--config", type=click.Path(exists=True))
@click.option("--verbose", count=True) # -v, -vv, -vvv
@click.option("--name", prompt="Your name") # prompt if missing
@click.password_option() # hide password input
Click has rich validation built in. IntRange, Path(exists=True), Choice, DateTime, etc.
Click: colors and confirmations
click.echo(click.style("Success!", fg="green", bold=True))
click.echo(click.style("Error", fg="red"))
if click.confirm("Delete?"):
...
# Progress bar
with click.progressbar(items) as bar:
for item in bar:
process(item)
click.echo is like print but smart about Unicode and pipe-friendly. click.style applies ANSI colors that disable themselves when output isn't a TTY (so logs don't get garbage codes).
Rich: pretty terminal output
pip install rich. Rich is a library for beautiful terminal output — colors, tables, progress bars, syntax highlighting.
from rich.console import Console
from rich.table import Table
console = Console()
console.print("[bold red]Error[/bold red] something went wrong")
console.print("[green]✓[/green] Done!")
table = Table(title="Employees")
table.add_column("Name", style="cyan")
table.add_column("Department")
table.add_column("Role", justify="right")
table.add_row("Alice", "Engineering", "Senior Dev")
table.add_row("Bob", "Engineering", "Tech Lead")
console.print(table)
Output:
Employees
┏━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Name ┃ Department ┃ Role ┃
┡━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Alice │ Engineering │ Senior Dev │
│ Bob │ Engineering │ Tech Lead │
└────────┴─────────────┴──────────────┘
Better than printing tabs.
Rich: progress bars
from rich.progress import track
for item in track(items, description="Processing..."):
process(item)
Or for more control:
from rich.progress import Progress
with Progress() as progress:
task = progress.add_task("Downloading...", total=100)
for chunk in download():
progress.update(task, advance=len(chunk))
Rich: tracebacks
from rich.traceback import install
install(show_locals=True)
One line at startup. Now any uncaught exception prints with colors, syntax-highlighted source, and (optionally) local variables. Game-changer for debugging.
Combining Click + Rich
import click
from rich.console import Console
from rich.table import Table
console = Console()
@click.command()
@click.argument("filename")
def main(filename):
table = Table(title=f"Analysis of {filename}")
table.add_column("Metric")
table.add_column("Value", justify="right")
with open(filename) as f:
content = f.read()
table.add_row("Lines", str(content.count('\n')))
table.add_row("Words", str(len(content.split())))
table.add_row("Chars", str(len(content)))
console.print(table)
if __name__ == "__main__":
main()
Click for argument parsing, Rich for pretty output. Each focuses on what it does best.
When to use each
- argparse — small, single-script tools. Stdlib, no dependencies. Fine for under ~50 lines.
- Click — medium-to-large tools, multiple subcommands, polished UX. Industry standard for production CLIs.
- Typer — Click-like but type-hint based. Same author. Fine alternative.
- Rich — anywhere output matters. Logs, tables, progress, errors.
For new projects with subcommands: Click + Rich is the modern stack.
A complete tool
import click
from rich.console import Console
from rich.table import Table
from rich.traceback import install
install()
console = Console()
@click.group()
def cli():
"""Employee directory."""
pass
@cli.command()
@click.option("--dept")
def ls(dept):
"""List employees."""
table = Table(title="Employees")
table.add_column("Name", style="cyan")
table.add_column("Dept")
table.add_column("Email")
for emp in EMPLOYEES.values():
if dept and emp["dept"] != dept:
continue
table.add_row(emp["name"], emp["dept"], emp["email"])
console.print(table)
@cli.command()
@click.argument("name")
def info(name):
"""Show employee details."""
emp = EMPLOYEES.get(name.lower())
if not emp:
console.print(f"[red]Not found:[/red] {name}")
raise click.exceptions.Exit(1)
for key, value in emp.items():
console.print(f" [cyan]{key}:[/cyan] {value}")
if __name__ == "__main__":
cli()
Subcommands. Tables. Color. Proper exit codes on error. Looks polished, took 30 lines.
Distribution
To make your tool installable as mytool (instead of python tool.py), use a pyproject.toml:
[project]
name = "mytool"
version = "0.1.0"
dependencies = ["click", "rich"]
[project.scripts]
mytool = "mytool.main:cli"
pip install . and mytool list works system-wide. Lesson 40 covers packaging.
Common stumbles
Forgetting --help. Don't. Use the libraries' auto-generated help; never roll your own.
Bad exit codes. Use sys.exit(1) (or click.exceptions.Exit(1)) for errors. Shells need this for scripting.
Mixing prints. print writes to stdout, errors should go to stderr (click.echo(..., err=True) or sys.stderr.write).
Hard-coded paths. Use click.Path(exists=True) or pathlib.Path so paths work cross-platform.
Globals in CLI handlers. Tests get hard. Keep functions pure; pass state explicitly.
Rich output in pipes. console.print auto-detects TTY and disables formatting when piped — usually correct. If not, configure Console(force_terminal=True).
Subcommand naming clash with builtins. def list(...): shadows the builtin in that scope. Use def ls(...): or rename via name=.
What's next
Lesson 38: FastAPI. Modern web API framework — async, type-hinted, auto-docs.
Recap
argparse (stdlib) for simple tools. Click for multi-command CLIs with decorators. Rich for tables, colors, progress bars, tracebacks. Combine Click + Rich for production-quality tools. Always add help= text and let the framework generate --help. Use click.Choice, IntRange, Path for typed validation. Distribute via pyproject.toml + [project.scripts].
Next lesson: FastAPI.