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
pip install rich
Output: (none — exits 0 on success)
uv add rich
Output: resolved + added to pyproject.toml
poetry add rich
Output: updated lockfile + virtualenv install
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.xseries (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— forrich.markdownrenderingpygments— forrich.syntaxsyntax highlighting
Companion packages (install separately):
rich-cli— the standalonerichshell command (PyPI:rich-cli, not part ofrich)textual— the TUI framework built on Rich's renderertyper— argument parsing with Rich-styled--help; Rich is auto-detectedclick-rich— Click integration if you're not using Typerrich-argparse— Rich formatter for stdlibargparseloguru— Rich-integrated logging alternativetqdm— alternative progress bar; Rich's ownProgresscovers most cases
Alternatives
| Package | Trade-off |
|---|---|
colorama | Minimal ANSI-color shim for Windows. Use for "I just need red text"; no tables/progress/tracebacks. |
blessings / blessed | Curses-style terminal control. Lower-level; use when you need direct cursor positioning. |
prompt_toolkit | Interactive input (REPL, autocomplete). Complementary to Rich, not a replacement. |
tqdm | Progress bars only. Rich's Progress covers the same ground with more features but slightly heavier import. |
tabulate | Plain-text tables. Lighter than Rich's Table; no color, no overflow handling. |
termcolor | Pure ANSI escape helpers. Tiny and dependency-free. |
Common gotchas
- Terminal width auto-detect breaks under CI / non-TTY environments. When stdout is piped, Rich defaults to 80 columns (or whatever
COLUMNSenv says). PassConsole(width=120, force_terminal=True)to render fully in CI logs. LiveandProgresscannot nest naïvely. Two simultaneous live renderers fight over the screen. Settransient=Trueon inner displays so they remove themselves on completion.- 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. Useconsole.print(user_input, markup=False)or escape withrich.markup.escape(). rich.traceback.install()hookssys.excepthookglobally. Two libraries both callinginstall()last one wins; uninstall by reassigningsys.excepthook.- Color depth defaults to truecolor on modern terminals. Old
COLORTERMenv detection occasionally misfires — explicitConsole(color_system="256")forces a safe fallback when integrating with legacy log aggregators. rich-cliis a separate PyPI package.pip install richdoes not install therichshell command — that'spip install rich-cli. Confusing naming; both maintained by Textualize.- Logging integration is opt-in. Importing
richdoesn't changelogging.basicConfigbehaviour. Calllogging.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
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:
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
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
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)
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
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.
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:
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.escapemoved to top-levelrich.markup.escape(the old shim still works but is deprecated).Panel.fitno longer acceptspaddingas a positional arg.Layout'sratioparameter becamesize(withratio=retained as an alias for one cycle).
13.x patch releases
- Each release adds new spinners / colours / themes; no removals.
rich.reprdecorator 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.
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:
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; passforce_terminal=Trueif 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 passtransient=Trueto 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:
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--helpfor plain Click without switching to Typer.rich-argparse— Rich formatter for stdlibargparse.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 torich.traceback; install one, not both.rich-toolkit— emerging UX helpers (interactive prompts, menus) on top of Rich.- Jupyter — Rich's
Consoleand 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
| rich | Python floor | Notes |
|---|---|---|
| 13.x | 3.8 | Recent stable; minor breaking changes from 12.x |
| 12.x | 3.7 | Older line; still seen in long-lived pinned deps |
| 11.x | 3.6.3 | Adds Live and Layout; basis for textual |
Terminal compatibility (informal):
| Terminal | Truecolor | 256 | Unicode boxes | Notes |
|---|---|---|---|---|
| iTerm2, Kitty, WezTerm | yes | yes | yes | Best targets |
| macOS Terminal.app | partial | yes | yes | No truecolor on some macOS versions |
| Windows Terminal | yes | yes | yes | Modern target; pre-installed on Win 11 |
| Windows ConHost (old) | no | yes | partial | Rich auto-downgrades on detection |
| Linux TTY (no X) | no | partial | no | Plain 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
loggingwith a plain formatter and only switch to Rich for top-level summaries. - Library code intended for embedding. Don't make
richa hard runtime dep of a library — let the application choose whether to install it. Userich.printonly when you're certain you're a CLI tool. - JSON-structured log outputs.
RichHandleris for humans. For structured logging to a SIEM or log aggregator, usestructlogor stdlibjsonformatter. - 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-coloror setNO_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