Terminal output doesn't have to be ugly. Rich is a Python library that makes beautiful output easy—tables, progress bars, syntax highlighting, markdown rendering, and more. Your CLI tools can look as polished as any GUI.
Getting Started
pip install richThe simplest upgrade—replace print() with Rich's version:
from rich import print
print("[bold green]Success![/] File saved to [blue underline]output.json[/]")
print({"name": "Owen", "tasks": ["write", "review", "ship"]})Rich's print handles markup for colors and styles, and pretty-prints data structures automatically. Dictionaries, lists, and objects render with syntax highlighting.
Tables That Don't Suck
ASCII tables with hand-aligned columns? Never again.
from rich.table import Table
from rich.console import Console
console = Console()
table = Table(title="Task Status")
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Task", style="white")
table.add_column("Status", justify="center")
table.add_column("Time", justify="right", style="dim")
table.add_row("TSK-001", "Write blog post", "[green]Done[/]", "23m")
table.add_row("TSK-002", "Review PR #142", "[yellow]In Progress[/]", "12m")
table.add_row("TSK-003", "Deploy to prod", "[red]Blocked[/]", "-")
table.add_row("TSK-004", "Update docs", "[dim]Pending[/]", "-")
console.print(table)This renders a bordered, properly-aligned table with colors. The output automatically adapts to terminal width—narrow terminals get truncated columns instead of broken layouts.
Progress Bars That Inform
For long-running operations, Rich's progress bars show what's happening:
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn
import time
with Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeRemainingColumn(),
) as progress:
task1 = progress.add_task("Downloading...", total=100)
task2 = progress.add_task("Processing...", total=100)
while not progress.finished:
progress.update(task1, advance=0.9)
progress.update(task2, advance=0.5)
time.sleep(0.02)Multiple concurrent progress bars, time estimates, spinners—all handled automatically. The bars update in place without flooding your terminal with lines.
For simpler cases, use the track helper:
from rich.progress import track
import time
for item in track(range(100), description="Processing..."):
time.sleep(0.01) # Your actual work hereSyntax Highlighting
Displaying code? Rich highlights it:
from rich.console import Console
from rich.syntax import Syntax
console = Console()
code = '''
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
'''
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)This renders Python code with proper highlighting and line numbers. Great for CLI tools that inspect or display source files.
Markdown in the Terminal
Rich renders Markdown directly:
from rich.console import Console
from rich.markdown import Markdown
console = Console()
md = Markdown("""
# Task Complete
The deployment finished successfully.
## What was deployed
- API server v2.1.0
- Worker processes (3 instances)
- Database migrations
## Next steps
1. Monitor error rates
2. Check latency metrics
3. Announce in #releases
> Note: Rollback available for 24 hours
""")
console.print(md)Headers, lists, blockquotes, code blocks—all render appropriately for the terminal. Perfect for README previews or help text.
Panels and Layout
Group related output in panels:
from rich.console import Console
from rich.panel import Panel
from rich.layout import Layout
console = Console()
# Simple panel
console.print(Panel("This is important!", title="Warning", border_style="yellow"))
# Nested layout for dashboards
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="main"),
Layout(name="footer", size=3)
)
layout["main"].split_row(
Layout(name="left"),
Layout(name="right")
)
layout["header"].update(Panel("My CLI Dashboard", style="bold white on blue"))
layout["left"].update(Panel("Left content"))
layout["right"].update(Panel("Right content"))
layout["footer"].update(Panel("Status: Running", style="dim"))
console.print(layout)Live Displays
For real-time updates—logs, monitoring, status dashboards—use Live:
from rich.live import Live
from rich.table import Table
import time
import random
def generate_table() -> Table:
table = Table(title="Server Status")
table.add_column("Server")
table.add_column("Status")
table.add_column("Load", justify="right")
for i in range(3):
load = random.randint(10, 95)
status = "[green]OK" if load < 80 else "[red]HIGH"
table.add_row(f"server-{i+1}", status, f"{load}%")
return table
with Live(generate_table(), refresh_per_second=4) as live:
for _ in range(20):
time.sleep(0.25)
live.update(generate_table())The table updates in place, no flickering, no scrolling. Perfect for monitoring tools.
Logging Integration
Replace your logging output with Rich's handler:
import logging
from rich.logging import RichHandler
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
handlers=[RichHandler(rich_tracebacks=True)]
)
log = logging.getLogger("myapp")
log.info("Starting up")
log.warning("Cache miss for key [bold]user:123[/]")
log.error("Connection failed")You get colored log levels, timestamps, and—critically—beautiful tracebacks that highlight the actual error with context.
Exception Handling
Speaking of tracebacks:
from rich.console import Console
console = Console()
try:
result = 1 / 0
except Exception:
console.print_exception(show_locals=True)Rich tracebacks show local variables at each frame, syntax-highlighted code context, and clear visual hierarchy. Debugging becomes noticeably easier.
Conditional Formatting
Rich handles non-TTY output gracefully:
from rich.console import Console
console = Console() # Auto-detects if output is a terminal
# When piped to a file, markup is stripped automatically
console.print("[bold red]Error:[/] Something went wrong")When output goes to a pipe or file, Rich strips formatting codes so you get clean text. No manual checking required.
The Console Object
For serious CLI tools, create one console and use it throughout:
from rich.console import Console
console = Console(stderr=True) # Errors go to stderr
def success(msg: str):
console.print(f"[green]✓[/] {msg}")
def error(msg: str):
console.print(f"[red]✗[/] {msg}")
def info(msg: str):
console.print(f"[blue]ℹ[/] {msg}")Rich makes terminal output a feature, not an afterthought. Your users will notice the difference.