Adding Progress Bars and Spinners to Python CLIs
Modern command-line applications require immediate visual feedback to indicate long-running operations. Implementing Interactive Terminal UI with Rich patterns ensures your tools remain responsive and user-friendly. Visual feedback reduces perceived latency and prevents premature user termination. Rich and tqdm remain the industry standards for terminal UX. This guide covers production-ready implementations for both deterministic and indeterminate tasks using Python 3.10+ standards.
Deterministic Progress Bars with rich.progress
For tasks with a known iteration count, rich.progress provides thread-safe, auto-refreshing progress tracking. Unlike legacy libraries, it handles terminal resizing and nested contexts gracefully. When building complex pipelines, integrate these indicators early in your Advanced Input Parsing & User Experience workflow to maintain consistent UX across all subcommands. Use total=None for unknown lengths to trigger a spinner fallback. Context managers guarantee clean exit on KeyboardInterrupt.
from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn
import time
def process_items(items: list[str]) -> None:
with Progress(
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeElapsedColumn(),
) as progress:
task = progress.add_task("[green]Processing...", total=len(items))
for item in items:
time.sleep(0.1) # Simulate I/O or CPU work
progress.update(task, advance=1)
Indeterminate Spinners with rich.spinner
When operation duration is unpredictable, spinners prevent user abandonment. The Spinner class integrates seamlessly with Console output. A common pitfall is RichLiveUpdateError: Cannot use a Live instance more than once. This occurs when reusing a Spinner context manager across multiple functions or threads. Always instantiate a new spinner per execution block to avoid state collision. Use console=Console() for explicit output routing and testing.
from rich.console import Console
from rich.spinner import Spinner
import time
console = Console()
def fetch_remote_data() -> None:
with Spinner("dots", text="Fetching remote data...", console=console):
time.sleep(2) # Network call simulation
console.print("[bold green]✓ Data retrieved successfully.")
Handling Nested Progress & Common Errors
Combining multiple progress indicators often triggers rendering conflicts. To resolve overlapping output, use rich.progress.track() for simple loops and reserve Progress contexts for complex, multi-stage workflows. Ensure sys.stdout is not hijacked by logging frameworks. Route logs through rich.logging.RichHandler to maintain spinner integrity and prevent ValueError: I/O operation on closed file during concurrent writes. Avoid mixing tqdm and rich in the same stdout stream.
from rich.progress import track
def heavy_computation(data: list[int]) -> list[int]:
results = []
for val in track(data, description="Computing..."):
results.append(val ** 2)
return results
Environment Configuration & Execution
Pin dependencies to ensure stable Live rendering across environments. The following configuration targets Python 3.10+ and enforces strict version boundaries.
[project]
name = "cli-progress-tool"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = ["rich>=13.0.0"]
Install the package and execute your module directly. Use python -m to guarantee correct relative imports.
pip install rich
python -m your_cli_module