Click
Click builds CLIs with decorators. It handles help text, type conversion, subcommands, and terminal output better than raw argparse.
Recipe
import click
@click.command()
@click.option("--name", default="World", help="Who to greet")
def hello(name):
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
hello()When to reach for this:
- Multi-command CLI tools
- User-friendly help and error messages
- Prompts, colors, and progress bars needed
Working Example
import click
@click.group()
@click.option("--verbose", "-v", is_flag=True)
@click.pass_context
def cli(ctx, verbose):
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
@cli.command()
@click.argument("filename", type=click.Path(exists=True))
@click.option("--format", type=click.Choice(["json", "csv"]), default="json")
@click.pass_context
def convert(ctx, filename, format):
if ctx.obj["verbose"]:
click.echo(f"Converting {filename} to {format}", err=True)
# conversion logic
click.echo(f"Done: {filename}")
@cli.command()
@click.option("--force", is_flag=True, help="Skip confirmation")
def clean(force):
if not force and not click.confirm("Delete all temp files?"):
raise click.Abort()
click.echo("Cleaned.")
if __name__ == "__main__":
cli()What this demonstrates:
@click.group()for subcommandsclick.Path(exists=True)validates file pathsclick.Choicerestricts option valuesclick.confirmfor interactive promptserr=Truesends verbose output to stderr
Deep Dive
Click vs argparse
| Feature | Click | argparse |
|---|---|---|
| Syntax | Decorators | Imperative |
| Help formatting | Rich | Basic |
| Testing | CliRunner | parse_args |
| Prompts | Built-in | Manual |
Gotchas
- Not using
click.echo- encoding issues on Windows. Fix: alwaysclick.echo, notprint. - Missing
pass_context- parent options unavailable. Fix:@click.pass_contextandctx.obj. - Command callback not testable - only CLI invocation. Fix: use
CliRunnerin pytest. - Too many options on one command - unusable. Fix: split into subcommands or config file.
- Click without entry point - users cannot install globally. Fix: register in
[project.scripts].
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Typer | Type hints preferred | No typing in project |
| argparse | Zero dependencies | Rich CLI needed |
| cyclopts | Dataclass config | Team knows Click |
FAQs
Click or Typer?
Typer if you use type hints everywhere. Click for more control and maturity.
How do I test Click apps?
from click.testing import CliRunner; runner = CliRunner(); runner.invoke(cli, ["convert", "f.txt"]).
How do I add shell completion?
@click.group() + install completion: _MYAPP_COMPLETE=bash_source myapp.
How do I handle exceptions?
Let exceptions propagate for tracebacks, or catch and raise click.ClickException("msg") for clean errors.
Multiple arguments?
Multiple @click.argument() decorators in order.
Environment variable defaults?
@click.option("--host", envvar="HOST", default="localhost").
How do I show a progress bar?
with click.progressbar(items) as bar: ....
Click and async?
Use @click.command() with asyncio.run() inside, or asyncclick fork.
How do I version a CLI?
@click.version_option() decorator.
Nested groups?
Groups can contain groups: @cli.group() inside another group.
Related
- Typer - type-hint CLI on Click
- argparse - stdlib alternative
- Rich Output - enhanced formatting
- CLI Best Practices - design guidelines
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.