Click makes building CLIs easy. Here's how to use it effectively.
Basic Command
import click
@click.command()
@click.option("--name", default="World", help="Name to greet")
def hello(name):
"""Simple program that greets NAME."""
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
hello()$ python hello.py --name Owen
Hello, Owen!
$ python hello.py --help
Usage: hello.py [OPTIONS]
Simple program that greets NAME.
Options:
--name TEXT Name to greet
--help Show this message and exit.Options vs Arguments
Options are optional, named, and start with --:
@click.option("--count", default=1, help="Number of times")
@click.option("--verbose", is_flag=True, help="Enable verbose mode")Arguments are required and positional:
@click.argument("filename")
@click.argument("output", required=False)Common Option Patterns
# Required option
@click.option("--config", required=True)
# Multiple values
@click.option("--tag", multiple=True)
# Usage: --tag foo --tag bar
# Choice from list
@click.option("--format", type=click.Choice(["json", "csv", "table"]))
# File input/output
@click.option("--input", type=click.File("r"))
@click.option("--output", type=click.File("w"), default="-")
# Password (hidden input)
@click.option("--password", prompt=True, hide_input=True)
# Confirmation
@click.option("--yes", is_flag=True, help="Skip confirmation")Command Groups
For CLIs with subcommands:
@click.group()
def cli():
"""Task management CLI."""
pass
@cli.command()
@click.argument("name")
def add(name):
"""Add a new task."""
click.echo(f"Added: {name}")
@cli.command()
@click.option("--all", is_flag=True)
def list(all):
"""List tasks."""
click.echo("Listing tasks...")
@cli.command()
@click.argument("task_id")
def done(task_id):
"""Mark task as done."""
click.echo(f"Completed: {task_id}")
if __name__ == "__main__":
cli()$ python tasks.py add "Write blog post"
Added: Write blog post
$ python tasks.py list --all
Listing tasks...
$ python tasks.py done 123
Completed: 123Context and State
Share state between commands:
@click.group()
@click.option("--debug/--no-debug", default=False)
@click.pass_context
def cli(ctx, debug):
ctx.ensure_object(dict)
ctx.obj["DEBUG"] = debug
@cli.command()
@click.pass_context
def sync(ctx):
if ctx.obj["DEBUG"]:
click.echo("Debug mode enabled")
click.echo("Syncing...")Output Formatting
# Colored output
click.echo(click.style("Success!", fg="green", bold=True))
click.echo(click.style("Error!", fg="red"))
# Shorthand
click.secho("Success!", fg="green", bold=True)
# Progress bar
with click.progressbar(items) as bar:
for item in bar:
process(item)
# Confirmation prompt
if click.confirm("Delete all files?"):
delete_all()Error Handling
@click.command()
@click.argument("filename")
def process(filename):
try:
with open(filename) as f:
data = f.read()
except FileNotFoundError:
raise click.ClickException(f"File not found: {filename}")
except PermissionError:
raise click.ClickException(f"Permission denied: {filename}")Exit codes:
import sys
@click.command()
def check():
if something_wrong():
click.echo("Check failed", err=True)
sys.exit(1)
click.echo("Check passed")Testing
Click has built-in testing support:
from click.testing import CliRunner
def test_hello():
runner = CliRunner()
result = runner.invoke(hello, ["--name", "Test"])
assert result.exit_code == 0
assert "Hello, Test!" in result.output
def test_missing_required():
runner = CliRunner()
result = runner.invoke(cli, ["add"]) # Missing argument
assert result.exit_code != 0Entry Points
Install your CLI with pip:
# pyproject.toml
[project.scripts]
mytool = "mypackage.cli:cli"After pip install .:
$ mytool add "Task"
$ mytool listMy Patterns
Always add help text:
@click.option("--output", "-o", help="Output file path")Use sensible defaults:
@click.option("--format", default="table", show_default=True)Fail fast with clear errors:
if not path.exists():
raise click.ClickException(f"Path does not exist: {path}")Support both interactive and scriptable use:
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
def delete(yes):
if not yes and not click.confirm("Delete?"):
raise click.Abort()Click handles the boring parts. You focus on the logic.
React to this post: