Rich is a Python library for rendering rich text, tables, progress bars, syntax-highlighted code, tracebacks, and dozens of other formatted elements in the terminal. It is the foundational rendering engine behind Textual and is used by thousands of CLI tools across the Python ecosystem. This guide covers its internal architecture, configuration surface, performance characteristics, security considerations, and extension points at the depth required for library maintainers, framework integrators, and contributors.
1. Architecture Overview
1.1 Design Philosophy
Rich follows a renderable protocol architecture. Every visual element — tables, panels, trees, progress bars, markdown — is a Python object that implements a simple protocol method. The Console class orchestrates rendering by calling that protocol on each object, collecting a stream of Segment tuples, and writing them to the output file descriptor. This design means Rich has no plugin registry, no configuration files, and no runtime services. All behavior is determined by constructor parameters and method calls at the point of use.
1.2 Rendering Pipeline
The full rendering path from console.print(obj) to terminal output:
console.print(*objects)
│
├─ For each object:
│ ├─ If str and markup=True → render_markup() → Text with Spans
│ ├─ If has __rich__() → call it → get renderable
│ ├─ If has __rich_console__() → use directly
│ └─ Else → Pretty(obj) fallback
│
├─ render() called on each renderable
│ ├─ Builds ConsoleOptions (max_width, encoding, etc.)
│ ├─ Calls renderable.__rich_console__(console, options)
│ │ └─ Yields Segment(text, style, control) tuples
│ └─ Applies style inheritance via StyleStack
│
├─ Segment stream post-processing:
│ ├─ Padding / alignment via Align, Padding renderables
│ ├─ Overflow handling (fold, crop, ellipsis)
│ ├─ Cell width normalization (Unicode-aware)
│ └─ Color downsampling (truecolor → 256 → 16 → none)
│
└─ Output:
├─ file.write() for each line buffer
├─ If record=True → also append to internal buffer
└─ If is_jupyter → HTML repr via JupyterMixin
Note
The rendering pipeline is entirely synchronous and single-threaded per Console instance. The only threading in Rich occurs in
Live(which uses_RefreshThreadfor periodic screen updates) andProgress(which inherits fromLive). Console itself is not thread-safe — concurrentprint()calls from multiple threads will interleave output. Use athreading.Lockor dedicate one Console per thread.
1.3 Component Inventory
| Component | Path | Role |
|---|---|---|
| Console | rich/console.py |
Central orchestrator: rendering, recording, export, paging, alternate screen |
| Text | rich/text.py |
Styled text with span-based formatting, wrapping, highlighting |
| Style | rich/style.py |
Immutable style descriptors (color, bold, italic, etc.), combinable via + |
| Segment | rich/segment.py |
Atomic output unit: (text, style, control) named tuple |
| Theme | rich/theme.py |
Named style collections, stackable via ThemeStack |
| Color | rich/color.py |
Color parsing, representation, and downsampling across color systems |
| Markup | rich/markup.py |
BBCode-like [bold red]text[/] parser → Text with spans |
| Table | rich/table.py |
Tabular layout with auto-sizing columns, borders, padding |
| Progress | rich/progress.py |
Multi-task progress display with 14 built-in column types |
| Live | rich/live.py |
Live-updating display region (alternate screen or inline) |
| Layout | rich/layout.py |
Spatial partitioning via Splitter (row/column) tree |
| Syntax | rich/syntax.py |
Pygments-backed code highlighting with line numbers and themes |
| Markdown | rich/markdown.py |
CommonMark rendering via markdown-it-py with 15+ element types |
| Traceback | rich/traceback.py |
Enhanced exception display with source context and locals |
| Pretty | rich/pretty.py |
Structural pretty-printing with depth limits and highlighting |
| Panel | rich/panel.py |
Box-bordered container for any renderable |
| Tree | rich/tree.py |
Recursive tree display with guide lines |
| Columns | rich/columns.py |
Auto-flowing column layout for iterables |
| Prompt | rich/prompt.py |
Interactive input with validation (Prompt, Confirm, IntPrompt, FloatPrompt) |
| Spinner | rich/spinner.py |
Animated spinner using data from _spinners.py |
| Status | rich/status.py |
Spinner + message display (wraps Live) |
| JSON | rich/json.py |
Syntax-highlighted JSON rendering |
| RichHandler | rich/logging.py |
logging.Handler subclass for stdlib integration |
| Highlighter | rich/highlighter.py |
ABC for text pattern highlighting (Regex, Repr, JSON, ISO8601) |
| Bar | rich/bar.py |
Horizontal bar renderable |
| Rule | rich/rule.py |
Horizontal rule with optional title |
| Align | rich/align.py |
Horizontal/vertical alignment wrapper |
| Padding | rich/padding.py |
Whitespace padding wrapper |
| Constrain | rich/constrain.py |
Width-constraining wrapper |
| Screen | rich/screen.py |
Full-screen renderable |
| Emoji | rich/emoji.py |
Emoji name resolution and rendering |
| Control | rich/control.py |
Terminal control sequences (cursor, clear, etc.) |
| Pager | rich/pager.py |
Output paging via system pager |
| Box | rich/box.py |
30+ predefined border styles for panels and tables |
| FileProxy | rich/file_proxy.py |
Wraps file objects to pass through Console |
| AnsiDecoder | rich/ansi.py |
Converts raw ANSI escape sequences back to Rich Text |
| Palette | rich/palette.py |
Color palette matching (nearest color in palette) |
| Measurement | rich/measure.py |
Width measurement protocol |
| Protocol | rich/protocol.py |
is_renderable(), rich_cast() helpers |
| Diagnose | rich/diagnose.py |
report() prints terminal capability diagnostic |
| Export Format | rich/_export_format.py |
HTML and SVG Jinja-style templates for recorded output |
1.4 Internal / Private Modules
| Module | Purpose |
|---|---|
_emoji_codes.py |
Emoji name → Unicode codepoint mapping dictionary |
_emoji_replace.py |
Regex-based emoji shortcode substitution in text |
_extension.py |
IPython %load_ext rich extension loader |
_fileno.py |
Safe get_fileno() that handles missing .fileno() |
_inspect.py |
Inspect renderable: introspect any Python object |
_log_render.py |
LogRender helper used internally by RichHandler |
_loop.py |
loop_first(), loop_last(), loop_first_last() iteration helpers |
_null_file.py |
NullFile — no-op IO object for quiet mode |
_palettes.py |
Standard 16-color and 256-color palette data |
_pick.py |
pick_bool() — first non-None boolean selector |
_ratio.py |
ratio_resolve(), ratio_reduce(), ratio_distribute() — integer layout math |
_spinners.py |
Spinner animation frame data (ported from cli-spinners) |
_stack.py |
Stack(list) — minimal push/pop stack |
_timer.py |
Context manager for timing code blocks |
_win32_console.py |
LegacyWindowsTerm — direct Win32 API console rendering |
_windows.py |
WindowsConsoleFeatures — detect VT support, truecolor |
_windows_renderer.py |
legacy_windows_render() — segment-to-Win32-API translation |
_wrap.py |
Word-wrapping algorithm with CJK and Unicode awareness |
_unicode_data/ |
23 Unicode version cell-width tables for cell_len() |
1.5 Dependency Graph (Internal)
Console ──→ Text ──→ Style ──→ Color ──→ Palette
│ │ │
│ ├──→ Segment ├──→ ColorSystem (enum)
│ └──→ Highlighter └──→ _palettes
│
├──→ Theme ──→ ThemeStack
├──→ Markup ──→ Text
├──→ Measure ──→ Measurement
├──→ Control
├──→ Pager
├──→ _win32_console (Windows legacy path)
└──→ _export_format (record/export)
Table ──→ Console, Text, Style, Segment, Box, Measure, Padding
Progress ──→ Live ──→ Console, _RefreshThread
Syntax ──→ Pygments, Text, Style, Segment
Markdown ──→ markdown-it-py, Text, Style, Syntax (for code blocks)
Traceback ──→ Syntax, Text, Panel, Columns, Highlighter
Pretty ──→ Text, Highlighter, _loop
RichHandler ──→ Console, Text, Highlighter, _log_render
2. System Requirements
2.1 Python Version Support
Rich requires Python 3.8+ (as declared in pyproject.toml). The codebase uses conditional imports from typing_extensions for features not available until Python 3.11.
2.2 Runtime Dependencies
| Package | Version | Purpose |
|---|---|---|
markdown-it-py |
>=2.2.0 |
CommonMark parsing for rich.markdown |
pygments |
>=2.13.0,<3.0.0 |
Syntax highlighting for rich.syntax and code blocks |
typing_extensions |
>=4.0.0,<5.0 |
Backport of TypeAlias, Protocol, etc. (Python < 3.11 only) |
These are the only hard dependencies. Rich has no C extensions, no compiled components, and no system library requirements (with one caveat on Windows — see below).
2.3 Optional / Soft Dependencies
| Package | Used By | Effect If Missing |
|---|---|---|
ipywidgets |
rich.jupyter |
Jupyter display degrades to plain text |
IPython |
rich._extension, rich.pretty |
%load_ext rich unavailable; pretty.install() skips IPython hook |
2.4 Platform-Specific Behavior
Linux / macOS: Full feature set. Color system auto-detected from $TERM and $COLORTERM environment variables. Emoji rendering depends on terminal font support.
Windows: Rich includes a complete Windows compatibility layer:
_windows.pydetects console VT processing support viaGetConsoleModeWin32 API_win32_console.pyprovidesLegacyWindowsTerm— a full Win32 console API renderer that maps Rich styles toSetConsoleTextAttributecalls when VT sequences are not available_windows_renderer.pytranslatesSegmentstreams into legacy Windows console calls- Windows Terminal, PowerShell 7+, and modern
cmd.exe(Windows 10 1607+) support VT sequences natively; legacy console falls back to 16-color Win32 API rendering
Jupyter: Auto-detected via ipywidgets availability. Output rendered as HTML via JupyterMixin._repr_mimebundle_(). Some features (Live, Progress animation, alternate screen) are unavailable or limited.
Databricks: Detected via DATABRICKS_RUNTIME_VERSION environment variable. Affects terminal feature detection in Console.__init__().
Important
When embedding Rich in environments with non-standard file descriptors (e.g., subprocess pipes, socket-backed streams, or serverless runtimes), use
Console(force_terminal=True)to bypassisatty()detection, and setwidth=Nexplicitly sinceos.get_terminal_size()will fail or return incorrect values.
3. Configuration Reference
Rich is configured entirely through constructor parameters and method calls — there are no configuration files. This section documents every configuration surface.
3.1 Environment Variables
Rich reads exactly two environment variables:
| Variable | Default | Effect |
|---|---|---|
UNICODE_VERSION |
"latest" (currently Unicode 15.1) |
Selects the Unicode version table used by cell_len() for calculating character display widths. Affects all width calculations including wrapping, alignment, and table column sizing. Set to e.g. "13.0.0" for terminals with older Unicode support. Tables are in rich/_unicode_data/. |
DATABRICKS_RUNTIME_VERSION |
Not set | When present, Rich treats the environment as a Databricks notebook, adjusting terminal detection logic in Console. |
Additionally, Rich respects the NO_COLOR convention (see no-color.org): when the NO_COLOR environment variable is set (to any value), Console auto-detects no_color=True, disabling all color output while preserving structural formatting.
Important
NO_COLORis not read as an environment variable directly in all code paths. Instead, Console checksno_colorparameter first, then falls back toos.environ.get("NO_COLOR"). If you passno_color=Falseexplicitly, it overrides the environment variable.
3.2 Console Constructor — Complete Parameter Reference
The Console class constructor accepts the following parameters. These control all rendering behavior for that Console instance.
Console(
*,
color_system: Optional[str] = "auto", # "auto"|"standard"|"256"|"truecolor"|"windows"|None
force_terminal: Optional[bool] = None, # Override isatty() detection
force_jupyter: Optional[bool] = None, # Override Jupyter detection
force_interactive: Optional[bool] = None, # Override interactive detection
soft_wrap: bool = False, # If True, don't wrap text (let terminal handle it)
theme: Optional[Theme] = None, # Custom theme for named styles
stderr: bool = False, # Write to stderr instead of stdout
file: Optional[IO[str]] = None, # Explicit output file (overrides stderr)
quiet: bool = False, # Suppress all output
width: Optional[int] = None, # Override detected terminal width
height: Optional[int] = None, # Override detected terminal height
style: Optional[StyleType] = None, # Default style for all output
no_color: Optional[bool] = None, # Disable color (respects NO_COLOR env)
tab_size: int = 8, # Tab stop width
record: bool = False, # Enable recording for export
markup: bool = True, # Enable [markup] parsing in strings
emoji: bool = True, # Enable :emoji: parsing
emoji_variant: Optional[str] = None, # "text"|"emoji" variant selector
highlight: bool = True, # Enable auto-highlighting
log_time: bool = True, # Show timestamp in console.log()
log_path: bool = True, # Show caller path in console.log()
log_time_format: Union[str, FormatTimeCallable] = "[%X]", # Time format
highlighter: HighlighterType = ReprHighlighter(), # Default highlighter
legacy_windows: Optional[bool] = None, # Force legacy Windows rendering
safe_box: bool = True, # Use ASCII box chars on legacy Windows
get_datetime: Optional[Callable[[], datetime]] = None, # Custom clock
get_time: Optional[Callable[[], float]] = None, # Custom monotonic clock
)
3.3 Color System Resolution
When color_system="auto" (the default), Console resolves the color system through this chain:
1. force_jupyter=True → no terminal colors (HTML output)
2. file is not a TTY and force_terminal not set → None (no color)
3. Windows legacy console → ColorSystem.WINDOWS (16 colors via Win32 API)
4. $COLORTERM == "truecolor" or "24bit" → ColorSystem.TRUECOLOR
5. "256color" in $TERM → ColorSystem.EIGHT_BIT (256 colors)
6. Default → ColorSystem.STANDARD (16 colors via ANSI)
The four ColorSystem enum values control how Color objects are downsampled:
| ColorSystem | Value | Colors | Method |
|---|---|---|---|
STANDARD |
1 | 16 | Map to nearest ANSI color |
EIGHT_BIT |
2 | 256 | Map to nearest xterm-256 palette entry |
TRUECOLOR |
3 | 16.7M | Direct RGB \x1b[38;2;R;G;Bm |
WINDOWS |
4 | 16 | Win32 SetConsoleTextAttribute (legacy path) |
3.4 Theme System
Themes are dictionaries mapping style names to Style objects. They are used to customize the appearance of built-in renderables.
from rich.theme import Theme
from rich.console import Console
custom_theme = Theme({
"info": "dim cyan",
"warning": "magenta",
"danger": "bold red",
"repr.number": "bold green", # Override built-in repr highlighting
"progress.percentage": "cyan", # Override progress bar colors
"table.header": "bold magenta", # Override table header style
})
console = Console(theme=custom_theme)
Built-in named styles (defined in rich/default_styles.py) include styles for every renderable: progress.*, bar.*, table.*, tree.*, markdown.*, syntax.*, traceback.*, logging.*, inspect.*, json.*, prompt.*, and more. The complete list is in DEFAULT_STYLES in that module.
Themes stack: console.push_theme(theme) / console.pop_theme() or use the ThemeContext context manager via console.use_theme(theme).
3.5 Traceback Configuration
rich.traceback.install() replaces sys.excepthook globally:
from rich.traceback import install
install(
width=100, # Output width
extra_lines=3, # Source context lines around error
theme="monokai", # Pygments theme for source code
word_wrap=True, # Wrap long lines
show_locals=False, # Show local variables at each frame
locals_max_length=10, # Truncate local collections
locals_max_string=80, # Truncate local strings
locals_hide_dunder=True, # Hide __dunder__ locals
locals_hide_sunder=False,# Hide _sunder locals
indent_guides=True, # Show indent guides in source
suppress=[], # List of modules to suppress frames from
max_frames=100, # Maximum frames to show
)
3.6 Logging Configuration
RichHandler integrates with Python's logging module:
import logging
from rich.logging import RichHandler
logging.basicConfig(
level="NOTSET",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(
rich_tracebacks=True, # Use Rich for exception tracebacks
tracebacks_show_locals=False,
markup=True, # Parse [markup] in log messages
show_time=True,
show_level=True,
show_path=True,
enable_link_path=True, # Make file paths clickable (OSC 8)
highlighter=ReprHighlighter(),
log_time_format="[%X]",
)]
)
3.7 Pretty Printer Configuration
rich.pretty.install() replaces the default REPL display hook:
from rich.pretty import install
install(
console=None, # Use default Console if None
overflow="fold", # "fold"|"crop"|"ellipsis"
crop=False, # Crop output to terminal width
indent_guides=True, # Show indent guides
max_length=None, # Max items to show in collections
max_string=None, # Max string length
max_depth=None, # Max nesting depth
expand_all=False, # Always expand collections
)
4. Deployment & Scaling
Rich is a library, not a service. "Deployment" means embedding it into applications. This section covers integration patterns and scaling considerations.
4.1 Installation
pip install rich
For minimal installs (e.g., Docker images where size matters), Rich's total install footprint including dependencies is approximately 5 MB.
4.2 Integration Patterns
Pattern 1: Global Console Singleton
Rich provides rich.get_console() which returns a module-level default Console. The public API functions rich.print(), rich.print_json(), and rich.inspect() all use this singleton. Reconfigure it with:
import rich
rich.reconfigure(width=120, no_color=True) # Modifies the global console
This pattern suits simple CLI tools. For libraries, create your own Console to avoid polluting the global state.
Pattern 2: Dedicated Console per Component
from rich.console import Console
error_console = Console(stderr=True, style="bold red")
log_console = Console(file=open("output.log", "w"), width=120, no_color=True)
user_console = Console(highlight=True, markup=True)
Pattern 3: Recording and Export
console = Console(record=True)
console.print(table)
console.print(syntax)
html_output = console.export_html(inline_styles=True) # For email/web embedding
svg_output = console.export_svg(title="My Output") # For documentation
text_output = console.export_text() # Plain text
Note
Recording buffers all output in memory. For long-running processes, call
console.clear()periodically to prevent unbounded memory growth, or use separateConsoleinstances for recorded vs. live output.
Pattern 4: File Output (CI/CD, Logging)
with open("report.txt", "w") as f:
console = Console(file=f, width=120, force_terminal=True)
console.print(table) # ANSI codes written to file
# Or without color:
with open("report.txt", "w") as f:
console = Console(file=f, width=120, no_color=True)
console.print(table) # Plain text, still formatted
4.3 Embedding in Libraries
Libraries that depend on Rich should:
- Create private Console instances — never import or modify
rich.get_console() - Accept a
consoleparameter — let callers inject their own Console for testing or output redirection - Respect
NO_COLOR— Rich does this automatically, but document it for your users - Pin to compatible versions — Rich follows semver; pin
rich>=13.0,<15
4.4 Jupyter Integration
Rich auto-detects Jupyter environments and renders as HTML. Force or disable with:
console = Console(force_jupyter=True) # Force HTML rendering
console = Console(force_jupyter=False) # Force terminal rendering in notebook
Warning
Known issue (#3868): Progress bars and Live displays do not work correctly with
ipykernel>=7due to changes in IPython's display mechanism. Workaround: pinipykernel<7or use static output methods instead of animated displays.
5. Operations
5.1 Diagnostic Tools
Terminal capability report:
python -m rich.diagnose
This calls rich.diagnose.report() and prints: Python version, Rich version, detected console dimensions, color system, encoding, and whether the terminal is interactive, a TTY, Jupyter, or in legacy Windows mode. Run this first when debugging rendering issues.
Feature showcase:
python -m rich
Renders a demonstration card showing tables, trees, syntax highlighting, markdown, progress bars, and other renderables. Useful for verifying the terminal supports Rich's output correctly.
5.2 CLI Sub-Modules
Rich ships four CLI-usable modules:
# JSON pretty-printer
python -m rich.json data.json
python -m rich.json data.json --indent 4
# Syntax highlighter
python -m rich.syntax script.py
python -m rich.syntax script.py --theme monokai --line-numbers --wrap --guides
# Markdown renderer
python -m rich.markdown README.md
python -m rich.markdown README.md --code-theme dracula --hyperlinks
# Feature demo
python -m rich
5.3 Common Operational Issues
Caution
Thread safety:
Consoleis not thread-safe. Concurrentprint()calls from multiple threads produce interleaved, garbled output. Either use a lock, or create oneConsoleper thread. TheLiveandProgressclasses use internal locks for their refresh thread, but the underlying Console write is still unprotected if you also callconsole.print()from another thread while Live/Progress is active.
Warning
Memory with
record=True: Every call toconsole.print()appends segments to an internal list when recording is enabled. In long-running applications (servers, daemons), this will leak memory indefinitely. Either disable recording, or callconsole.clear()after eachexport_*()call.
Troubleshooting Checklist:
| Symptom | Cause | Fix |
|---|---|---|
| No colors in output | isatty() returns False (pipe, redirect, CI) |
Console(force_terminal=True) |
| Wrong colors / garbled output | Terminal doesn't support detected color system | Console(color_system="standard") or Console(color_system="256") |
| Broken box characters | Legacy Windows console or wrong encoding | Console(safe_box=True) (default) or use box.ASCII |
| Wide characters misaligned | Terminal's Unicode version differs from Rich's | Set UNICODE_VERSION env var to match terminal |
| Emoji not rendering | Terminal font lacks emoji glyphs | Use emoji_variant="text" or Console(emoji=False) |
| Traceback links don't work | Terminal doesn't support OSC 8 hyperlinks | Normal — links degrade to plain text |
| VSCode Remote SSH tracebacks link to local paths | Known issue (#4036) | File paths in tracebacks point to the remote filesystem; VSCode maps them to local |
Live flickers or corrupts |
Printing to console outside Live context | All output must go through the Live's console while active |
| Progress stalls in Jupyter | ipykernel>=7 compatibility (#3868) |
Pin ipykernel<7 or use non-animated output |
| Elapsed time stuck after restart | Known issue (#3705) | stop_task() then start_task() does not reset elapsed tracking correctly |
5.4 Testing Rich-Dependent Code
Capture output for assertions:
from rich.console import Console
console = Console(file=StringIO(), force_terminal=True, width=80, color_system="truecolor")
console.print("[bold]Hello[/bold]")
output = console.file.getvalue()
assert "\x1b[1mHello\x1b[0m" in output
Strip styles for content-only testing:
console = Console(file=StringIO(), no_color=True, width=80)
console.print(my_table)
plain_output = console.file.getvalue()
assert "Expected Value" in plain_output
Use Console.capture():
console = Console(width=80)
with console.capture() as capture:
console.print("[bold]test[/bold]")
output = capture.get()
6. Performance
6.1 Rendering Cost Model
Rich's rendering is CPU-bound, dominated by:
- Cell width calculation (
cell_len()incells.py) — called for every string segment to calculate display width. Uses a binary search over Unicode version tables in_unicode_data/. This is the single hottest path. - Style combination —
Style.__add__()merges style attributes. Called once per span boundary during text rendering. - Color downsampling — converting truecolor to 256-color or 16-color palettes requires Euclidean distance calculations across the palette. Only triggered when the color system is less than truecolor.
- Text wrapping —
_wrap.pyperforms word-boundary analysis with full Unicode awareness. Cost scales with text length and number of wrap points.
6.2 Performance Characteristics by Renderable
| Renderable | Relative Cost | Bottleneck |
|---|---|---|
Text |
Low | cell_len() for width calculation |
Panel |
Low | Border rendering + inner content |
Table (small) |
Low-Medium | Column width measurement (measures all cells) |
Table (1000+ rows) |
High | Full row measurement pass, segment generation |
Syntax (small file) |
Medium | Pygments tokenization + style mapping |
Syntax (10K+ lines) |
High | Pygments tokenization dominates |
Markdown |
Medium | markdown-it-py parsing + recursive rendering |
Tree (deep) |
Medium | Recursive rendering, guide character computation |
Progress |
Low per-tick | Main cost is the refresh rate, not rendering |
Pretty (deep objects) |
High | traverse() walks entire object graph |
6.3 Optimization Techniques
Reduce unnecessary measurement:
# Pre-set column widths to skip auto-measurement
table = Table()
table.add_column("Name", width=20) # Fixed width, no measurement
table.add_column("Value", ratio=1) # Ratio-based, minimal measurement
Control Pretty traversal depth:
from rich.pretty import Pretty
# Limit depth for large/nested objects
console.print(Pretty(huge_dict, max_depth=3, max_length=50))
Minimize Live/Progress refresh rate:
# Default is 12.5 fps — reduce for lower CPU usage
with Progress(refresh_per_second=4) as progress:
...
Avoid record mode in production:
# Don't use record=True unless you need export
console = Console(record=False) # Default, but be explicit in perf-critical paths
6.4 Startup Cost
Rich's import cost is moderate (~50-100ms on typical hardware) due to:
- Loading
_emoji_codes.py(large dictionary) - Loading
_spinners.py(animation data) - Importing
pygments(Rich imports it at module level insyntax.py) - Loading Unicode cell-width tables
For CLI tools where startup latency matters, use lazy imports:
def render_table(data):
from rich.table import Table # Import only when needed
from rich.console import Console
...
7. Security
7.1 Input Handling
Rich processes untrusted input in several contexts. The security surface is small because Rich is a rendering library with no network, filesystem, or subprocess operations (with one exception: SystemPager).
Warning
Markup injection: If you pass user-controlled strings to
console.print()withmarkup=True(the default), malicious input containing[link=https://evil.com]click here[/link]will render as a clickable hyperlink in terminals that support OSC 8. This could be used for phishing. Mitigations:
- Use
console.print(user_input, markup=False)for untrusted input- Use
rich.markup.escape(user_input)to neutralize markup tags before embedding in markup strings- Use
Text(user_input)which never parses markup
ANSI injection via AnsiDecoder: The AnsiDecoder class in rich/ansi.py parses raw ANSI escape sequences into Rich Text. If the input contains malicious escape sequences (e.g., terminal control codes that rewrite the prompt), AnsiDecoder will convert them into Rich styles but will NOT pass through raw control sequences. However, be aware that AnsiDecoder is a best-effort parser — unusual or malformed sequences may produce unexpected output.
7.2 SystemPager Security
Console.pager() can invoke the system pager (typically less or more):
with console.pager():
console.print(long_text)
This calls subprocess.Popen(pager_command, shell=True) using the $PAGER environment variable. If $PAGER is set to a malicious command, it will execute. This is standard Unix behavior and matches how git, man, and other tools work. In untrusted environments, use console.pager(pager=Pager()) with a custom Pager class that does not invoke subprocesses.
7.3 Export Security
export_html()does not sanitize output for XSS. If Rich renders user-controlled text and you embed the HTML in a web page, standard XSS vectors apply. Sanitize the output before serving.export_svg()embeds text in SVG<text>elements. Same XSS caveat applies if served in a web context.
Caution
Never serve
export_html()orexport_svg()output directly in a web application without sanitization when the rendered content includes user-controlled input. Rich's export is designed for local viewing and documentation, not for untrusted web contexts.
7.4 Dependency Security
Rich has three runtime dependencies. The security surface of each:
pygments— parses source code for tokenization. It does not execute code. No known security concerns for rendering use.markdown-it-py— parses Markdown text. Does not execute embedded code blocks. ReDoS resistance depends on version; keep updated.typing_extensions— type annotation utilities only. No runtime security surface.
8. Developer Internals
8.1 The Rich Console Protocol
This is the core extensibility mechanism. Any Python object can become a Rich renderable by implementing one or more of these methods:
__rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult
The primary protocol. Called by Console.render(). Must yield Segment or other renderable objects. Receives the calling Console and current ConsoleOptions (containing max_width, min_width, encoding, justify, overflow, no_wrap, highlight, markup, height, is_alt_screen).
from rich.console import Console, ConsoleOptions, RenderResult
from rich.segment import Segment
class MyRenderable:
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
yield Segment("Hello, ")
yield Segment("World!", console.get_style("bold"))
yield Segment.line() # newline
__rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement
Optional. Returns the minimum and maximum width the renderable needs. Used by Table and other layout renderables to determine column widths. If not implemented, Console measures by trial-rendering at the maximum width.
from rich.measure import Measurement
class MyRenderable:
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
return Measurement(10, 40) # min=10, max=40 cells
__rich__(self) -> RenderableType
Simplest protocol. Returns any renderable (string, Text, Table, etc.). Used when the object is not itself a renderable but can produce one.
class User:
def __rich__(self) -> str:
return f"[bold]{self.name}[/bold] ({self.email})"
8.2 Segment — The Atomic Output Unit
Segment is a NamedTuple with three fields:
class Segment(NamedTuple):
text: str # The actual characters
style: Optional[Style] = None # Visual style
control: Optional[Tuple[ControlType, ...]] = None # Terminal control codes
Control segments (cursor movement, screen clearing) are distinct from text segments. The Segment class provides 24 static/class methods for stream manipulation:
Segment.line()— newline segmentSegment.apply_style(segments, style)— apply a style to a segment streamSegment.filter_control(segments)— remove control segmentsSegment.split_lines(segments)— split stream at newlinesSegment.set_shape(lines, width, height)— pad/crop to exact dimensionsSegment.get_line_length(line)— cell width of a lineSegment.get_shape(lines)— measure width/height of line listSegment.simplify(segments)— merge adjacent segments with same styleSegment.strip_styles(segments)— remove all stylesSegment.strip_links(segments)— remove hyperlink stylesSegment.adjust_line_length(line, width)— pad or crop to widthSegment.align_top/middle/bottom(lines, width, height, style)— vertical alignment
8.3 Style System Internals
Style is immutable and combinable. Internally it stores attributes as a bitmask for fast comparison and combination:
style = Style(bold=True, color="red")
style2 = Style(italic=True, bgcolor="blue")
combined = style + style2 # bold red on blue, italic
The _Bit descriptor class maps attribute names to bit positions in _set_attributes and _attributes integers. _set_attributes tracks which attributes are explicitly set (vs. inherited), enabling proper style stacking where a child style only overrides what it explicitly declares.
Style parsing: Style.parse("bold red on blue underline") tokenizes the string and maps keywords to style attributes. Color names, hex codes (#ff0000), and RGB (rgb(255,0,0)) are supported. Named styles are resolved through the Console's ThemeStack.
Style caching: Style objects are cached via __hash__. Console maintains a style cache (_style_cache) that maps style strings to resolved Style objects, avoiding repeated parsing.
8.4 Text Internals
Text is Rich's most complex class (54 methods). Key internals:
- Spans: Text stores formatting as a list of
Span(start, end, style)tuples, not inline markup. This allows O(1) style application and efficient slicing. - Cell length tracking:
Text._lengthcaches the cell width. Invalidated on mutation. - Wrapping:
Text.wrap()delegates to_wrap.divide()which finds word boundaries using Unicode line-break properties, then returns indices where breaks should occur. - Highlighting:
Text.highlight_regex()andText.highlight_words()add spans without modifying the underlying text string. - Fitting:
Text.fit(width)returns a list ofTextlines, each no wider thanwidthcells.
8.5 Table Layout Algorithm
Table column sizing follows this algorithm (in Table._calculate_column_widths()):
1. Measure all cells in each column → (min_width, max_width) per column
2. Add column padding to measurements
3. If total max_width <= available_width:
Use max_width for all columns (content fits)
4. Else: use ratio_reduce() to shrink columns proportionally
Each column shrinks from its max toward its min
Columns with ratio=0 are fixed and don't shrink
5. Columns with explicit width= are honored exactly
6. Columns with ratio=N get N proportional shares of remaining space
7. ratio_distribute() handles integer rounding (distributes remainders)
The layout math lives in _ratio.py:
ratio_resolve(total, ratios)— distributetotalamong ratio-weighted itemsratio_reduce(total, ratios, maximums, values)— reduce values to fit in totalratio_distribute(total, ratios, minimums)— distribute total respecting minimums
8.6 Progress Architecture
Progress extends Live and manages a list of Task objects:
Progress
├── Live (handles screen refresh via _RefreshThread)
│ └── Console (actual output)
├── tasks: List[Task] (each has id, total, completed, fields)
├── columns: List[ProgressColumn] (define the visual layout)
└── make_tasks_table() → Table (called on each refresh)
The 14 built-in ProgressColumn subclasses:
| Column Class | Renders |
|---|---|
TextColumn |
Static or templated text |
BarColumn |
The progress bar itself |
SpinnerColumn |
Animated spinner |
TimeElapsedColumn |
Time since task started |
TimeRemainingColumn |
Estimated time to completion |
FileSizeColumn |
Current bytes as human-readable |
TotalFileSizeColumn |
Total bytes as human-readable |
DownloadColumn |
Current/total download progress |
TransferSpeedColumn |
Transfer speed |
MofNCompleteColumn |
"M/N" counter |
TaskProgressColumn |
Percentage text |
RenderableColumn |
Arbitrary renderable |
Custom columns extend ProgressColumn and implement render(task: Task) -> RenderableType.
8.7 Live Display Mechanism
Live uses a background thread (_RefreshThread) that calls Live.refresh() at the configured refresh_per_second rate. The refresh cycle:
_RefreshThread.run():
while not event.wait(1/refresh_per_second):
Live.refresh()
Live.refresh():
1. Get renderable from get_renderable()
2. Render to segments via Console
3. Compute diff from previous render (lines changed)
4. Move cursor up to top of live region
5. Overwrite changed lines
6. Clear any extra lines from previous (longer) render
Live supports two modes:
- Inline (default): Renders below the current cursor position, rewriting in place
- Alternate screen (
screen=True): Uses the terminal's alternate screen buffer
8.8 Markup Parser
rich/markup.py implements a BBCode-like tag parser. The render() function:
Input: "[bold red]Hello [italic]world[/italic]![/bold red]"
Parse: TAG_OPEN("bold red"), TEXT("Hello "), TAG_OPEN("italic"),
TEXT("world"), TAG_CLOSE("italic"), TEXT("!"), TAG_CLOSE("bold red")
Output: Text("Hello world!") with spans:
Span(0, 12, "bold red")
Span(6, 11, "italic")
Supported tag syntax:
[style]text[/style]— named close[style]text[/]— auto-close innermost[link=url]text[/link]— hyperlinks (OSC 8)[@click=handler]text[/@click]— event callbacks (Textual integration)[not bold]— explicit negation- Backslash escaping:
\[not a tag\]
8.9 Color Implementation
Color supports multiple color specifications:
Color.parse("red") # Named (16 standard + X11 colors)
Color.parse("#ff0000") # Hex
Color.parse("rgb(255,0,0)") # RGB tuple
Color.parse("color(196)") # xterm-256 palette index
Color.default() # Terminal default
Color downsampling (truecolor → 256 → 16) uses Palette.match() which computes Euclidean distance in RGB space against the target palette. The _palettes.py module contains the standard 16-color and 256-color palette data.
8.10 Code Patterns and Conventions
Protocol-driven design: Rich uses Python protocols (__rich_console__, __rich_measure__, __rich__) rather than class inheritance for extensibility. This means any object — even from third-party libraries — can be made renderable without subclassing.
Immutable value objects: Style, Segment, Measurement, Color are all immutable (or effectively immutable NamedTuples). This enables safe caching and sharing.
String-based style specification: Throughout the codebase, StyleType = Union[str, Style] is accepted. Strings are lazily parsed via Console.get_style(). This makes the API ergonomic ("bold red") while remaining efficient (parsed once, then cached).
@rich.repr.auto decorator: Applied to Rich's own classes to generate __repr__ methods that use __rich_repr__() for both standard and Rich-formatted output. Classes yield (key, value) or (key, value, default) tuples from __rich_repr__().
Overflow handling: All text rendering supports three overflow modes — fold (break mid-word), crop (truncate), ellipsis (truncate with ...). Controlled via ConsoleOptions.overflow.
_cell_len caching: The Text class caches cell-width calculations aggressively. The cell_len() function itself is optimized with a fast path for ASCII-only strings.
Conditional Windows code: Windows-specific modules (_win32_console.py, _windows.py, _windows_renderer.py) are only imported on Windows via conditional imports in console.py. The legacy_windows flag gates this entire code path.
9. Integration Points
9.1 Python Standard Library Integration
| stdlib Module | Rich Integration | Entry Point |
|---|---|---|
logging |
RichHandler replaces default handler |
rich.logging.RichHandler |
sys.excepthook |
install() replaces exception hook |
rich.traceback.install() |
sys.displayhook |
install() replaces REPL display |
rich.pretty.install() |
warnings |
Can be rendered through Console | Via Console.print() |
io.StringIO |
Console writes to any IO[str] |
Console(file=StringIO()) |
subprocess |
SystemPager uses subprocess.Popen |
Console.pager() |
9.2 IPython / Jupyter Integration
%load_ext rich— loads the Rich IPython extension (_extension.py), which callspretty.install()andtraceback.install()JupyterMixin— base class providing_repr_mimebundle_()that renders Rich output as HTML in Jupyter cells- Auto-detection: Console checks for
ipywidgetsavailability and active Jupyter kernel
9.3 Pygments Integration
Rich wraps Pygments for syntax highlighting in rich/syntax.py:
Syntaxaccepts any Pygments lexer name or auto-detects from filenamePygmentsSyntaxThemewraps Pygments themes (e.g.,"monokai","dracula","github-dark")ANSISyntaxThemeprovides a 16-color alternative that doesn't use Pygments themes- Token styles are converted from Pygments
Token.*types to RichStyleobjects
9.4 Textual Framework Integration
Rich is the rendering engine for Textual, Textual's TUI framework. Integration points:
RenderHook— Console supports render hooks that Textual uses to intercept and redirect output@clickmarkup tags — parsed by Rich, handled by Textual's event systemSegment— Textual's screen buffer is built from Rich SegmentsStrip— Textual extends Rich's rendering with its own Strip class (list of Segments with cached width)
9.5 Third-Party Ecosystem
Libraries that integrate with Rich (non-exhaustive, from issue tracker and community):
- Click —
from rich.click import rich_click(viarich-clickpackage) - pytest —
pytest-richfor test output formatting - Django — Rich tracebacks via
RICH_TRACEBACK=1(community package) - FastAPI / Uvicorn — RichHandler for server logs
- Typer — uses Rich for all output formatting
9.6 Extending Rich
Custom Renderable:
from rich.console import Console, ConsoleOptions, RenderResult
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style
class HeatMap:
def __init__(self, data: list[list[float]]):
self.data = data
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
for row in self.data:
for value in row:
r = int(value * 255)
style = Style(bgcolor=f"rgb({r},0,{255-r})")
yield Segment(" ", style)
yield Segment.line()
def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
width = len(self.data[0]) * 2 if self.data else 0
return Measurement(width, width)
Custom Highlighter:
from rich.highlighter import RegexHighlighter
class LogHighlighter(RegexHighlighter):
base_style = "log."
highlights = [
r"(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})",
r"(?P<level>ERROR|WARN|INFO|DEBUG)",
r"(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})",
]
# Requires theme styles: "log.timestamp", "log.level", "log.ip"
Custom Progress Column:
from rich.progress import ProgressColumn, Task
from rich.text import Text
class GPUColumn(ProgressColumn):
def render(self, task: Task) -> Text:
gpu_pct = task.fields.get("gpu", 0)
return Text(f"GPU: {gpu_pct}%", style="bold cyan")
9.7 Known Issues and Caveats for Integrators
| Issue | Details | Status |
|---|---|---|
| #4071 | Legacy Windows layer always uses stdout handle even when Console targets stderr | Open |
| #4028 | ConsoleOptions formatting overrides (justify, overflow) not propagated to nested renderables in all cases |
Open |
| #3887 | Highlighter applied by Console may conflict with highlighter set on RichHandler, producing double-highlighting | Open |
| #2993 | Text.markup property fails to properly escape backslashes when reconstructing markup from spans |
Open |
| #2073 | Pretty-printed items with multi-line __repr__ are not indented correctly at depth > 1 |
Open |
| #3820 | Panel(Columns(...), expand=False) ignores expand=False — Panel always expands to full width |
Open |
| #3701 | console.print() with zero arguments does not print a blank line (differs from builtins.print() behavior) |
Open |