Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read

Better Terminal Output for Python Ops Tools with rich

Tables, progress bars, colored logs, and readable tracebacks. How the rich library turns a wall of print() statements into a CLI your team enjoys using.

  • #python
  • #bash
  • #cli

The internal tools I build live or die on how readable their output is. A kubectl-style status command that dumps a flat wall of print() lines gets ignored; the same data in a clean table gets used. For a long time I wrote my own ANSI escape codes and column-padding logic to bridge that gap, which was miserable. Then I found rich, and I deleted most of that code.

rich is a Python library for beautiful terminal output: tables, progress bars, syntax highlighting, colored logging, and tracebacks you can actually read. Here is how I use it in real ops tooling, and where an AI assistant fits into building it out.

Install and the Console object

pip install rich

Everything starts with a Console. Think of it as a smarter print that understands markup, colors, and terminal width:

from rich.console import Console

console = Console()
console.print("[bold green]Deploy succeeded[/bold green]")
console.print("[yellow]warning:[/] disk at 82%")

The [bold green]...[/bold green] markup is rendered as styling on a real terminal and stripped automatically when output is piped to a file or a non-TTY. That last part matters: your logs stay clean when redirected.

Tables that format themselves

This is the feature I reach for constantly. Building a table is declarative, and column widths sort themselves out:

from rich.table import Table
from rich.console import Console

console = Console()
table = Table(title="Pod Status")
table.add_column("Name", style="cyan")
table.add_column("Ready", justify="center")
table.add_column("Restarts", justify="right", style="magenta")

table.add_row("api-7f9", "✓", "0")
table.add_row("worker-2a1", "✗", "14")

console.print(table)

No manual padding, no guessing column widths, no breaking when a value is longer than expected. For status dashboards inside a CLI this alone justifies the dependency.

Progress bars for long operations

When a script processes a few hundred items or downloads a batch of files, a progress bar turns “is this hung?” into visible, reassuring motion:

from rich.progress import track
import time

for host in track(hosts, description="Checking hosts..."):
    check_host(host)

track() wraps any iterable and renders a live bar with a count and ETA. For more control — multiple concurrent bars, custom columns — there is a full Progress API, but track() covers most needs.

Pro Tip: rich automatically suppresses animations when output is not a TTY, so a progress bar in a cron job logs a single clean summary line instead of thousands of redraw escape sequences. You get a nice interactive experience without garbage in your log files.

Readable tracebacks

When an ops script crashes, the default Python traceback is a dense wall of frames. rich can render tracebacks with syntax highlighting and local variables, which cuts debugging time noticeably:

from rich.traceback import install

install(show_locals=True)

Call that once at startup and every uncaught exception gets the pretty treatment. The show_locals=True option prints the local variables at each frame — invaluable when a script fails on input you cannot reproduce.

Be careful with show_locals=True in anything that handles secrets, though: it will happily print the value of a variable holding a token. I leave it off in production paths and only flip it on for local debugging.

Colored, structured logging

rich ships a logging handler that gives you timestamps, level colors, and clickable file links for free:

import logging
from rich.logging import RichHandler

logging.basicConfig(
    level=logging.INFO,
    format="%(message)s",
    handlers=[RichHandler(rich_tracebacks=True)],
)
log = logging.getLogger("deploytool")

log.info("Starting deploy")
log.warning("Retrying connection")
log.error("Deploy failed")

Because it is a standard logging handler, you can keep a plain handler for file output and use RichHandler only for the console. Your files stay grep-able while humans get color.

Live status displays with Live and Status

For commands that do work in phases, a spinner or a continuously-updating panel beats printing a new line every second. Status gives you a spinner with a message you can change as the work moves:

from rich.console import Console
import time

console = Console()
with console.status("[bold green]Connecting...") as status:
    time.sleep(1)
    status.update("[bold green]Fetching inventory...")
    time.sleep(1)
    status.update("[bold green]Validating...")
    time.sleep(1)
console.print("[green]Done[/green]")

The spinner animates while the block runs and disappears cleanly when it exits. For a fully custom dashboard that redraws a table in place — say, live pod status that refreshes every few seconds — rich.live.Live lets you replace a renderable repeatedly without scrolling the terminal. I use it for “watch”-style commands that would otherwise spam the scrollback.

Panels and rules for structure

Long output is easier to scan when it is visually grouped. Panel boxes a block of content with an optional title, and rule draws a labeled horizontal divider:

from rich.console import Console
from rich.panel import Panel

console = Console()
console.rule("[bold]Pre-flight checks")
console.print(Panel("All 12 nodes reachable\nDisk headroom: 38%",
                    title="Cluster", border_style="green"))
console.rule("[bold]Deploy")

These are small touches, but in a tool teammates run dozens of times a day, the structure is what makes output skimmable instead of a scroll-back wall.

Keeping it honest in pipelines

The one rule I follow: never let pretty output change the data your tool emits when machines are consuming it. If a downstream script parses your output, give it a --json flag that bypasses rich entirely:

import json

if args.json:
    print(json.dumps(results))
else:
    console.print(build_table(results))

Pretty output is for humans; structured output is for pipes. Supporting both keeps your tool friendly and scriptable.

Letting AI build the boilerplate

Wiring up tables, columns, and progress bars is repetitive, and an AI assistant is genuinely good at it. I will describe my data shape to Claude or Cursor and ask for a rich.Table with the right columns and justification, or a track() loop around an existing for-loop.

It is a fast junior engineer for this kind of formatting work, but I still review what it produces before it ships, because output code can leak data. Specifically I check:

  • That show_locals=True is not enabled on any path that touches secrets.
  • That a --json/non-TTY path exists so the tool stays scriptable.
  • That no real credentials were pasted into the prompt to “show the data” — I hand the AI a fake row, never a real token.

I keep those notes in the prompt workspace and route output-heavy tools through the code review dashboard before release.

Conclusion

rich replaces hand-rolled ANSI codes and column math with declarative tables, drop-in progress bars, colored logging, and readable tracebacks — and it gracefully degrades to clean output when piped. Keep a structured --json path for machines, keep show_locals away from secrets, and let an AI assistant draft the formatting while you review the parts that handle real data. Your internal tools will go from ignored to genuinely pleasant.

More in the Bash and Python automation category. Reusable starters are in the prompt library, and curated sets are in the prompt packs.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.