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.
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.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.