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 rich

The 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 here

Syntax 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.

React to this post: