Rich & Textual
Rich makes terminal output readable - colors, tables, progress bars, syntax-highlighted tracebacks. Textual builds on Rich to create full terminal user interfaces (TUIs) with widgets, layouts, and async events. Together they replace brittle print debugging and curses boilerplate.
Recipe
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Deploy Status")
table.add_column("Service")
table.add_column("Version")
table.add_row("api", "2.1.0")
table.add_row("worker", "2.1.0")
console.print(table)When to reach for this:
- CLI tools that operators run daily (deploy, migrate, data fixes)
- Long-running scripts needing progress feedback
- Internal dashboards without a web UI budget
- Pretty error output during local development
Working Example
from __future__ import annotations
import time
from dataclasses import dataclass
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
@dataclass
class Job:
name: str
rows: int
def process_jobs(jobs: list[Job]) -> None:
console = Console(stderr=True)
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TaskProgressColumn(),
console=console,
) as progress:
task_id = progress.add_task("Processing", total=sum(j.rows for j in jobs))
for job in jobs:
progress.update(task_id, description=job.name)
for _ in range(job.rows):
time.sleep(0.01)
progress.advance(task_id)
console.print("[green]Done[/green]")
if __name__ == "__main__":
process_jobs([Job("users", 50), Job("orders", 30)])What this demonstrates:
Console(stderr=True)keeps stdout free for pipingProgresscomposes columns for spinner, label, bar, and percent- Task description updates per job name
- Color markup (
[green]) without manual escape codes
Deep Dive
How It Works
- Console - Detects terminal width, color support, and emoji rendering; falls back gracefully in CI logs.
- Markup -
[bold red]error[/]style tags;console.print(obj)pretty-prints dataclasses and dicts. - Live display -
rich.live.Liverefreshes tables or logs in place without flicker. - Textual - Event-driven TUI framework; apps subclass
App, definecompose()for widgets, handleon_button_pressedetc.
Rich vs Textual
| Tool | Best For |
|---|---|
| Rich | One-shot CLI output, progress, logging beautification |
| Textual | Interactive TUIs: config wizards, log tailers, internal ops consoles |
stdlib curses | Minimal deps on embedded systems only |
Python Notes
# Install Rich tracebacks globally in a CLI entrypoint
from rich.traceback import install
install(show_locals=True)
# Textual minimal app skeleton
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class HelloApp(App):
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello from Textual")
yield Footer()
if __name__ == "__main__":
HelloApp().run()Gotchas
- Printing tables to stdout in piped CLIs - Breaks downstream
jqorgrep. Fix:Console(file=sys.stderr)for human output. - Assuming color in CI - Some runners strip ANSI. Fix:
Console(force_terminal=True)only when needed; test in plain mode. - Textual without async discipline - Blocking
time.sleepin handlers freezes the UI. Fix:await asyncio.sleeporrun_worker. - Huge tables in narrow terminals - Columns truncate confusingly. Fix:
table.add_column(..., overflow="fold")or export CSV. - Rich in library code unconditionally - Forces Rich as a hard dependency for consumers. Fix: optional pretty output behind a
--verboseflag. - Logging handler duplication - Rich
LoggingHandlerplus stdlib handler doubles lines. Fix: configure one handler tree.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
click + plain print | Simple CLIs with minimal formatting | Operators need tables, progress, or live status |
typer | Typed argparse replacement | You need interactive full-screen UIs |
blessed / prompt_toolkit | Input-heavy REPLs | You want declarative widget layouts (Textual) |
| Web UI (FastAPI + HTMX) | Non-technical users need access | SSH-only servers or air-gapped environments |
FAQs
Does Rich work on Windows terminals?
Yes - Rich detects legacy console limitations and degrades gracefully. Windows Terminal supports full color.
Can I use Rich inside pytest?
Yes - Console(record=True) captures output for assertions; avoid progress bars in default test runs.
When should I pick Textual over a small web app?
When operators live in SSH sessions and you need keyboard-driven workflows without hosting another service.
How do I log JSON and still look pretty locally?
Keep production logs as JSON on stdout; attach Rich handler only when LOG_FORMAT=pretty in dev.
Does Textual support mouse clicks?
Yes on supported terminals - enable in app settings; still provide keyboard alternatives.
How do I test a Textual app?
Use textual.pilot async test harness to simulate key presses and assert widget state.
Can Rich render Markdown?
rich.markdown.Markdown renders MD in the terminal - useful for --help extensions.
How do I disable colors for scripting?
Set NO_COLOR=1 in the environment - Rich respects the standard.
Is Rich slow for large outputs?
Rendering 10k rows inline is slow - paginate, sample, or write to a file and open an editor.
Can I embed Rich in FastAPI?
Rich targets terminals, not HTTP - use it in management commands and worker CLIs, not API responses.
Related
- CLI Tools Basics - argparse/typer entrypoints
- Errors & Logging Basics - structured logging patterns
- pytest Setup - testing CLI tools
- Shell Productivity - terminal workflow
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.