Python

Building CLI Tools with Click and Typer

Learn how to use Click and Typer to build professional CLI tools in Python with automatic help, type validation, and subcommands.

August 2026 10 min read 11 views 0 hearts

Building CLI Tools with Click and Typer: A Hands-On Guide for Python Developers

Let me tell you something I've learned the hard way: if your Python script needs arguments, flags, or subcommands, you could hack something together with sys.argv and a bunch of if-statements. But that's like building a house with a butter knife.

At PythonSkillset, we see too many developers wasting hours debugging command-line interfaces that could be done in minutes with proper tools. So let me show you how Click and Typer can turn your messy scripts into professional CLI tools.

Why Bother with CLI Libraries?

Before we jump in, ask yourself: have you ever written something like this?

import sys

if len(sys.argv) < 2:
    print("Usage: script.py <name>")
    sys.exit(1)

It works, but it's fragile. You have to manually handle types, defaults, help text, and error messages. Two months later, you'll forget how your own argument parsing works.

Click and Typer solve this by giving you: - Automatic help generation (like --help) - Type validation - Subcommands (like git commit, git push) - Colorful output without extra work

Getting Started with Click

Click is the older, more established library. It uses decorators to wrap your functions into CLI commands.

Installation

pip install click

Your First Click Command

Here's a basic greeting program:

import click

@click.command()
@click.option('--name', default='World', help='Who to greet')
@click.option('--count', default=1, help='Number of greetings')
def greet(name, count):
    """Greet someone with multiple repetitions."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")

if __name__ == '__main__':
    greet()

Run it:

python greet.py --name PythonSkillset --count 3

Output:

Hello, PythonSkillset!
Hello, PythonSkillset!
Hello, PythonSkillset!

Notice how click.echo() handles different consoles better than plain print(). And you get --help for free:

python greet.py --help

Shows:

Usage: greet.py [OPTIONS]

  Greet someone with multiple repetitions.

Options:
  --name TEXT     Who to greet
  --count INTEGER  Number of greetings
  --help          Show this message and exit.

Making Arguments Required

Sometimes you want positional arguments (no -- prefix). Here's how:

import click

@click.command()
@click.argument('filename')
@click.option('--verbose', is_flag=True, help='Show detailed output')
def process_file(filename, verbose):
    """Process a file."""
    if verbose:
        click.echo(f"Processing {filename}...")
    click.echo(f"Done with {filename}")

if __name__ == '__main__':
    process_file()

Run:

python process.py data.csv --verbose

Enter Typer: The Modern Alternative

Typer is built on top of Click but feels more Pythonic. It uses type hints and has better support for modern Python features.

Installation

pip install typer

Your First Typer Command

import typer

def main(name: str = typer.Option("World", help="Who to greet"),
         count: int = typer.Option(1, help="Number of greetings")):
    """Greet someone with multiple repetitions."""
    for _ in range(count):
        typer.echo(f"Hello, {name}!")

if __name__ == '__main__':
    typer.run(main)

See the difference? Typer automatically uses Python type hints to determine argument types. No more click.STRING or click.INT.

Typer Subcommands (Like Git)

This is where Typer shines. Building a tool with subcommands is dead simple:

import typer

app = typer.Typer()

@app.command()
def add(name: str, email: str):
    """Add a new user."""
    typer.echo(f"Added {name} ({email})")

@app.command()
def list_users():
    """List all users."""
    typer.echo("Listing users...")

@app.command()
def remove(name: str):
    """Remove a user."""
    typer.echo(f"Removed {name}")

if __name__ == '__main__':
    app()

Now you have a real CLI tool:

python users.py add "John Doe" john@example.com
python users.py list
python users.py remove "John Doe"

Real-World Example: File Backup Tool

Let me walk you through something I actually built for PythonSkillset's internal use. A simple backup script:

import typer
import shutil
from pathlib import Path
from datetime import datetime

app = typer.Typer()

@app.command()
def backup(source: str = typer.Argument(..., help="Source directory"),
            destination: str = typer.Argument(..., help="Backup destination"),
            compress: bool = typer.Option(False, "--compress", "-c", help="Compress backup")):
    """Backup a directory to a destination."""

    src = Path(source)
    dst = Path(destination)

    if not src.exists():
        typer.echo(f"Error: Source {source} does not exist", err=True)
        raise typer.Exit(code=1)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"backup_{src.name}_{timestamp}"
    backup_path = dst / backup_name

    try:
        shutil.copytree(src, backup_path)
        typer.echo(f"Backup created at {backup_path}")

        if compress:
            shutil.make_archive(str(backup_path), 'zip', backup_path)
            shutil.rmtree(backup_path)
            typer.echo("Backup compressed")

    except Exception as e:
        typer.echo(f"Backup failed: {e}", err=True)
        raise typer.Exit(code=1)

@app.command()
def list(directory: str = typer.Argument(".", help="Directory to list backups")):
    """List available backups."""
    path = Path(directory)
    backups = [p for p in path.glob("backup_*")]

    if not backups:
        typer.echo("No backups found")
        return

    for backup in sorted(backups, reverse=True):
        size = sum(f.stat().st_size for f in backup.rglob('*')) if backup.is_dir() else backup.stat().st_size
        typer.echo(f"{backup.name} ({size/1024:.1f} KB)")

if __name__ == '__main__':
    app()

This gives you:

python backup.py backup /home/user/projects /mnt/backup_drive
python backup.py backup /home/user/projects /mnt/backup_drive --compress
python backup.py list /mnt/backup_drive

When to Use Which

Choose Click when: - You need maximum compatibility with older Python versions - You're working on a larger project with complex CLI requirements - You prefer decorators over type hints

Choose Typer when: - You're using Python 3.6+ - You want cleaner, more readable code - You need subcommands and modern features - You're building something new and want the latest practices

At PythonSkillset, we've standardized on Typer for new projects because of its simplicity. But Click remains perfectly valid for existing codebases.

One Last Tip

Both libraries let you customize colors and styles:

# Click
click.secho("Error!", fg='red', bold=True)

# Typer
typer.secho("Success!", fg=typer.colors.GREEN, bold=True)

This makes your CLI tools feel polished and professional.

What's Next?

Start small. Pick a script you use frequently and wrap it with Click or Typer. You'll wonder how you lived without automatic help generation and type validation.

For deeper dives, check out: - Click Documentation - Typer Documentation

Both have excellent guides and examples.

Remember: your CLI tools are the face of your automation. Make them beautiful.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.