Python CLI Applications
Modern Python command-line interface development using Click and Rich for building beautiful, functional terminal applications.
Python CLI Applications
Modern Python command-line interface development using Click and Rich for building beautiful, functional terminal applications.
Overview
Click is a composable command-line interface toolkit that makes creating CLI tools simple through decorators and automatic help generation. Rich is a Python library for rendering sophisticated formatting and styling in the terminal, including colours, tables, progress bars, syntax highlighting, and more. Together, they enable creation of professional CLI applications with minimal boilerplate.
flowchart TD
A[CLI Invocation] --> B{Click Parser}
B --> C[Commands]
B --> D[Options/Flags]
B --> E[Arguments]
C --> F[Command Handler]
D --> F
E --> F
F --> G{Rich Console}
G --> H[Styled Output]
G --> I[Tables/Panels]
G --> J[Progress Bars]
G --> K[Syntax Highlighting]
H --> L[Terminal Display]
I --> L
J --> L
K --> L
style B fill:#4A90E2
style G fill:#E24A90
Click Basics
Click uses Python decorators to turn functions into CLI commands with automatic help generation, type conversion, and validation.
Key Concepts
- Commands: Functions decorated with
@click.command()or@click.group() - Options: Named parameters prefixed with
--or-(e.g.,--verbose,-v) - Arguments: Positional parameters without prefixes
- Context: Shared state object passed between commands
- Decorators: Stack decorators to define CLI structure
Common Patterns
import click
# Basic command
@click.command()
@click.option('--count', default=1, help='Number of iterations')
@click.option('--name', prompt='Your name', help='The person to greet')
@click.argument('output', type=click.File('w'), default='-')
def hello(count, name, output):
"""Simple programme that greets NAME COUNT times."""
for _ in range(count):
click.echo(f'Hello, {name}!', file=output)
# Multiple options with short flags
@click.command()
@click.option('-v', '--verbose', is_flag=True, help='Enable verbose mode')
@click.option('-q', '--quiet', is_flag=True, help='Suppress output')
@click.option('-o', '--output', type=click.Path(), help='Output file path')
def process(verbose, quiet, output):
"""Process data with various output options."""
if verbose:
click.echo('Verbose mode enabled')
if not quiet:
click.echo('Processing...')
# Choice options
@click.command()
@click.option('--format', type=click.Choice(['json', 'yaml', 'xml']),
default='json', help='Output format')
@click.option('--level', type=click.IntRange(0, 10), default=5)
def export(format, level):
"""Export data in specified format."""
click.echo(f'Exporting as {format} with level {level}')
# Multiple values
@click.command()
@click.option('--exclude', multiple=True, help='Patterns to exclude')
@click.argument('paths', nargs=-1, required=True, type=click.Path(exists=True))
def search(exclude, paths):
"""Search in multiple paths with exclusions."""
click.echo(f'Searching in: {paths}')
click.echo(f'Excluding: {exclude}')
# Password prompts
@click.command()
@click.option('--username', prompt=True)
@click.option('--password', prompt=True, hide_input=True,
confirmation_prompt=True)
def login(username, password):
"""Login with credentials."""
click.echo(f'Logging in as {username}')
Examples
Command groups for subcommands:
@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
"""Main CLI entry point."""
ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug
@cli.command()
@click.pass_context
def init(ctx):
"""Initialise the application."""
if ctx.obj['DEBUG']:
click.echo('Debug mode enabled')
click.echo('Initialising...')
@cli.command()
@click.argument('name')
@click.pass_context
def create(ctx, name):
"""Create a new resource."""
click.echo(f'Creating {name}')
# Usage: cli.py init
# cli.py create myapp
Nested command groups:
@click.group()
def cli():
"""Application CLI."""
pass
@cli.group()
def database():
"""Database commands."""
pass
@database.command()
def migrate():
"""Run database migrations."""
click.echo('Running migrations...')
@database.command()
def seed():
"""Seed database with initial data."""
click.echo('Seeding database...')
# Usage: cli.py database migrate
# cli.py database seed
Click Advanced Features
Click provides powerful features for validation, callbacks, prompts, and custom types.
Key Concepts
- Callbacks: Functions that transform/validate option values
- Custom types: Extend Click's type system
- Confirmation prompts: Safety checks for destructive operations
- Environment variables: Automatic environment variable support
- Parameter decorators: Reusable parameter definitions
Common Patterns
import click
import os
from pathlib import Path
# Validation callback
def validate_port(ctx, param, value):
"""Ensure port is in valid range."""
if value < 1024 or value > 65535:
raise click.BadParameter('Port must be between 1024 and 65535')
return value
@click.command()
@click.option('--port', default=8080, callback=validate_port, type=int)
def serve(port):
"""Start server on specified port."""
click.echo(f'Starting server on port {port}')
# Custom type
class EmailType(click.ParamType):
name = 'email'
def convert(self, value, param, ctx):
if '@' not in value:
self.fail(f'{value} is not a valid email', param, ctx)
return value.lower()
EMAIL = EmailType()
@click.command()
@click.option('--email', type=EMAIL, required=True)
def register(email):
"""Register with email address."""
click.echo(f'Registered: {email}')
# Confirmation prompts
@click.command()
@click.option('--yes', is_flag=True, help='Skip confirmation')
@click.argument('filename')
def delete(yes, filename):
"""Delete a file with confirmation."""
if not yes:
click.confirm(f'Delete {filename}?', abort=True)
click.echo(f'Deleting {filename}')
# Environment variables
@click.command()
@click.option('--api-key', envvar='API_KEY', required=True,
help='API key (or set API_KEY env var)')
@click.option('--endpoint', envvar='API_ENDPOINT',
default='https://api.example.com')
def api_call(api_key, endpoint):
"""Make API call using credentials."""
click.echo(f'Calling {endpoint}')
# Reusable parameter decorator
def common_options(f):
"""Decorator for common CLI options."""
f = click.option('--verbose', '-v', is_flag=True)(f)
f = click.option('--dry-run', is_flag=True)(f)
f = click.option('--config', type=click.Path(exists=True))(f)
return f
@click.command()
@common_options
def deploy(verbose, dry_run, config):
"""Deploy application."""
if verbose:
click.echo('Verbose output enabled')
if dry_run:
click.echo('Dry run - no changes made')
Examples
Progress indication with Click:
import click
import time
@click.command()
@click.argument('files', nargs=-1, type=click.Path(exists=True))
def process_files(files):
"""Process multiple files with progress."""
with click.progressbar(files, label='Processing files') as bar:
for file in bar:
# Simulate processing
time.sleep(0.1)
click.echo('Complete!')
# Alternative with manual updates
@click.command()
def long_task():
"""Long running task with progress."""
items = range(100)
with click.progressbar(items) as bar:
for item in bar:
time.sleep(0.05)
Colour and styling:
@click.command()
def status():
"""Show coloured status output."""
click.echo(click.style('Success', fg='green', bold=True))
click.echo(click.style('Warning', fg='yellow'))
click.echo(click.style('Error', fg='red', bold=True))
click.echo(click.style('Info', fg='blue'))
# Background colours
click.echo(click.style('Highlighted', bg='yellow', fg='black'))
# Secho is shorthand for styled echo
@click.command()
def alerts():
"""Show alerts."""
click.secho('✓ Operation successful', fg='green')
click.secho('✗ Operation failed', fg='red', bold=True)
Context passing and chaining:
@click.group()
@click.option('--config-file', type=click.Path())
@click.pass_context
def cli(ctx, config_file):
"""CLI with shared configuration."""
ctx.ensure_object(dict)
# Load and store config
ctx.obj['config'] = load_config(config_file) if config_file else {}
@cli.command()
@click.option('--env', default='production')
@click.pass_context
def deploy(ctx, env):
"""Deploy to environment."""
config = ctx.obj['config']
click.echo(f'Deploying to {env} with config: {config}')
@cli.command()
@click.pass_context
def status(ctx):
"""Check status."""
config = ctx.obj['config']
click.echo(f'Status check with config: {config}')
Rich Console Output
Rich provides beautiful, formatted terminal output with minimal effort, supporting colours, styles, tables, and more.
Key Concepts
- Console: Main output interface with formatting capabilities
- Styles: Colour and text formatting (bold, italic, underline)
- Print inspection: Automatic pretty-printing of Python objects
- Markup: BBCode-style inline formatting
- Live display: Dynamically updating output
Common Patterns
from rich.console import Console
from rich.style import Style
from rich.theme import Theme
# Basic console
console = Console()
# Simple output
console.print("Hello, [bold magenta]World[/bold magenta]!")
console.print("[red]Error:[/red] Something went wrong")
console.print("[green]✓[/green] Success")
# Styles
console.print("Bold", style="bold")
console.print("Red on white", style="red on white")
console.print("Italic cyan", style="italic cyan")
# Custom styles
error_style = Style(color="red", bold=True)
console.print("Critical error", style=error_style)
# Custom theme
custom_theme = Theme({
"info": "cyan",
"warning": "yellow",
"error": "bold red",
"success": "bold green"
})
console = Console(theme=custom_theme)
console.print("This is informational", style="info")
console.print("This is a warning", style="warning")
# Pretty printing Python objects
data = {
'name': 'Alice',
'age': 30,
'skills': ['Python', 'Docker', 'Kubernetes'],
'active': True
}
console.print(data)
# JSON highlighting
import json
json_data = json.dumps(data, indent=2)
console.print_json(json_data)
# Rule/divider
console.rule("[bold blue]Section Title")
console.rule() # Simple line
# Padding and alignment
console.print("Centered text", justify="center")
console.print("Right-aligned", justify="right")
Examples
Logging with Rich:
import logging
from rich.logging import RichHandler
# Configure logging with Rich
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
handlers=[RichHandler(rich_tracebacks=True)]
)
log = logging.getLogger("rich")
log.info("Starting application")
log.warning("This is a warning")
log.error("An error occurred")
# With custom formatting
handler = RichHandler(
show_time=True,
show_path=True,
markup=True,
rich_tracebacks=True,
tracebacks_show_locals=True
)
Status indicators and spinners:
from rich.console import Console
import time
console = Console()
# Status spinner
with console.status("[bold green]Processing...") as status:
time.sleep(2)
status.update("[bold blue]Still processing...")
time.sleep(2)
console.print("[green]✓ Complete!")
# Different spinner styles
with console.status("Loading...", spinner="dots"):
time.sleep(3)
Pretty exceptions:
from rich.console import Console
from rich.traceback import install
# Install Rich traceback handler
install(show_locals=True)
console = Console()
# Now all exceptions are beautiful
def buggy_function():
x = 1
y = 0
return x / y
try:
buggy_function()
except Exception:
console.print_exception(show_locals=True)
Rich Tables
Rich tables provide flexible, styled tabular data display with automatic sizing and alignment.
Key Concepts
- Table: Main table object with configurable appearance
- Columns: Define headers, styles, and alignment
- Rows: Data rows added incrementally
- Styling: Per-cell, per-row, or per-column styling
- Box styles: Built-in border styles
Common Patterns
from rich.console import Console
from rich.table import Table
from rich import box
console = Console()
# Basic table
table = Table(title="Users")
table.add_column("ID", style="cyan", justify="right")
table.add_column("Name", style="magenta")
table.add_column("Email", style="green")
table.add_row("1", "Alice", "alice@example.com")
table.add_row("2", "Bob", "bob@example.com")
table.add_row("3", "Charlie", "charlie@example.com")
console.print(table)
# Table with different box style
table = Table(title="Server Status", box=box.ROUNDED)
table.add_column("Server", style="cyan")
table.add_column("Status", style="green")
table.add_column("Uptime", justify="right")
table.add_row("web-01", "✓ Running", "99.9%")
table.add_row("web-02", "✓ Running", "99.8%")
table.add_row("db-01", "✗ Down", "0.0%", style="red")
console.print(table)
# Auto-sizing and overflow
table = Table(show_header=True, header_style="bold blue",
show_lines=True, expand=True)
table.add_column("ID", width=10)
table.add_column("Description", overflow="fold")
table.add_column("Status")
# Grid table (no borders)
from rich.table import Table
grid = Table.grid(padding=(0, 2))
grid.add_column(style="cyan")
grid.add_column(style="magenta")
grid.add_row("Name:", "Alice")
grid.add_row("Age:", "30")
grid.add_row("Location:", "London")
console.print(grid)
Examples
Dynamic table from data:
from rich.console import Console
from rich.table import Table
def display_data(data):
"""Display list of dicts as table."""
console = Console()
if not data:
console.print("[yellow]No data to display[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
# Add columns from first item
for key in data[0].keys():
table.add_column(key.title())
# Add rows
for item in data:
table.add_row(*[str(v) for v in item.values()])
console.print(table)
# Usage
users = [
{'id': 1, 'name': 'Alice', 'role': 'Admin'},
{'id': 2, 'name': 'Bob', 'role': 'User'},
{'id': 3, 'name': 'Charlie', 'role': 'User'}
]
display_data(users)
Conditional row styling:
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Process Status")
table.add_column("PID", style="cyan")
table.add_column("Name", style="white")
table.add_column("CPU %", justify="right")
table.add_column("Status")
processes = [
(1234, "python", 45.2, "high"),
(5678, "docker", 12.1, "normal"),
(9012, "node", 78.9, "critical")
]
for pid, name, cpu, status in processes:
if status == "critical":
style = "bold red"
elif status == "high":
style = "yellow"
else:
style = "green"
table.add_row(str(pid), name, f"{cpu:.1f}", status, style=style)
console.print(table)
Rich Progress Bars
Rich provides sophisticated progress tracking with multiple tasks, columns, and real-time updates.
Key Concepts
- Progress: Main progress tracker supporting multiple tasks
- Task: Individual progress item with percentage completion
- Columns: Customisable progress display elements
- Context manager: Automatic start/stop handling
- Live updates: Real-time display updates
Common Patterns
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
from rich.console import Console
import time
# Basic progress bar
with Progress() as progress:
task = progress.add_task("[cyan]Processing...", total=100)
while not progress.finished:
progress.update(task, advance=1)
time.sleep(0.05)
# Multiple tasks
with Progress() as progress:
download_task = progress.add_task("[red]Downloading...", total=1000)
process_task = progress.add_task("[green]Processing...", total=500)
while not progress.finished:
progress.update(download_task, advance=5)
progress.update(process_task, advance=2)
time.sleep(0.02)
# Custom columns
progress = Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TextColumn("•"),
TextColumn("[blue]{task.completed}/{task.total}"),
)
with progress:
task = progress.add_task("Processing files", total=50)
for i in range(50):
time.sleep(0.1)
progress.update(task, advance=1)
# Indeterminate progress (unknown total)
with Progress() as progress:
task = progress.add_task("[cyan]Working...", total=None)
# Task runs indefinitely until manually stopped
for _ in range(100):
progress.update(task, advance=1)
time.sleep(0.05)
Examples
File processing with progress:
from rich.progress import Progress, BarColumn, TimeRemainingColumn
from pathlib import Path
import time
def process_files(directory):
"""Process all files in directory with progress."""
files = list(Path(directory).glob("*.txt"))
with Progress(
"[progress.description]{task.description}",
BarColumn(),
"[progress.percentage]{task.percentage:>3.0f}%",
TimeRemainingColumn(),
) as progress:
task = progress.add_task("Processing files", total=len(files))
for file in files:
# Process file
process_file(file)
progress.update(task, advance=1)
def process_file(filepath):
"""Simulate file processing."""
time.sleep(0.1)
Nested progress (downloads with extraction):
from rich.progress import Progress
import time
with Progress() as progress:
# Overall task
overall = progress.add_task("[green]Overall", total=100)
# Download task
download = progress.add_task("[cyan]Download", total=100)
for i in range(100):
progress.update(download, advance=1)
progress.update(overall, advance=0.5)
time.sleep(0.02)
# Extract task
extract = progress.add_task("[yellow]Extract", total=100)
for i in range(100):
progress.update(extract, advance=1)
progress.update(overall, advance=0.5)
time.sleep(0.02)
Progress with console output:
from rich.progress import Progress
from rich.console import Console
import time
console = Console()
with Progress() as progress:
task = progress.add_task("[cyan]Processing items...", total=10)
for i in range(10):
# You can still print during progress
console.log(f"Processing item {i+1}")
time.sleep(0.5)
progress.update(task, advance=1)
Rich Panels and Formatting
Rich panels, markdown rendering, and syntax highlighting provide polished terminal output.
Key Concepts
- Panels: Bordered boxes for content
- Markdown: Native markdown rendering
- Syntax: Code syntax highlighting
- Columns: Multi-column layout
- Tree: Hierarchical tree structures
Common Patterns
from rich.console import Console
from rich.panel import Panel
from rich.markdown import Markdown
from rich.syntax import Syntax
from rich.columns import Columns
from rich.tree import Tree
console = Console()
# Basic panel
console.print(Panel("Hello, World!"))
console.print(Panel("Error message", title="Error", border_style="red"))
# Panel with content
from rich.align import Align
message = Align.center("Centred message in panel")
console.print(Panel(message, title="Status", border_style="green"))
# Markdown rendering
markdown_text = """
# Heading 1
## Heading 2
This is **bold** and this is *italic*.
- List item 1
- List item 2
- List item 3
```python
def hello():
print("Hello, World!")
""" console.print(Markdown(markdown_text))
Syntax highlighting
code = ''' def greet(name: str) -> None: """Greet someone.""" print(f"Hello, {name}!")
if name == "main": greet("Alice") '''
syntax = Syntax(code, "python", theme="monokai", line_numbers=True) console.print(Panel(syntax, title="example.py"))
Tree structure
tree = Tree("📁 Project") tree.add("📄 README.md") tree.add("📄 setup.py") src = tree.add("📁 src") src.add("📄 init.py") src.add("📄 main.py") tests = tree.add("📁 tests") tests.add("📄 test_main.py") console.print(tree)
Columns layout
console.print(Columns([ Panel("Panel 1", border_style="green"), Panel("Panel 2", border_style="blue"), Panel("Panel 3", border_style="red") ]))
### Examples
**Configuration display:**
```python
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
def show_config(config):
"""Display configuration in panels."""
console = Console()
# Create table for config
table = Table(show_header=False, box=None, padding=(0, 2))
table.add_column("Key", style="cyan")
table.add_column("Value", style="white")
for key, value in config.items():
table.add_row(key, str(value))
# Wrap in panel
console.print(Panel(table, title="Configuration", border_style="blue"))
show_config({
'host': 'localhost',
'port': 8080,
'debug': True,
'workers': 4
})
Help text with panels:
from rich.console import Console
from rich.panel import Panel
from rich.columns import Columns
console = Console()
commands = [
Panel("[cyan]init[/cyan]\nInitialise project", border_style="cyan"),
Panel("[green]start[/green]\nStart server", border_style="green"),
Panel("[yellow]stop[/yellow]\nStop server", border_style="yellow"),
Panel("[red]clean[/red]\nClean up files", border_style="red")
]
console.print(Panel("Available Commands", style="bold white"))
console.print(Columns(commands, equal=True, expand=True))
Code snippet display:
from rich.console import Console
from rich.syntax import Syntax
from rich.panel import Panel
def show_snippet(code, language="python", title="Code"):
"""Display code snippet with syntax highlighting."""
console = Console()
syntax = Syntax(code, language, theme="dracula", line_numbers=True)
console.print(Panel(syntax, title=title, border_style="cyan"))
show_snippet(
code="SELECT * FROM users WHERE active = true;",
language="sql",
title="Query Example"
)
Combining Rich with Click
Integrate Rich's beautiful output with Click's CLI framework for professional command-line tools.
Key Concepts
- Rich console in Click commands: Use Rich for all output
- Rich-click: Drop-in replacement for Click with Rich formatting
- Custom error handling: Rich exceptions in CLI
- Progress in CLI: Track long-running operations
- Formatted help: Beautiful help messages
Common Patterns
import click
from rich.console import Console
from rich.table import Table
from rich.progress import Progress
from rich.panel import Panel
import time
console = Console()
@click.group()
def cli():
"""Application CLI with Rich output."""
pass
@cli.command()
@click.argument('name')
def greet(name):
"""Greet someone with style."""
console.print(f"[bold green]Hello, {name}![/bold green]")
console.print(Panel(f"Welcome, {name}!", border_style="blue"))
@cli.command()
def status():
"""Show system status."""
table = Table(title="System Status")
table.add_column("Component", style="cyan")
table.add_column("Status", style="green")
table.add_column("Uptime")
table.add_row("Database", "✓ Running", "99.9%")
table.add_row("API", "✓ Running", "99.8%")
table.add_row("Cache", "✓ Running", "100%")
console.print(table)
@cli.command()
@click.option('--count', default=10, help='Number of items')
def process(count):
"""Process items with progress bar."""
with Progress() as progress:
task = progress.add_task("[cyan]Processing...", total=count)
for i in range(count):
time.sleep(0.1)
progress.update(task, advance=1)
console.print("[green]✓ Complete![/green]")
@cli.command()
def error_demo():
"""Demonstrate error handling."""
try:
# Simulate error
raise ValueError("Something went wrong")
except Exception:
console.print_exception(show_locals=True)
if __name__ == '__main__':
cli()
Examples
Using rich-click for automatic formatting:
import rich_click as click
from rich.console import Console
# Configure rich-click
click.rich_click.USE_RICH_MARKUP = True
click.rich_click.SHOW_ARGUMENTS = True
click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
@click.group()
def cli():
"""
[bold cyan]My Application[/bold cyan]
A beautiful CLI application with Rich formatting.
"""
pass
@cli.command()
@click.option('--name', help='Your name', required=True)
@click.option('--count', default=1, help='Number of greetings')
def hello(name, count):
"""
Greet someone multiple times.
This command demonstrates Rich-formatted help text.
"""
console = Console()
for _ in range(count):
console.print(f"[green]Hello, {name}![/green]")
Complex CLI with Rich tables and panels:
import click
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box
console = Console()
@click.group()
@click.option('--format', type=click.Choice(['table', 'json']), default='table')
@click.pass_context
def cli(ctx, format):
"""Data management CLI."""
ctx.ensure_object(dict)
ctx.obj['format'] = format
@cli.command()
@click.pass_context
def list_users(ctx):
"""List all users."""
users = [
{'id': 1, 'name': 'Alice', 'role': 'Admin', 'active': True},
{'id': 2, 'name': 'Bob', 'role': 'User', 'active': True},
{'id': 3, 'name': 'Charlie', 'role': 'User', 'active': False}
]
if ctx.obj['format'] == 'json':
import json
console.print_json(json.dumps(users))
else:
table = Table(title="Users", box=box.ROUNDED)
table.add_column("ID", style="cyan", justify="right")
table.add_column("Name", style="magenta")
table.add_column("Role", style="blue")
table.add_column("Status")
for user in users:
status = "[green]✓ Active[/green]" if user['active'] else "[red]✗ Inactive[/red]"
table.add_row(
str(user['id']),
user['name'],
user['role'],
status
)
console.print(table)
@cli.command()
@click.argument('name')
@click.option('--role', type=click.Choice(['Admin', 'User']), default='User')
def create_user(name, role):
"""Create a new user."""
console.print(Panel(
f"Creating user: [cyan]{name}[/cyan]\nRole: [blue]{role}[/blue]",
title="User Creation",
border_style="green"
))
console.print("[green]✓ User created successfully![/green]")
if __name__ == '__main__':
cli()
CLI with live updates:
import click
from rich.live import Live
from rich.table import Table
import time
import random
@click.command()
@click.option('--duration', default=10, help='Duration in seconds')
def monitor(duration):
"""Monitor system metrics in real-time."""
def generate_table():
"""Generate metrics table."""
table = Table(title="System Metrics")
table.add_column("Metric", style="cyan")
table.add_column("Value", justify="right", style="green")
table.add_row("CPU", f"{random.randint(0, 100)}%")
table.add_row("Memory", f"{random.randint(0, 100)}%")
table.add_row("Disk", f"{random.randint(0, 100)}%")
return table
with Live(generate_table(), refresh_per_second=4) as live:
for _ in range(duration * 4):
time.sleep(0.25)
live.update(generate_table())
if __name__ == '__main__':
monitor()
argparse Alternative
argparse is Python's standard library option for CLI parsing—lightweight but less ergonomic than Click.
Key Concepts
- ArgumentParser: Main parser object
- add_argument(): Define positional and optional arguments
- parse_args(): Parse command-line arguments
- Subparsers: Support for subcommands
- Manual help formatting: Requires more code than Click
Common Patterns
import argparse
# Basic parser
parser = argparse.ArgumentParser(
description='Process some data',
epilog='Example: python script.py input.txt --output result.txt'
)
# Positional argument
parser.add_argument('input', help='Input file path')
# Optional arguments
parser.add_argument('--output', '-o', help='Output file path', default='output.txt')
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output')
parser.add_argument('--count', type=int, default=1, help='Number of iterations')
parser.add_argument('--format', choices=['json', 'yaml', 'xml'], default='json')
# Parse arguments
args = parser.parse_args()
# Use arguments
if args.verbose:
print(f'Processing {args.input}')
print(f'Output: {args.output}')
print(f'Format: {args.format}')
# Subcommands
parser = argparse.ArgumentParser(prog='myapp')
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Create subcommand
init_parser = subparsers.add_parser('init', help='Initialise project')
init_parser.add_argument('--name', required=True, help='Project name')
# Deploy subcommand
deploy_parser = subparsers.add_parser('deploy', help='Deploy application')
deploy_parser.add_argument('--env', choices=['dev', 'staging', 'prod'], required=True)
args = parser.parse_args()
if args.command == 'init':
print(f'Initialising {args.name}')
elif args.command == 'deploy':
print(f'Deploying to {args.env}')
Examples
argparse with type validation:
import argparse
from pathlib import Path
def valid_port(value):
"""Validate port number."""
ivalue = int(value)
if ivalue < 1024 or ivalue > 65535:
raise argparse.ArgumentTypeError(f'{value} is not a valid port')
return ivalue
def existing_file(value):
"""Validate file exists."""
path = Path(value)
if not path.exists():
raise argparse.ArgumentTypeError(f'{value} does not exist')
return path
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=valid_port, default=8080)
parser.add_argument('--config', type=existing_file)
args = parser.parse_args()
CLI Best Practices
Design principles and patterns for building robust, user-friendly command-line applications.
Key Concepts
- UNIX philosophy: Do one thing well, composable tools
- Exit codes: Return meaningful exit codes (0 for success, non-zero for errors)
- Configuration hierarchy: Command-line > environment > config file > defaults
- Input/output: Support stdin/stdout for pipelines
- Colour detection: Respect NO_COLOR, check TTY
Common Patterns
import click
from rich.console import Console
import sys
import os
# Configuration hierarchy
@click.command()
@click.option('--config', type=click.Path(exists=True), envvar='APP_CONFIG')
@click.option('--debug', is_flag=True, envvar='APP_DEBUG')
def app(config, debug):
"""Application with config hierarchy."""
# Priority: CLI > ENV > config file > defaults
pass
# Proper exit codes
@click.command()
def deploy():
"""Deploy with proper exit handling."""
try:
# Deployment logic
if not validate_deployment():
click.echo("Validation failed", err=True)
sys.exit(1)
perform_deployment()
click.echo("Deployment successful")
sys.exit(0)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(2)
# Stdin/stdout support
@click.command()
@click.argument('input', type=click.File('r'), default='-')
@click.argument('output', type=click.File('w'), default='-')
def transform(input, output):
"""Transform input to output (supports pipes)."""
for line in input:
processed = line.upper()
output.write(processed)
# Usage: echo "hello" | python app.py transform | grep HELLO
# Colour detection
console = Console()
def supports_colour():
"""Check if terminal supports colour."""
if os.getenv('NO_COLOR'):
return False
if not sys.stdout.isatty():
return False
return True
if not supports_colour():
console = Console(force_terminal=False, no_color=True)
# Verbose/quiet modes
@click.command()
@click.option('--verbose', '-v', count=True, help='Increase verbosity')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output')
def process(verbose, quiet):
"""Process with configurable output."""
if verbose >= 2:
click.echo("DEBUG: Detailed information")
elif verbose >= 1:
click.echo("INFO: Standard information")
if not quiet:
click.echo("Processing...")
# Dry-run mode
@click.command()
@click.option('--dry-run', is_flag=True, help='Show what would be done')
def cleanup(dry_run):
"""Clean up files."""
files_to_delete = ['temp.txt', 'cache.db']
for file in files_to_delete:
if dry_run:
click.echo(f"Would delete: {file}")
else:
click.echo(f"Deleting: {file}")
os.remove(file)
Examples
Structured logging in CLI:
import click
import logging
from rich.logging import RichHandler
@click.group()
@click.option('--log-level', default='INFO',
type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR']))
@click.pass_context
def cli(ctx, log_level):
"""CLI with structured logging."""
ctx.ensure_object(dict)
# Configure logging
logging.basicConfig(
level=log_level,
format="%(message)s",
handlers=[RichHandler()]
)
ctx.obj['logger'] = logging.getLogger('app')
@cli.command()
@click.pass_context
def process(ctx):
"""Process data with logging."""
logger = ctx.obj['logger']
logger.debug("Starting process")
logger.info("Processing data")
logger.warning("Resource usage high")
Configuration file support:
import click
import json
from pathlib import Path
DEFAULT_CONFIG = {
'host': 'localhost',
'port': 8080,
'debug': False
}
def load_config(config_path):
"""Load configuration from file."""
if config_path and Path(config_path).exists():
with open(config_path) as f:
return {**DEFAULT_CONFIG, **json.load(f)}
return DEFAULT_CONFIG
@click.command()
@click.option('--config', type=click.Path(),
envvar='APP_CONFIG',
default='~/.config/myapp/config.json')
@click.option('--host', envvar='APP_HOST', help='Override host')
@click.option('--port', type=int, envvar='APP_PORT', help='Override port')
def serve(config, host, port):
"""Start server with configuration."""
cfg = load_config(config)
# CLI options override config file
if host:
cfg['host'] = host
if port:
cfg['port'] = port
click.echo(f"Starting server on {cfg['host']}:{cfg['port']}")
Error handling patterns:
import click
from rich.console import Console
import sys
console = Console()
class AppError(Exception):
"""Base application error."""
pass
class ValidationError(AppError):
"""Validation failed."""
pass
@click.command()
def deploy():
"""Deploy with error handling."""
try:
validate_environment()
perform_deployment()
console.print("[green]✓ Deployment successful[/green]")
except ValidationError as e:
console.print(f"[yellow]Validation Error:[/yellow] {e}")
sys.exit(1)
except AppError as e:
console.print(f"[red]Error:[/red] {e}")
sys.exit(2)
except KeyboardInterrupt:
console.print("\n[yellow]Interrupted by user[/yellow]")
sys.exit(130)
except Exception as e:
console.print("[red]Unexpected error:[/red]")
console.print_exception(show_locals=True)
sys.exit(3)
Quick Reference
Click Command Decorators
| Decorator | Purpose |
|---|---|
@click.command() |
Define a command |
@click.group() |
Define a command group |
@click.option('--name') |
Add named option/flag |
@click.argument('name') |
Add positional argument |
@click.pass_context |
Pass Click context object |
@click.pass_obj |
Pass context object data |
Click Option Types
| Type | Example |
|---|---|
type=int |
Integer value |
type=float |
Float value |
type=click.Path() |
File/directory path |
type=click.File('r') |
Open file handle |
type=click.Choice([...]) |
Multiple choice |
type=click.IntRange(0, 10) |
Integer range |
is_flag=True |
Boolean flag |
multiple=True |
Accept multiple values |
Rich Console Methods
| Method | Purpose |
|---|---|
console.print() |
Print with formatting |
console.print_json() |
Print JSON with highlighting |
console.log() |
Print with timestamp |
console.rule() |
Print horizontal rule |
console.clear() |
Clear screen |
console.status() |
Show status spinner |
Rich Styling
| Style | Example |
|---|---|
| Colour | [red]text[/red], [blue]text[/blue] |
| Bold | [bold]text[/bold] |
| Italic | [italic]text[/italic] |
| Underline | [underline]text[/underline] |
| Combined | [bold red on white]text[/bold red on white] |
Common Imports
# Click imports
import click
from click import (
command, group, option, argument,
pass_context, pass_obj, echo, secho,
Path, File, Choice, IntRange
)
# Rich imports
from rich.console import Console
from rich.table import Table
from rich.progress import Progress, track
from rich.panel import Panel
from rich.syntax import Syntax
from rich.markdown import Markdown
from rich.tree import Tree
from rich import box
# Rich-click (drop-in Click replacement)
import rich_click as click
Exit Codes Convention
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Misuse of command |
| 126 | Command cannot execute |
| 127 | Command not found |
| 130 | Terminated by Ctrl+C |
Common Issues and Solutions
| Issue | Solution |
|---|---|
| Click options not working | Check decorator order—options should be closest to function definition |
| Rich not displaying colours | Check NO_COLOR env var; verify terminal supports colour; use Console(force_terminal=True) |
| Progress bar not updating | Ensure you're calling progress.update(task, advance=n) in loop |
| Context not passing between commands | Use @click.pass_context and ctx.ensure_object(dict) in parent command |
| Multiple values not working | Use multiple=True in option/argument definition |
| Table columns not aligning | Specify column widths explicitly or use Table(expand=True) |
| Unicode characters not rendering | Ensure terminal encoding is UTF-8; check locale settings |
| Stdin/stdout not working with Rich | Use Console(file=sys.stderr) for progress/status whilst writing to stdout |
| Tests failing with Rich output | Use Console(file=io.StringIO()) to capture output in tests |
| Click command not found in group | Verify command is added with @group.command() not @click.command() |
Debugging Tips
# Debug Click context
@click.command()
@click.pass_context
def debug_ctx(ctx):
"""Debug Click context."""
click.echo(f"Context: {ctx.obj}")
click.echo(f"Parent: {ctx.parent}")
click.echo(f"Params: {ctx.params}")
# Test Rich output
from rich.console import Console
import io
output = io.StringIO()
console = Console(file=output)
console.print("[red]Test[/red]")
result = output.getvalue()
assert "Test" in result
# Verify colour support
from rich.console import Console
console = Console()
print(f"Colour system: {console.color_system}")
print(f"Is terminal: {console.is_terminal}")
print(f"Legacy Windows: {console.legacy_windows}")
# Profile Rich rendering
import cProfile
from rich.console import Console
console = Console()
cProfile.run('console.print(large_table)')
Related Topics
The following topics complement Python CLI application development:
- Python - pytest: Testing CLI applications with fixtures and mocks
- Python - Type Hints and Static Analysis: Type safety for CLI code
- Python - Packaging and Dependency Management: Distributing CLI tools via PyPI
- Python - asyncio: Async/await patterns for I/O-heavy CLI operations
- Python - Logging: Structured logging for production CLI tools
- Bash: Shell scripting for CLI tool automation and integration