Jesse A. Green / notebook

Rich — Administrator & Developer Guide

Generated May 2026 from Textualize/rich at 46cebbb

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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 _RefreshThread for periodic screen updates) and Progress (which inherits from Live). Console itself is not thread-safe — concurrent print() calls from multiple threads will interleave output. Use a threading.Lock or 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.py detects console VT processing support via GetConsoleMode Win32 API
  • _win32_console.py provides LegacyWindowsTerm — a full Win32 console API renderer that maps Rich styles to SetConsoleTextAttribute calls when VT sequences are not available
  • _windows_renderer.py translates Segment streams 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 bypass isatty() detection, and set width=N explicitly since os.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_COLOR is not read as an environment variable directly in all code paths. Instead, Console checks no_color parameter first, then falls back to os.environ.get("NO_COLOR"). If you pass no_color=False explicitly, 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 separate Console instances 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:

  1. Create private Console instances — never import or modify rich.get_console()
  2. Accept a console parameter — let callers inject their own Console for testing or output redirection
  3. Respect NO_COLOR — Rich does this automatically, but document it for your users
  4. 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>=7 due to changes in IPython's display mechanism. Workaround: pin ipykernel<7 or 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: Console is not thread-safe. Concurrent print() calls from multiple threads produce interleaved, garbled output. Either use a lock, or create one Console per thread. The Live and Progress classes use internal locks for their refresh thread, but the underlying Console write is still unprotected if you also call console.print() from another thread while Live/Progress is active.

Warning

Memory with record=True: Every call to console.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 call console.clear() after each export_*() 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:

  1. Cell width calculation (cell_len() in cells.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.
  2. Style combination — Style.__add__() merges style attributes. Called once per span boundary during text rendering.
  3. 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.
  4. Text wrapping — _wrap.py performs 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 in syntax.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() with markup=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() or export_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 segment
  • Segment.apply_style(segments, style) — apply a style to a segment stream
  • Segment.filter_control(segments) — remove control segments
  • Segment.split_lines(segments) — split stream at newlines
  • Segment.set_shape(lines, width, height) — pad/crop to exact dimensions
  • Segment.get_line_length(line) — cell width of a line
  • Segment.get_shape(lines) — measure width/height of line list
  • Segment.simplify(segments) — merge adjacent segments with same style
  • Segment.strip_styles(segments) — remove all styles
  • Segment.strip_links(segments) — remove hyperlink styles
  • Segment.adjust_line_length(line, width) — pad or crop to width
  • Segment.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._length caches 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() and Text.highlight_words() add spans without modifying the underlying text string.
  • Fitting: Text.fit(width) returns a list of Text lines, each no wider than width cells.

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) — distribute total among ratio-weighted items
  • ratio_reduce(total, ratios, maximums, values) — reduce values to fit in total
  • ratio_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 calls pretty.install() and traceback.install()
  • JupyterMixin — base class providing _repr_mimebundle_() that renders Rich output as HTML in Jupyter cells
  • Auto-detection: Console checks for ipywidgets availability and active Jupyter kernel

9.3 Pygments Integration

Rich wraps Pygments for syntax highlighting in rich/syntax.py:

  • Syntax accepts any Pygments lexer name or auto-detects from filename
  • PygmentsSyntaxTheme wraps Pygments themes (e.g., "monokai", "dracula", "github-dark")
  • ANSISyntaxTheme provides a 16-color alternative that doesn't use Pygments themes
  • Token styles are converted from Pygments Token.* types to Rich Style objects

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
  • @click markup tags — parsed by Rich, handled by Textual's event system
  • Segment — Textual's screen buffer is built from Rich Segments
  • Strip — 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 (via rich-click package)
  • pytest — pytest-rich for 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