cheat sheet

rich

Package-level reference for rich on PyPI — install variants, single-file design, version policy, and alternatives.

rich

What it is

Rich is a Python library for rich text and beautiful formatting in the terminal — styled colors, tables, panels, trees, syntax-highlighted code, progress bars, and live displays. It was created by Will McGugan in 2020 and is now developed by Textualize Inc. alongside the textual TUI framework, which builds on Rich's renderer.

Reach for rich whenever a CLI script needs more than print() — ANSI color, columnar output, progress, or pretty tracebacks. Reach for textual when you want a full interactive TUI; reach for colorama if you only need basic cross-platform color and nothing else; reach for click / typer for CLI argument parsing (both integrate cleanly with Rich).

Install

bash
pip install rich

Output: (none — exits 0 on success)

bash
uv add rich

Output: resolved + added to pyproject.toml

bash
poetry add rich

Output: updated lockfile + virtualenv install

bash
pipx install rich-cli    # the standalone `rich` command-line tool (separate PyPI package)

Output: installs rich CLI on PATH for piping colored output from the shell

Versioning & Python support

  • Current stable line is the 13.x series (as of late 2025). Rich has had a roughly annual major release cadence with very few breaking changes.
  • Supports Python 3.8+ on recent releases.
  • Loose semver — patch releases are bug fixes, minor releases add features. Major bumps drop EOL Python versions and remove APIs deprecated for at least a year.
  • The library is intentionally single-package — everything ships in rich, no per-feature extras.

Package metadata

  • Maintainer: Textualize Inc. (Will McGugan)
  • Project home: github.com/Textualize/rich
  • Docs: rich.readthedocs.io
  • PyPI: pypi.org/project/rich
  • License: MIT
  • Governance: Commercially backed (Textualize) with permissive OSS license
  • First released: 2020
  • Downloads: tens of millions per month — consistently in the PyPI top 50

Optional dependencies & extras

Rich is deliberately a single package with minimal dependencies — no extras_require markers. Core install pulls only:

  • markdown-it-py — for rich.markdown rendering
  • pygments — for rich.syntax syntax highlighting

Companion packages (install separately):

  • rich-cli — the standalone rich shell command (PyPI: rich-cli, not part of rich)
  • textual — the TUI framework built on Rich's renderer
  • typer — argument parsing with Rich-styled --help; Rich is auto-detected
  • click-rich — Click integration if you're not using Typer
  • rich-argparse — Rich formatter for stdlib argparse
  • loguru — Rich-integrated logging alternative
  • tqdm — alternative progress bar; Rich's own Progress covers most cases

Alternatives

PackageTrade-off
coloramaMinimal ANSI-color shim for Windows. Use for "I just need red text"; no tables/progress/tracebacks.
blessings / blessedCurses-style terminal control. Lower-level; use when you need direct cursor positioning.
prompt_toolkitInteractive input (REPL, autocomplete). Complementary to Rich, not a replacement.
tqdmProgress bars only. Rich's Progress covers the same ground with more features but slightly heavier import.
tabulatePlain-text tables. Lighter than Rich's Table; no color, no overflow handling.
termcolorPure ANSI escape helpers. Tiny and dependency-free.

Common gotchas

  1. Terminal width auto-detect breaks under CI / non-TTY environments. When stdout is piped, Rich defaults to 80 columns (or whatever COLUMNS env says). Pass Console(width=120, force_terminal=True) to render fully in CI logs.
  2. Live and Progress cannot nest naïvely. Two simultaneous live renderers fight over the screen. Set transient=True on inner displays so they remove themselves on completion.
  3. Markup syntax conflicts with bracketed user input. console.print(user_input) interprets [red]...[/red] style markup — if a user types [whatever], Rich raises a markup error. Use console.print(user_input, markup=False) or escape with rich.markup.escape().
  4. rich.traceback.install() hooks sys.excepthook globally. Two libraries both calling install() last one wins; uninstall by reassigning sys.excepthook.
  5. Color depth defaults to truecolor on modern terminals. Old COLORTERM env detection occasionally misfires — explicit Console(color_system="256") forces a safe fallback when integrating with legacy log aggregators.
  6. rich-cli is a separate PyPI package. pip install rich does not install the rich shell command — that's pip install rich-cli. Confusing naming; both maintained by Textualize.
  7. Logging integration is opt-in. Importing rich doesn't change logging.basicConfig behaviour. Call logging.basicConfig(handlers=[RichHandler()]) explicitly.

Real-world recipes

Package-level recipes — patterns where TTY detection, CI integration, or wrapping matters. For the full Markup/Table/Tree/Progress API see the companion Python article.

Recipe 1 — a CI-friendly status table

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

console = Console(width=100, force_terminal=True)

t = Table(title="Build status")
t.add_column("Stage", style="cyan")
t.add_column("Result")
t.add_row("lint",    "[green]passed[/green]")
t.add_row("test",    "[green]passed[/green]")
t.add_row("deploy",  "[red]failed[/red]")

console.print(t)

Output:

text
              Build status
┏━━━━━━━━┳━━━━━━━━━┓
┃ Stage  ┃ Result  ┃
┡━━━━━━━━╇━━━━━━━━━┩
│ lint   │ passed  │
│ test   │ passed  │
│ deploy │ failed  │
└────────┴─────────┘

force_terminal=True keeps the colours and box-drawing in piped CI logs.

Recipe 2 — progress with subtasks

python
from rich.progress import Progress, BarColumn, TextColumn, TimeRemainingColumn

with Progress(
    TextColumn("[progress.description]{task.description}"),
    BarColumn(),
    "[progress.percentage]{task.percentage:>3.0f}%",
    TimeRemainingColumn(),
) as progress:
    overall = progress.add_task("[cyan]overall", total=100)
    for batch in range(10):
        sub = progress.add_task(f"[green]batch {batch}", total=10)
        for i in range(10):
            # do work
            progress.update(sub, advance=1)
            progress.update(overall, advance=1)
        progress.remove_task(sub)

Output: a stacked-bar progress display with ETA, overall + per-batch bars; per-batch bars disappear as each batch completes. Cleaner than tqdm nested bars for hierarchical work.

Recipe 3 — drop-in for logging with rich tracebacks

python
import logging
from rich.logging import RichHandler
from rich.traceback import install

install(show_locals=True)             # pretty tracebacks

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

log = logging.getLogger("app")
log.info("starting")
log.warning("disk %d%% full", 87)
try:
    1 / 0
except ZeroDivisionError:
    log.exception("math failed")

Output: time-prefixed coloured log lines; the log.exception call produces a fully-styled traceback with local-variable context.

Recipe 4 — a Live dashboard (Layout + refresh)

python
import time
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.panel import Panel

console = Console()
layout = Layout()
layout.split_column(
    Layout(name="header", size=3),
    Layout(name="body"),
    Layout(name="footer", size=3),
)
layout["body"].split_row(Layout(name="left"), Layout(name="right"))

with Live(layout, refresh_per_second=4, console=console):
    for i in range(20):
        layout["header"].update(Panel(f"Tick {i}", style="cyan"))
        layout["left"].update(Panel(f"Workers: {i * 3}"))
        layout["right"].update(Panel(f"Errors: {max(0, i - 10)}"))
        layout["footer"].update(Panel("Press ^C to exit"))
        time.sleep(0.25)

Output: a four-panel dashboard updating in place. Stop with Ctrl-C; the screen restores cleanly.

Recipe 5 — terminal-aware output for a CLI tool

python
import sys
from rich.console import Console

# Auto-detect: human terminal → coloured boxes; piped → plain text
console = Console(
    no_color = not sys.stdout.isatty(),
    force_terminal = sys.stdout.isatty(),
)

if console.is_terminal:
    console.rule("[bold]Results")
    console.print("[green]done[/green]")
else:
    print("done")           # piped to grep, no markup

Output: styled in a terminal, plain when piped. Default Rich behaviour is close to this but doesn't hurt to be explicit.

Performance tuning

Defer Rich imports for fast --help

Rich's full import (Console, Table, Progress, Syntax, Markdown) takes ~80-150 ms. For a CLI where --help should be instant, import inside the function that needs it, not at module top.

python
def cmd_status():
    from rich.console import Console
    from rich.table import Table
    ...

Output: python tool.py --help stays under 50 ms; only python tool.py status pays the Rich import cost.

High-frequency progress

Progress refreshes 10 times/sec by default; on very tight inner loops the formatting is significant. Tune via refresh_per_second:

python
with Progress(refresh_per_second=2) as progress:    # 2 Hz; cheaper
    ...

Output: smoother CPU usage; bar updates twice a second.

Capture vs print for testing

console.capture() redirects output into a string instead of stdout — useful in tests, but each capture allocates a buffer. Re-use one Console(record=True) for many tests.

Truecolor vs 256-color

Truecolor produces ~3x more ANSI escapes per character than 256-color. For massive logs, Console(color_system="256") halves the output size with no perceptible quality drop for typical messages.

Version migration guide

Rich's API has been remarkably stable since 11.x. Breaking changes are rare and clearly announced; deprecations stay for ~6 months. Notable migration points:

12 → 13

  • markup.escape moved to top-level rich.markup.escape (the old shim still works but is deprecated).
  • Panel.fit no longer accepts padding as a positional arg.
  • Layout's ratio parameter became size (with ratio= retained as an alias for one cycle).

13.x patch releases

  • Each release adds new spinners / colours / themes; no removals.
  • rich.repr decorator stabilised — __rich_repr__ is now the canonical way to add Rich-friendly repr.

Pinning advice

  • Library authors: pin permissively (rich>=12,<14). The API is stable enough that floating is reasonable.
  • Application authors: pin tighter (rich==13.7.1) for byte-exact CI logs; output formatting tweaks across minor versions can break golden-output tests.

Companion rich-cli

rich-cli (the standalone shell command) is on a slower cycle than rich itself. Pinning the two together is unnecessary — the CLI re-imports rich at runtime.

Testing & CI integration

Rich's Console(record=True) and Console(file=...) make it straightforward to test styled output without ANSI parsing.

python
from io import StringIO
from rich.console import Console
from rich.table import Table

def test_table_render():
    out = StringIO()
    console = Console(file=out, force_terminal=False, width=40)
    t = Table()
    t.add_column("name")
    t.add_row("alice")
    console.print(t)
    text = out.getvalue()
    assert "alice" in text
    assert "name" in text

Output: plain-text Rich output captured into a string; assertions are substring checks.

For golden-snapshot tests with styles intact:

python
from rich.console import Console

console = Console(record=True, width=80)
console.print("[red]hello[/red]")
svg = console.export_svg(title="test")
html = console.export_html(inline_styles=True)

Output: Rich exports its console buffer as SVG or HTML — diff-friendly snapshots for terminal output.

CI-mode helpers:

  • console.is_terminal — false in CI by default; pass force_terminal=True if you want ANSI in the log.
  • RichHandler(rich_tracebacks=True) in logging — adds rich tracebacks to test failures without changing test code.
  • Disable animation in CI: Progress(disable=True) or pass transient=True to keep CI logs flat.

Security considerations

Rich's markup mini-language is the main consideration. console.print("[bold]" + user_input + "[/bold]") will misparse if user_input contains [anything]. Always escape:

python
from rich.markup import escape
console.print(f"[bold]{escape(user_input)}[/bold]")

Output: literal brackets in user_input are rendered as-is rather than interpreted as markup. The same applies to log messages: log.info("got %s", user_input) is fine because RichHandler escapes args; log.info(f"got [bold]{user_input}[/bold]") is not.

Logging sensitive data

RichHandler by default doesn't redact anything. PII redaction must happen in the formatter or before passing values to log calls — same as with stdlib logging.

Traceback show_locals

rich.traceback.install(show_locals=True) is fantastic for debugging but dumps every local variable to stderr on a crash — including secrets that happen to be in scope. Disable for production: install() (no show_locals).

Ecosystem integrations

  • typer — argparse-style CLI builder by the FastAPI author. Auto-detects Rich and uses styled --help.
  • click-rich — Rich-styled --help for plain Click without switching to Typer.
  • rich-argparse — Rich formatter for stdlib argparse.
  • loguru — alternative logging library; integrates with Rich via a sink.
  • pytest-rich — Rich-styled pytest output.
  • textual — TUI framework built on Rich's renderer; widgets, layouts, mouse, async.
  • pretty-errors, better-exceptions — alternatives to rich.traceback; install one, not both.
  • rich-toolkit — emerging UX helpers (interactive prompts, menus) on top of Rich.
  • Jupyter — Rich's Console and most renderables ship _repr_html_() and _repr_mimebundle_() so they display correctly in notebooks.
  • tqdm — overlapping use case; not an integration, but the two can coexist as long as you don't run them at the same time on the same TTY.

Troubleshooting common errors

MarkupError: closing tag ... does not match any open tag

User-supplied or interpolated text contains [...] segments that Rich parses as markup. Escape with rich.markup.escape() or pass markup=False to the print/log call.

Bar / table looks ugly in CI logs

Stdout isn't a TTY, so Rich falls back to plain text. Force styled output with Console(force_terminal=True, width=120). Some CI viewers don't interpret ANSI escapes — turn off color with Console(no_color=True) and rely on box-drawing alone.

"Live and Progress cannot be used simultaneously"

Two live renderers fight for the screen. Move one inside the other, or use Progress with transient=True so it tears down at completion.

Notebook output shows raw escape sequences

Old IPython versions or non-Jupyter notebook hosts may not render Rich's HTML repr. Use from rich.jupyter import print or call console.print(...) directly — Rich detects Jupyter and emits HTML automatically in most cases.

rich-cli is not on PATH after pip install rich

rich-cli is a separate PyPI package. Install it: pip install rich-cli (or pipx install rich-cli for a global install).

Compatibility matrix

richPython floorNotes
13.x3.8Recent stable; minor breaking changes from 12.x
12.x3.7Older line; still seen in long-lived pinned deps
11.x3.6.3Adds Live and Layout; basis for textual

Terminal compatibility (informal):

TerminalTruecolor256Unicode boxesNotes
iTerm2, Kitty, WezTermyesyesyesBest targets
macOS Terminal.apppartialyesyesNo truecolor on some macOS versions
Windows TerminalyesyesyesModern target; pre-installed on Win 11
Windows ConHost (old)noyespartialRich auto-downgrades on detection
Linux TTY (no X)nopartialnoPlain text fallback

Rich respects the NO_COLOR env var convention and auto-detects via COLORTERM / TERM.

When NOT to use this

  • Tight inner loops with millions of log lines per second. Rich's formatting overhead (markup parsing, terminal-detection per call) adds up. Use stdlib logging with a plain formatter and only switch to Rich for top-level summaries.
  • Library code intended for embedding. Don't make rich a hard runtime dep of a library — let the application choose whether to install it. Use rich.print only when you're certain you're a CLI tool.
  • JSON-structured log outputs. RichHandler is for humans. For structured logging to a SIEM or log aggregator, use structlog or stdlib json formatter.
  • Headless servers where logs go straight to a file. Rich's auto-detection mostly does the right thing, but a small amount of unnecessary work happens per call. For pure file logging, prefer stdlib.
  • Cross-language ANSI parity matters. If a Go or Rust tool reads your Python tool's output, Rich's styling may confuse the parser. Pass --no-color or set NO_COLOR=1 (Rich respects this).

See also

  • Python: rich — Console, tables, progress, live displays, syntax
  • Packages: pip-tqdm — alternative for simple progress bars
  • Linux: bat — Rust analogue for syntax-highlighted file viewing in the shell