Building CLI Progress Indicators: Real-time User Feedback
Long-running CLI operations can leave users wondering if the tool is working or frozen. Progress indicators—bars, spinners, status messages—provide reassurance and estimate time remaining. The Rich library makes adding professional progress indicators simple.
Basic Progress Bar
Here's a complete progress bar that updates in real-time:
import typer
from rich.progress import Progress
import time
app = typer.Typer()
@app.command()
def download(count: int = 100):
"""Download files with progress tracking."""
with Progress() as progress:
task = progress.add_task("[cyan]Downloading...", total=count)
for i in range(count):
time.sleep(0.01) # Simulate download work
progress.update(task, advance=1)
typer.echo("[green]Download complete![/green]")
if __name__ == "__main__":
app()
The Progress context manager handles rendering. add_task() creates a progress tracker, and update() advances it. Users see a formatted bar with percentage, rate, and time remaining.
Multiple Concurrent Tasks
Show progress for several parallel operations:
import typer
from rich.progress import Progress
import time
app = typer.Typer()
@app.command()
def batch_process(items: int = 10):
"""Process items in parallel."""
with Progress() as progress:
# Create multiple progress tasks
upload_task = progress.add_task("[red]Uploading...", total=items)
process_task = progress.add_task("[yellow]Processing...", total=items)
verify_task = progress.add_task("[green]Verifying...", total=items)
# Simulate concurrent work
for i in range(items):
progress.update(upload_task, advance=1)
time.sleep(0.02)
progress.update(process_task, advance=1)
time.sleep(0.02)
progress.update(verify_task, advance=1)
time.sleep(0.02)
typer.echo("Batch processing complete!")
if __name__ == "__main__":
app()
Each task is tracked independently with different colors and descriptions. Users see which stages are running simultaneously.
Spinners for Indeterminate Operations
When you don't know the total, use a spinner:
import typer
from rich.spinner import Spinner
from rich.console import Console
import time
console = Console()
app = typer.Typer()
@app.command()
def connect(host: str):
"""Connect to a host."""
with console.status(f"[bold cyan]Connecting to {host}..."):
time.sleep(2) # Simulate connection time
typer.echo(f"[green]Connected to {host}[/green]")
if __name__ == "__main__":
app()
console.status() displays a spinner and message while work happens. When the context exits, the spinner disappears and normal output resumes.
Progress Columns and Customization
Customize progress bars with columns showing different metrics:
import typer
from rich.progress import (
Progress,
SpinnerColumn,
BarColumn,
TextColumn,
TimeRemainingColumn,
DownloadColumn,
TransferSpeedColumn,
)
import time
app = typer.Typer()
@app.command()
def transfer(megabytes: int = 100):
"""Transfer data with detailed progress."""
columns = [
TextColumn("[progress.description]{task.description}"),
DownloadColumn(),
BarColumn(),
TransferSpeedColumn(),
TimeRemainingColumn(),
]
with Progress(*columns) as progress:
task = progress.add_task("Transferring", total=megabytes * 1024 * 1024)
# Simulate data transfer
for _ in range(megabytes):
time.sleep(0.01)
progress.update(task, advance=1024 * 1024)
typer.echo("Transfer complete!")
if __name__ == "__main__":
app()
Different column types show: description, download amount, percentage bar, transfer speed, and time remaining. Rich arranges them responsively for the terminal width.
Logging Progress with Status Updates
Combine progress with live status messages:
import typer
from rich.progress import Progress
from rich import print
import time
app = typer.Typer()
@app.command()
def deploy(services: int = 5):
"""Deploy services with status updates."""
with Progress() as progress:
main_task = progress.add_task("[cyan]Deploying...", total=services)
for i in range(services):
service_name = f"service-{i+1}"
print(f"[yellow]Deploying[/yellow] {service_name}")
time.sleep(0.5) # Simulate deployment
progress.update(main_task, advance=1)
print(f"[green]✓[/green] {service_name} running")
print("[green bold]All services deployed![/green bold]")
if __name__ == "__main__":
app()
Interleave progress bars with print statements. Rich handles the layout, keeping the progress bar at the bottom while allowing text output above.
Progress Bars in Loops with Items
Show progress iterating over items:
import typer
from rich.progress import track
import time
app = typer.Typer()
@app.command()
def process_files(count: int = 20):
"""Process files with automatic progress."""
files = [f"file_{i}.txt" for i in range(count)]
for filename in track(files, description="Processing..."):
time.sleep(0.1) # Simulate file processing
typer.echo("All files processed!")
if __name__ == "__main__":
app()
The track() function wraps an iterable and shows progress automatically. No manual advance() calls needed.
Comparison Table: Progress Indicator Types
| Type | Use Case | Effort | Information |
|---|---|---|---|
| Simple bar | Known total items | Low | Percentage, rate |
| Multi-task bar | Parallel operations | Low-Medium | Multiple stages |
| Spinner | Indeterminate work | Very low | Status only |
| Custom columns | Complex metrics | Medium | Detailed metrics |
| Status message | Brief operations | Low | Contextual message |
Key Takeaways
- Use
Progressfor operations with a known total count; update withprogress.update(task, advance=amount). - Use
console.status()for indeterminate operations (spinners) when total is unknown. - Add multiple tasks to one
Progressto show parallel operations with different progress states. - Customize progress with columns:
DownloadColumn,TransferSpeedColumn,TimeRemainingColumn. - Use
track()to auto-progress over iterables without manual advance() calls. - Combine progress bars with print statements for detailed status logging.
Frequently Asked Questions
How do I show a progress bar for file downloads?
Use DownloadColumn with TransferSpeedColumn. Calculate total bytes and call progress.update(task, advance=bytes_received) as data arrives.
Can I update progress from threads or async tasks?
Yes, but carefully. The Progress context is thread-safe for updates. For async, update from the same loop; use asyncio.to_thread() for thread-based workers.
How do I show sub-tasks within a main progress bar?
Create multiple tasks with add_task() and update each independently. Rich renders them vertically or collapses them based on terminal height.
What if the operation completes faster than the progress bar refreshes?
Rich handles this gracefully. The progress bar shows 100% and exits. Users see a brief animation.
Can I save progress output to a log file?
Use Progress(file=open('log.txt', 'w')) to write progress to a file. Be aware that ANSI codes may appear in the file; use force_terminal=False to disable colors.