Intermediate
You write a script that processes files, hits an API, or runs a batch job — and every status update is a wall of identical white text scrolling past. By the time something goes wrong, you have no idea which line was the warning and which was the success message. Plain print() was fine for debugging in 2010, but your CLI tools deserve better.
Python’s rich library solves this without forcing you to learn curses or terminal escape codes. It gives you colored output, formatted tables, live progress bars, syntax-highlighted code, and beautiful panels — all with the same print()-style API you already know. It requires one pip install and works on Windows, macOS, and Linux.
This article covers the full rich toolkit: styled text and color, the Console object, tables, panels, progress bars, live displays, and Markdown rendering. Each section builds on the last, and by the end you will have built a real file-scanner CLI that looks like it belongs in a professional tool.
Python rich: Quick Example
Here is a minimal working example that shows three of rich‘s most useful features — styled text, a table, and a progress bar — before we dive into how each one works.
# quick_rich.py
from rich.console import Console
from rich.table import Table
from rich.progress import track
import time
console = Console()
# 1. Styled output
console.print("[bold green]Build complete![/bold green] [dim]3 warnings[/dim]")
# 2. A quick table
table = Table(title="Build Results")
table.add_column("File", style="cyan")
table.add_column("Status", style="green")
table.add_column("Time (ms)")
table.add_row("main.py", "OK", "142")
table.add_row("utils.py", "OK", "89")
table.add_row("config.py", "WARN", "201")
console.print(table)
# 3. A progress bar over any iterable
for _ in track(range(5), description="Cleaning up..."):
time.sleep(0.2)
console.print("[bold]Done.[/bold]")
Output:
Build complete! 3 warnings
Build Results
+-----------+--------+----------+
| File | Status | Time (ms)|
+-----------+--------+----------+
| main.py | OK | 142 |
| utils.py | OK | 89 |
| config.py | WARN | 201 |
+-----------+--------+----------+
Cleaning up... 100% |##########| 5/5 [00:01<00:00]
Done.
The key pattern is the Console object -- it is the main entry point for everything rich does. Markup tags like [bold green] are processed by rich and translated into terminal escape codes automatically. You never write an ANSI escape sequence by hand.
Read on to understand how each piece works and when to reach for it. The sections below cover installation, the full markup syntax, tables, panels, progress tracking, and live displays.
What Is rich and Why Use It?
rich is a Python library for rich text and beautiful formatting in the terminal. It was created by Will McGugan and has become one of the most downloaded Python packages -- used internally by tools like Textual, Pytest, and pip itself. The design goal is simple: make terminal output look as good as a web page without making developers learn terminal internals.
The gap it fills is real. Python's built-in print() outputs plain text. The logging module adds timestamps and levels but no visual hierarchy. For scripts that run in terminals, this means errors look identical to info messages, long outputs are impossible to scan, and progress has no visual representation. rich solves all three problems with a clean, Pythonic API.
| Feature | print() / logging | rich |
|---|---|---|
| Colored output | Manual ANSI codes | Markup tags |
| Tables | Manual string alignment | Table class |
| Progress bars | Not available | track() / Progress |
| Panels / boxes | Not available | Panel class |
| Syntax highlighting | Not available | Syntax class |
| Markdown rendering | Not available | Markdown class |
| Tracebacks | Default Python format | install() hook |
Install it with pip before running any examples:
# In your terminal
pip install rich
No extra dependencies are needed. rich detects whether the terminal supports color and falls back to plain text gracefully -- so your script degrades cleanly in CI pipelines or when output is redirected to a file.
The Console Object
Everything in rich flows through the Console class. Think of it as a smarter replacement for Python's built-in print(). You create one instance at the top of your module and use it throughout.
# console_basics.py
from rich.console import Console
console = Console()
# Basic styled print
console.print("Hello, [bold]world[/bold]!")
# Multiple styles combined
console.print("[bold red]ERROR[/bold red]: File not found -- /etc/config.yml")
# Log-style output with timestamp
console.log("Starting batch job...")
# Print to stderr (useful for errors)
err_console = Console(stderr=True)
err_console.print("[red]Something went wrong[/red]")
# Print with a rule (horizontal divider)
console.rule("[bold blue]Section Title[/bold blue]")
console.print("Content below the rule")
Output:
Hello, world!
ERROR: File not found -- /etc/config.yml
[HH:MM:SS] Starting batch job...
Something went wrong
---- Section Title ----
Content below the rule
The markup syntax is intentional BBCode-style: [bold], [red], [italic], [underline], and combinations like [bold green]. Close tags with [/bold] or close everything with [/]. If you need to print a literal bracket without triggering markup, escape it: [[].
console.log() adds an automatic timestamp and the calling filename/line, making it useful as a lightweight replacement for logging.debug() during development. The stderr=True option creates a second console that writes to standard error -- a clean way to separate error output from normal output when your script's stdout gets piped to a file.
Rich Markup Reference
The full markup syntax covers colors by name, hex codes, bold, italic, underline, strikethrough, and links. Here is a practical reference with all the patterns you will use regularly.
# markup_reference.py
from rich.console import Console
console = Console()
# Named colors (16 standard + 256-color + true color)
console.print("[red]red[/red] [green]green[/green] [blue]blue[/blue]")
console.print("[bright_red]bright red[/bright_red] [grey50]grey[/grey50]")
# Hex and RGB colors
console.print("[#ff6b6b]coral[/#ff6b6b] [rgb(100,200,100)]mint green[/]")
# Text styles
console.print("[bold]bold[/bold] [italic]italic[/italic] [underline]underline[/underline]")
console.print("[bold italic]bold italic combined[/bold italic]")
console.print("[strike]strikethrough[/strike] [dim]dimmed text[/dim]")
# Background colors
console.print("[on red]white text on red background[/on red]")
console.print("[black on yellow]black on yellow[/]")
# Clickable hyperlinks (in supporting terminals)
console.print("[link=https://pythonhowtoprogram.com]Python How To Program[/link]")
Output (colors rendered in terminal):
red green blue
bright red grey
coral mint green
bold italic underline
bold italic combined
~~strikethrough~~ dimmed text
[white text on red background]
[black on yellow]
Python How To Program <- clickable in iTerm2, Windows Terminal, etc.
The on prefix sets background color. Hex codes require the # inside brackets. For RGB, use rgb(r,g,b) notation. All named colors come from the standard 256-color terminal palette plus a handful of readable aliases -- run python -m rich.color from your terminal to see the full palette with live previews.
Tables
The Table class renders grid data in a clean box format. It handles column alignment, header styling, row striping, and automatic width calculation -- things that require manual string formatting with print().
# rich_tables.py
from rich.console import Console
from rich.table import Table
console = Console()
# Basic table with column options
table = Table(title="Python Package Versions")
table.add_column("Package", style="cyan", no_wrap=True)
table.add_column("Version", justify="center")
table.add_column("License", justify="right", style="dim")
table.add_column("Status", style="green")
table.add_row("rich", "13.7.0", "MIT", "[green]active[/green]")
table.add_row("httpx", "0.27.0", "BSD", "[green]active[/green]")
table.add_row("pydantic", "2.7.1", "MIT", "[green]active[/green]")
table.add_row("deprecated", "1.2.13", "MIT", "[yellow]unmaintained[/yellow]")
console.print(table)
Output:
Python Package Versions
+------------+---------+---------+-------------+
| Package | Version | License | Status |
+------------+---------+---------+-------------+
| rich | 13.7.0 | MIT | active |
| httpx | 0.27.0 | BSD | active |
| pydantic | 2.7.1 | MIT | active |
| deprecated | 1.2.13 | MIT | unmaintained|
+------------+---------+---------+-------------+
Each column takes a style for the cell text, a justify for alignment ("left", "center", "right"), and no_wrap=True to prevent line wrapping in narrow terminals. You can also use header_style on the column to override the header color independently. Row cells accept the same markup as console.print(), which is how the "active" / "unmaintained" statuses show in different colors without extra code.
For a compact table without borders, pass box=None to the Table constructor. For a minimal line table, pass box=rich.box.SIMPLE -- import rich.box and run python -m rich.box to preview all 20+ box styles.
Panels
A Panel wraps any renderable -- text, a table, another panel -- in a titled border box. It is the quickest way to draw the reader's attention to a specific section of output, such as a final summary or an error report.
# rich_panels.py
from rich.console import Console
from rich.panel import Panel
console = Console()
# Simple text panel
console.print(Panel("Build succeeded in 4.2 seconds.", title="Result", border_style="green"))
# Panel with markup
console.print(Panel(
"[bold red]3 tests failed[/bold red]\n"
" test_auth.py::test_login\n"
" test_api.py::test_rate_limit\n"
" test_db.py::test_connection",
title="[bold red]Test Failures[/bold red]",
border_style="red",
subtitle="Run pytest -v for details"
))
# Nested panels
from rich.columns import Columns
left = Panel("CPU: 42%", title="Resources")
right = Panel("RAM: 1.2 GB", title="Resources")
console.print(Columns([left, right]))
Output:
+---------- Result ----------+
| Build succeeded in 4.2s. |
+----------------------------+
+------- Test Failures -----+
| 3 tests failed |
| test_auth.py::test_login|
| test_api.py::... |
| test_db.py::... |
+--- Run pytest -v for... --+
+-- Resources --+ +-- Resources --+
| CPU: 42% | | RAM: 1.2 GB |
+---------------+ +---------------+
The border_style parameter accepts the same color strings as markup tags -- so a red border for errors and green for success is one parameter change. Columns arranges multiple renderables side by side, which is useful for dashboards or displaying two related summaries at once.
Progress Bars
rich offers two levels of progress tracking: the simple track() function that wraps any iterable, and the full Progress context manager for multi-task and custom displays.
The track() Function
For the common case -- iterating over a list and showing progress -- track() is a one-line replacement for a plain for loop.
# progress_track.py
from rich.progress import track
import time
files = ["data_001.csv", "data_002.csv", "data_003.csv", "data_004.csv", "data_005.csv"]
results = []
for filename in track(files, description="Processing files..."):
time.sleep(0.3) # simulate work
results.append(f"{filename}: done")
print(f"Processed {len(results)} files.")
Output (animated in terminal):
Processing files... 60% |###### | 3/5 [00:00<00:00]
The Progress Context Manager
For multiple simultaneous tasks or custom column layouts, use the Progress context manager directly. This is what tools like pip use for parallel downloads.
# progress_multi.py
from rich.progress import Progress, SpinnerColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
import time
with Progress(
SpinnerColumn(),
"[progress.description]{task.description}",
BarColumn(),
TaskProgressColumn(),
TimeRemainingColumn(),
) as progress:
task_a = progress.add_task("[cyan]Fetching data...", total=100)
task_b = progress.add_task("[magenta]Parsing rows...", total=200)
while not progress.finished:
time.sleep(0.05)
progress.update(task_a, advance=10)
progress.update(task_b, advance=8)
Output (animated, two bars simultaneously):
/ Fetching data... [##########] 100/100 0:00:00
/ Parsing rows... [######## ] 160/200 0:00:02
The Progress columns are composable -- you pick exactly what appears in the progress row. SpinnerColumn shows an animated spinner while a task is running. TimeRemainingColumn estimates how long until completion. For tasks of unknown length (like a network request), pass total=None and the bar pulses instead of filling.
Syntax Highlighting
The Syntax class renders any code block with full syntax highlighting. This is useful in CLI tools that output generated code, configuration snippets, or error context -- anywhere you want the reader to be able to read code without squinting.
# rich_syntax.py
from rich.console import Console
from rich.syntax import Syntax
console = Console()
code = '''
def fibonacci(n: int) -> int:
"""Return the nth Fibonacci number."""
if n <= 1:
return n
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
'''
# Highlight with line numbers and a theme
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)
# Highlight just one region (highlight_lines)
syntax_focus = Syntax(code, "python", theme="monokai", line_numbers=True, highlight_lines={4, 5, 6})
console.print(syntax_focus)
Output:
1 def fibonacci(n: int) -> int:
2 """Return the nth Fibonacci number."""
3 if n <= 1:
4 return n
5 a, b = 0, 1
6 for _ in range(2, n + 1): <-- highlighted
7 a, b = b, a + b
8 return b
The theme parameter accepts any Pygments theme name -- "monokai", "dracula", "github-dark", and many others. highlight_lines takes a set of line numbers and renders them with a contrasting background, useful for pointing the reader to the line that matters. You can also load code from a file using Syntax.from_path("myfile.py").
Markdown Rendering
For CLI tools that output dynamic reports or documentation, rich's Markdown class renders Markdown text into formatted terminal output -- headings, bold, code blocks, bullet lists, and horizontal rules all work.
# rich_markdown.py
from rich.console import Console
from rich.markdown import Markdown
console = Console()
md_text = """
# Deployment Summary
**Environment:** production
**Status:** success
## Files Changed
- `app/main.py` -- updated route handlers
- `app/config.py` -- added new env vars
- `requirements.txt` -- pinned rich==13.7.0
## Next Steps
1. Monitor error rates in Datadog
2. Run smoke tests against production endpoint
3. Notify the team in Slack
---
> Always verify the deployment before notifying stakeholders.
"""
console.print(Markdown(md_text))
Output:
Deployment Summary
==================
Environment: production Status: success
Files Changed
--------
- app/main.py -- updated route handlers
- app/config.py -- added new env vars
- requirements.txt -- pinned rich==13.7.0
Next Steps
--------
1. Monitor error rates in Datadog
2. Run smoke tests against production endpoint
3. Notify the team in Slack
--------
Always verify the deployment before notifying stakeholders.
The terminal rendering is not pixel-perfect HTML -- headings become underlined text, bold stays bold, code blocks get a dark background -- but it is dramatically more readable than printing raw Markdown strings. For tools that generate reports to be read in the terminal, this is a quick win.
Inspect and Pretty Printing
Two more tools you will reach for in debugging: rich.inspect() and rich.pretty.pprint().
# rich_inspect.py
from rich import inspect, pretty
# inspect() shows all attributes and methods of any object
import requests # or any object
response_obj = type("FakeResponse", (), {"status_code": 200, "text": "OK", "headers": {}})()
inspect(response_obj, methods=True)
# pretty.pprint() is a colorful, indented replacement for pprint.pprint
data = {
"users": [
{"id": 1, "name": "Alice", "roles": ["admin", "editor"]},
{"id": 2, "name": "Bob", "roles": ["viewer"]},
],
"total": 2,
"page": 1,
}
pretty.pprint(data)
Output:
+-- FakeResponse ---+
| status_code = 200 |
| text = "OK" |
| headers = {} |
+-------------------+
{
'users': [
{'id': 1, 'name': 'Alice', 'roles': ['admin', 'editor']},
{'id': 2, 'name': 'Bob', 'roles': ['viewer']}
],
'total': 2,
'page': 1
}
inspect(obj) is the replacement for dir(obj) -- it shows attributes and their values in a readable panel, with an optional methods=True flag to include callable methods. pretty.pprint() uses the same indentation logic as the standard library's pprint but adds color to distinguish strings, numbers, keys, and containers at a glance.
Real-Life Example: File Scanner CLI
This example builds a CLI that scans a directory, reports file sizes and types in a table, shows a progress bar while scanning, and summarizes the result in a panel -- everything a real tool would need.
# file_scanner.py
import os
import sys
from pathlib import Path
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.progress import track
def format_size(size_bytes: int) -> str:
"""Return human-readable file size."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 ** 2:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / 1024**2:.1f} MB"
def scan_directory(directory: str) -> list[dict]:
"""Return a list of file info dicts."""
results = []
all_files = list(Path(directory).rglob("*"))
files_only = [f for f in all_files if f.is_file()]
for path in track(files_only, description="[cyan]Scanning files..."):
try:
stat = path.stat()
results.append({
"name": path.name,
"ext": path.suffix or "(none)",
"size": stat.st_size,
"size_fmt": format_size(stat.st_size),
})
except (PermissionError, OSError):
pass # skip unreadable files
return sorted(results, key=lambda x: x["size"], reverse=True)
def build_table(results: list[dict]) -> Table:
"""Build a rich Table from scan results."""
table = Table(title="Top Files by Size", show_lines=True)
table.add_column("File", style="cyan", no_wrap=True)
table.add_column("Extension", style="dim", justify="center")
table.add_column("Size", justify="right", style="yellow")
for row in results[:15]: # show top 15
table.add_row(row["name"], row["ext"], row["size_fmt"])
return table
def main(directory: str = ".") -> None:
console = Console()
console.rule("[bold blue]File Scanner[/bold blue]")
results = scan_directory(directory)
if not results:
console.print(Panel("[red]No files found.[/red]", border_style="red"))
return
console.print(build_table(results))
total_size = sum(r["size"] for r in results)
ext_counts: dict[str, int] = {}
for r in results:
ext_counts[r["ext"]] = ext_counts.get(r["ext"], 0) + 1
top_ext = max(ext_counts, key=lambda k: ext_counts[k])
console.print(Panel(
f"[bold]Total files:[/bold] {len(results)}\n"
f"[bold]Total size:[/bold] {format_size(total_size)}\n"
f"[bold]Most common type:[/bold] {top_ext} ({ext_counts[top_ext]} files)",
title="Summary",
border_style="green",
))
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "."
main(target)
Output (example run on a small project directory):
-------------- File Scanner ---------------
Scanning files... 100% |##########| 24/24
Top Files by Size
+------------------+-----------+----------+
| File | Extension | Size |
+------------------+-----------+----------+
| data.json | .json | 142.3 KB |
| requirements.txt | .txt | 1.8 KB |
| main.py | .py | 3.1 KB |
| config.yaml | .yaml | 0.9 KB |
+------------------+-----------+----------+
+------- Summary ---------+
| Total files: 24 |
| Total size: 148 KB|
| Most common type: .py (12 files)|
+-------------------------+
This project combines every feature from this article: track() for the scan progress, a Table for the file listing, and a Panel for the summary. To extend it, add a --ext filter argument using argparse or Typer, or replace the results[:15] slice with a --top N argument.
Frequently Asked Questions
Why is my rich output showing without color?
rich detects the terminal's color support automatically. If output is piped to a file, another process, or a terminal that reports no color support, rich disables color by default. To force color regardless, pass force_terminal=True to the Console constructor: Console(force_terminal=True). In CI systems like GitHub Actions, rich usually detects color correctly because modern CI terminals set COLORTERM=truecolor.
How do I print a string containing brackets without triggering markup?
Escape literal brackets by doubling them: [[] becomes a printed [ in the output. Alternatively, pass markup=False to console.print() to disable markup processing entirely for that call: console.print("[not markup]", markup=False). The markup=False option is useful when printing user-supplied data that might contain brackets.
Can I integrate rich with Python's standard logging module?
Yes. Import RichHandler from rich.logging and add it to your logger: logging.basicConfig(handlers=[RichHandler()]). This replaces the default plaintext log formatter with rich's colored, time-stamped output, giving you all of rich's styling for log messages without changing any logging.info() calls in your codebase. Add rich_tracebacks=True to the handler for beautifully formatted exception traces.
How do I write rich output to a file instead of the terminal?
Pass a file object to Console: Console(file=open("report.txt", "w")). By default, rich will strip ANSI codes when the output is not a terminal, so the file will contain plain text. To write an HTML file with the colors preserved as CSS, use Console(record=True), call your print statements, then console.save_html("report.html"). This produces a self-contained HTML file that renders the colored output in a browser.
How do I use rich to improve Python exception tracebacks?
Call rich.traceback.install() at the top of your script or entry point. After that, any unhandled exception will display a rich-formatted traceback instead of the default Python traceback -- with syntax highlighting, local variable values printed inline, and the exception message prominently colored. This works globally for the entire process, so one line at the start of main.py is all you need.
How do I update a single line of output in place?
Use rich.live.Live as a context manager. Inside the with block, call live.update(renderable) to replace the displayed content with a new renderable -- a table, a panel, or styled text. The terminal redraws only the changed section, not the whole screen. This is the right tool for dashboards that refresh every few seconds, such as a monitoring script that polls an API and updates metrics in place.
Conclusion
Python's rich library transforms terminal output from an afterthought into a feature. This article covered the Console object and markup syntax for styled text, the Table class for grid data, Panel for framing important output, track() and Progress for progress bars, Syntax for highlighted code, Markdown for formatted reports, and inspect() / pretty.pprint() for debugging. The real-life file scanner brought these together into a tool that looks and behaves like professional software.
Extend the file scanner by adding a --watch flag that re-scans on changes, a JSON export option using Console(file=...), or a live dashboard using rich.live.Live that refreshes every 5 seconds. Each of those features is under 20 lines of additional code once rich is in place.
The official documentation at rich.readthedocs.io covers every class and parameter in detail, including the full list of box styles, Pygments themes, and the complete Markdown rendering specification. Run python -m rich from your terminal for an instant live demo of every feature.