DEV Community

Cover image for Python click: Build Beautiful CLI Tools in Minutes
German Yamil
German Yamil

Posted on

Python click: Build Beautiful CLI Tools in Minutes

click vs argparse

Both build CLI tools. click does it with less code:

# argparse — 12 lines for a simple command
import argparse
parser = argparse.ArgumentParser(description="Say hello")
parser.add_argument("name", help="Your name")
parser.add_argument("--count", type=int, default=1, help="Times to repeat")
args = parser.parse_args()
for _ in range(args.count):
    print(f"Hello, {args.name}!")

# click — same result, cleaner
import click

@click.command()
@click.argument("name")
@click.option("--count", default=1, help="Times to repeat")
def hello(name, count):
    for _ in range(count):
        click.echo(f"Hello, {name}!")
Enter fullscreen mode Exit fullscreen mode

Install: pip install click

Basic Commands

import click

@click.command()
@click.argument("filename")
@click.option("--output", "-o", default="output.txt", help="Output file")
@click.option("--verbose", "-v", is_flag=True, help="Show details")
def process(filename, output, verbose):
    """Process FILENAME and write results to OUTPUT."""
    if verbose:
        click.echo(f"Processing {filename}...")

    # Your logic here
    click.echo(f"Done → {output}")

if __name__ == "__main__":
    process()
Enter fullscreen mode Exit fullscreen mode

Run: python script.py data.csv --output results.txt -v

Auto-generated help:

Usage: script.py [OPTIONS] FILENAME

  Process FILENAME and write results to OUTPUT.

Options:
  -o, --output TEXT  Output file
  -v, --verbose      Show details
  --help             Show this message and exit.
Enter fullscreen mode Exit fullscreen mode

Arguments vs Options

@click.command()
@click.argument("src")              # Required, positional
@click.argument("dst", default=".")  # Optional, positional
@click.option("--force", is_flag=True)  # Optional, named flag
@click.option("--mode", type=click.Choice(["fast", "safe"]), default="safe")
def copy(src, dst, force, mode):
    click.echo(f"Copying {src}{dst} (mode={mode}, force={force})")
Enter fullscreen mode Exit fullscreen mode

Arguments: positional, usually required
Options: named with --flag, usually optional

Type Validation

@click.command()
@click.option("--count", type=int, default=1)          # Must be integer
@click.option("--ratio", type=float)                   # Must be float
@click.option("--mode", type=click.Choice(["a", "b"])) # Must be one of
@click.option("--file", type=click.Path(exists=True))  # Must exist
@click.option("--out", type=click.Path())              # Path (doesn't check)
@click.option("--range", type=click.IntRange(1, 10))   # Int within range
def process(count, ratio, mode, file, out, range):
    pass
Enter fullscreen mode Exit fullscreen mode

click validates and shows a clear error if the wrong type is provided.

Prompts and Confirmation

@click.command()
@click.option("--name", prompt="Your name", help="Who to greet")
@click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True)
def setup(name, password):
    click.echo(f"Hello, {name}!")

@click.command()
@click.argument("path")
def delete(path):
    click.confirm(f"Delete {path}?", abort=True)  # Stops if user says no
    click.echo(f"Deleted {path}")
Enter fullscreen mode Exit fullscreen mode

Multiple Commands (CLI Groups)

import click

@click.group()
def cli():
    """My project CLI."""
    pass

@cli.command()
@click.argument("name")
def add(name):
    """Add a new item."""
    click.echo(f"Added: {name}")

@cli.command()
def list():
    """List all items."""
    click.echo("item1\nitem2\nitem3")

@cli.command()
@click.argument("name")
def delete(name):
    """Delete an item."""
    click.confirm(f"Delete {name}?", abort=True)
    click.echo(f"Deleted: {name}")

if __name__ == "__main__":
    cli()
Enter fullscreen mode Exit fullscreen mode

Usage:

python cli.py add "my item"
python cli.py list
python cli.py delete "my item"
python cli.py --help
Enter fullscreen mode Exit fullscreen mode

Colors and Formatting

@click.command()
def status():
    click.echo(click.style("✅ Success", fg="green"))
    click.echo(click.style("⚠️  Warning", fg="yellow"))
    click.echo(click.style("❌ Error", fg="red", bold=True))

    # Progress bar
    items = list(range(100))
    with click.progressbar(items, label="Processing") as bar:
        for item in bar:
            process(item)
Enter fullscreen mode Exit fullscreen mode

Passing Context Between Commands

@click.group()
@click.option("--debug", is_flag=True)
@click.pass_context
def cli(ctx, debug):
    ctx.ensure_object(dict)
    ctx.obj["debug"] = debug

@cli.command()
@click.pass_context
def run(ctx):
    if ctx.obj["debug"]:
        click.echo("Debug mode on")
    click.echo("Running...")
Enter fullscreen mode Exit fullscreen mode

Making it Installable

Add to pyproject.toml or setup.py:

[project.scripts]
mytool = "mypackage.cli:cli"
Enter fullscreen mode Exit fullscreen mode

After pip install -e .:

mytool add "item"    # Works globally
Enter fullscreen mode Exit fullscreen mode

When to Use click vs argparse

Scenario Use
Simple script, no deps argparse (stdlib)
Complex CLI with subcommands click
Beautiful output, progress bars click
Distributable tool click

Further Reading


Get the Full Pipeline

This article is part of the Python AI Publishing Pipeline series — a complete system to write, validate, and publish technical ebooks with Python and Claude.

📋 Free checklist: 7 steps to ship a Python ebook — PDF, no email required.

🚀 Full pipeline + source code: germy5.gumroad.com/l/xhxkzz — $9.99, 30-day money-back guarantee.


If this was useful, the ❤️ button helps other developers find it.

Building a Python content pipeline? I sell the complete automation system as a one-time download — Dev.to API, Claude API, launchd, Gumroad. Check it out ($9.99)

Top comments (0)