Back to Blog
Python

Python Typer Rich Integration and Progress Bars

python typer rich integration and progress bars: Learn to integrate Rich progress bars into Typer CLI applications, covering track, Progress, dynamic updates, and prod...

TyperRichProgress BarsCLITerminal UI
A terminal window with a progress bar filling up, symbolizing Python Typer CLI progress tracking with Rich.

When a Typer CLI command performs a long-running operation—downloading files, processing batches, or running a pipeline—users need visible feedback. Typer already uses Rich for help pages and error rendering, so adding a progress bar is a natural extension. The python typer rich integration and progress bars pattern relies on Rich's progress module, which Typer does not wrap directly but works cleanly alongside it.

The Minimal Progress Bar with track

The simplest way to show progress is Rich's track() function, which wraps an iterable and updates a progress bar automatically. Inside a Typer command, you can use it directly:

import typer from rich.progress import track import time app = typer.Typer() @app.command() def process(items: int = 10): for item in track(range(items), description="Processing"): time.sleep(0.5) if __name__ == "__main__": app()

track() yields each value from the iterable and updates the bar after each iteration. It works well for simple loops where the total length is known. The description parameter appears above the bar. This is the fastest way to add progress without managing a Progress object.

Using Progress for More Control

When you need to update the bar manually—for example, when progress is driven by callbacks or non-sequential work—use the Progress class. You can create a Progress instance, add a task, and update it as work completes.

import typer from rich.progress import Progress, BarColumn, TextColumn import time app = typer.Typer() @app.command() def upload(): with Progress( TextColumn("[bold blue]{task.description}"), BarColumn(), TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), ) as progress: task = progress.add_task("Uploading", total=100) for chunk in range(100): time.sleep(0.02) progress.update(task, advance=1) if __name__ == "__main__": app()

The Progress context manager handles start and stop. add_task returns a task ID, and update advances the completed amount. You can customize the bar with columns like BarColumn, TextColumn, SpinnerColumn, or TimeRemainingColumn. This approach is useful when progress is not a simple for loop.

Integrating with Typer's Console

Typer exposes a console attribute on the Typer instance, but it is a rich.console.Console used internally for help and errors. For progress bars, you typically create your own Console or use Rich's default. However, you can reuse Typer's console to keep styling consistent:

import typer from rich.progress import Progress app = typer.Typer() @app.command() def run(): with Progress(console=app.console) as progress: task = progress.add_task("Working", total=10) for i in range(10): progress.update(task, advance=1) if __name__ == "__main__": app()

Passing console=app.console ensures the progress bar uses the same theme and width settings as Typer's output. This is a clean integration point because Typer's console is already configured for the terminal.

Handling Exceptions and Cleanup

A progress bar that does not close on an exception leaves the terminal in a broken state. The with statement handles this correctly: if an exception occurs inside the block, Rich stops the progress display and re-raises the exception. For manual control without with, call progress.stop() in a finally block.

progress = Progress() progress.start() try: task = progress.add_task("Processing", total=100) # work that may raise except Exception: progress.stop() raise finally: if progress.live.is_started: progress.stop()

In practice, the context manager is simpler and less error-prone. Use it unless you need to keep the progress bar alive across multiple functions.

Performance and Refresh Rate

Updating a progress bar on every iteration of a tight loop can slow down the overall execution, especially when the loop body is fast. Rich batches updates by default, but you can control the refresh rate with the refresh_per_second parameter on Progress. Lowering it reduces terminal writes and CPU usage.

with Progress(refresh_per_second=10) as progress: task = progress.add_task("Fast loop", total=10000) for i in range(10000): progress.update(task, advance=1)

For loops that complete in microseconds, a refresh rate of 10–20 Hz is usually sufficient. The bar will appear smooth while the overhead stays negligible. If you are processing millions of items, consider updating every N iterations instead of every single one.

Multiple Tasks and Nested Progress

Rich supports multiple progress bars simultaneously. This is useful when a command processes several independent files or stages. Each task has its own ID and can be updated independently.

from rich.progress import Progress, SpinnerColumn, BarColumn with Progress(SpinnerColumn(), BarColumn(), TextColumn("{task.description}")) as progress: task1 = progress.add_task("Stage 1", total=100) task2 = progress.add_task("Stage 2", total=100) for i in range(100): progress.update(task1, advance=1) if i % 2 == 0: progress.update(task2, advance=1)

Nested loops can be handled by creating a Progress per level, but that often causes layout issues. A common pattern is to use a single Progress with multiple tasks and update them from the inner loop. For deeply nested work, consider flattening the progress into a single task with a computed total.

Compatibility and Terminal Behavior

Rich's progress bars rely on terminal capabilities such as carriage returns and ANSI escape codes. They work in most modern terminals, including Windows Terminal, VS Code, and standard Unix terminals. When output is redirected to a file or piped, Rich automatically disables the live display and prints a simple line-based output, which prevents garbage from appearing in logs. This behavior is built into Rich and does not require special handling in Typer.

One caveat: if you combine a progress bar with typer.echo or print inside the loop, the output may interleave and corrupt the display. Use progress.console.print() instead, or write log messages to a separate stream. Rich's Console is aware of the live display and will handle output correctly when used through the same console instance.

Advanced Pattern: Progress with Dynamic Totals

Sometimes the total amount of work is not known in advance. Rich allows you to add a task without a total and later set it when the total becomes known. This is useful for streaming operations or when the input size is discovered incrementally.

with Progress() as progress: task = progress.add_task("Reading", total=None) for chunk in stream(): progress.update(task, total=chunk_count, advance=1)

Setting total after the task has started updates the bar's denominator. Until a total is set, the bar shows an indeterminate spinner. This pattern keeps the progress display accurate even when the total is only known after the first few iterations.

For most Typer commands, the track function or a simple Progress with a known total covers the common cases. The integration is lightweight because Typer already depends on Rich, so no extra dependency is required. Choose the approach that matches the control you need over the display and the structure of your work loop.

python typer rich integration and progress bars: Practical U | RYUSLOG DEV