Building a CLI Tool with Click That People Actually Enjoy Using

By James Nguyen Updated September 24, 2026
Building a CLI Tool with Click That People Actually Enjoy Using

My first internal CLI tool used Python's built-in argparse, and despite functioning correctly, nobody on my team used it voluntarily, reaching instead for a patchwork of manual scripts and copy-pasted commands. Rebuilding it with Click taught me the real difference between a tool that technically works and one people actually reach for.

Why Click Over argparse

argparse handles argument parsing competently but leaves you building everything else, help text formatting, subcommands, colored output, from scratch. Click wraps that same core functionality in decorators that handle the tedious parts automatically, and the resulting help text and error messages look genuinely polished without extra work.

import click

@click.group()
def cli():
    '''A tool for managing deployment environments.'''
    pass

@cli.command()
@click.argument("environment")
@click.option("--force", is_flag=True, help="Skip confirmation prompt.")
def deploy(environment: str, force: bool):
    '''Deploy the current branch to ENVIRONMENT.'''
    if not force:
        click.confirm(f"Deploy to {environment}?", abort=True)
    click.echo(f"Deploying to {environment}...")

Subcommands That Feel Organized, Not Bolted On

Click's group decorator lets you nest commands naturally, deploy status, deploy rollback, deploy logs, rather than the flat, awkward flag-based structure argparse pushes you toward for anything beyond a single-purpose script.

Confirmation Prompts People Don't Route Around

The click.confirm and click.prompt functions handle interactive input cleanly, including sensible defaults and input validation, and I've found teammates genuinely trust these built-in confirmations rather than developing a habit of blindly passing --force to skip past custom-built prompts that felt clunky in earlier versions.

Colored Output Without a Separate Dependency

Click ships with basic ANSI color support built in, letting me highlight errors in red and success messages in green without pulling in a separate library just for that, a small detail that made the tool feel considerably more professional with almost no extra effort.

click.secho("Deployment failed", fg="red", bold=True)
click.secho("Deployment succeeded", fg="green")

Progress Bars for Long-Running Operations

For commands that process a batch of items, syncing files, running migrations, click.progressbar gives immediate visual feedback that a command is actually working rather than silently hanging, something that eliminated a recurring complaint about the tool "freezing" when it was actually just running quietly.

with click.progressbar(items, label="Syncing files") as bar:
    for item in bar:
        sync_file(item)

Testing CLI Commands Properly

Click's CliRunner lets you invoke commands programmatically in tests, checking exit codes and captured output without actually spawning a subprocess, which made writing genuine test coverage for the tool's behavior far less painful than I expected going in.

from click.testing import CliRunner

def test_deploy_requires_confirmation():
    runner = CliRunner()
    result = runner.invoke(cli, ["deploy", "production"], input="n\n")
    assert result.exit_code != 0

Packaging It So It's Actually Installable

Wiring the CLI entry point into pyproject.toml means teammates install it with a single pip install and get a proper command available on their path, rather than needing to remember a specific python script.py invocation, a detail that sounds trivial but genuinely affects whether people bother using a tool at all.

[project.scripts]
deploytool = "deploytool.cli:cli"

Where I've Kept It Deliberately Simple

I've resisted adding a plugin system or overly clever auto-discovery of commands, since the added complexity wasn't solving a real problem the team actually had, and simple, predictable behavior has mattered more for adoption than theoretical extensibility nobody was asking for.

The Adoption Difference

Within a couple weeks of shipping the Click-based rewrite, the team's manual copy-pasted deployment commands genuinely disappeared, replaced by everyone actually using the tool, a shift that had less to do with any single feature and more to do with the cumulative effect of good help text, clear confirmations, and output that looked like it was built with real care rather than assembled from the bare minimum argparse required.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles