How Rich Detects and Renders Output in Jupyter Notebooks: Inside the Textualize Source Code

Rich automatically detects Jupyter environments using the _is_jupyter() helper in rich/console.py and renders styled HTML via the JupyterMixin._repr_mimebundle_() method in rich/jupyter.py, while falling back to ANSI terminal codes in standard IPython sessions.

The Textualize/rich library solves the challenge of producing beautiful terminal output across different Python environments. When your code runs inside a Jupyter notebook, Google Colab, or Databricks, Rich detects this context and switches from ANSI escape sequences to HTML-based rendering, ensuring your tables, panels, and syntax highlighting remain visually consistent regardless of the runtime.

Detecting a Jupyter Environment

Rich determines whether it is operating inside a Jupyter-compatible environment using environment inspection rather than simple module presence checks. This detection happens during Console instantiation and drives all subsequent rendering decisions.

The _is_jupyter() Implementation

The private helper _is_jupyter() in rich/console.py performs the actual environment detection. It first checks for the presence of get_ipython, a builtin available in IPython contexts but absent in standard Python interpreters.


# rich/console.py

def _is_jupyter() -> bool:  # pragma: no cover

    """Check if we're running in a Jupyter notebook."""
    try:
        get_ipython  # type: ignore[name-defined]

    except NameError:
        return False
    ipython = get_ipython()  # type: ignore[name-defined]

    shell = ipython.__class__.__name__
    if (
        "google.colab" in str(ipython.__class__)      # Colab

        or os.getenv("DATABRICKS_RUNTIME_VERSION")   # Databricks

        or shell == "ZMQInteractiveShell"            # Jupyter or qtconsole

    ):
        return True
    elif shell == "TerminalInteractiveShell":
        return False   # plain IPython terminal

    else:
        return False   # any other interpreter

The function specifically identifies ZMQInteractiveShell (standard Jupyter), google.colab classes, and Databricks runtimes via environment variables. It explicitly returns False for TerminalInteractiveShell, ensuring that plain IPython terminals receive ANSI output rather than HTML.

Console Initialization Flag

When you instantiate a Console, the constructor calls _is_jupyter() unless you explicitly override detection with the force_jupyter parameter.


# rich/console.py – inside Console.__init__

self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter

This boolean flag (self.is_jupyter) determines the entire rendering pipeline. When True, Rich prepares MIME bundles for IPython's display system; when False, it writes ANSI escape codes directly to sys.stdout.

The Jupyter Rendering Pipeline

Rich provides two complementary mechanisms for Jupyter output defined in rich/jupyter.py: the JupyterMixin protocol integration and the low-level display() helper.

JupyterMixin and the _repr_mimebundle_ Protocol

Most Rich renderables (such as Table, Panel, and Syntax) inherit from JupyterMixin, which supplies the _repr_mimebundle_ method. IPython automatically calls this method when displaying objects in a notebook context.

The method performs four distinct operations:

  1. Obtains the global console via rich.get_console()
  2. Renders the object to a list of Segment objects using console.render(self, console.options)
  3. Converts segments to HTML via _render_segments() and to plain text via console._render_buffer()
  4. Returns a dictionary with "text/plain" and "text/html" keys

# rich/jupyter.py – JupyterMixin

def _repr_mimebundle_(self, include, exclude, **kwargs):
    console = get_console()
    segments = list(console.render(self, console.options))
    html = _render_segments(segments)
    text = console._render_buffer(segments)
    data = {"text/plain": text, "text/html": html}
    return data

Converting Segments to HTML with _render_segments

The _render_segments function transforms Rich's internal Segment objects into HTML markup. It processes simplified segments (via Segment.simplify()) to collapse adjacent styles, performs HTML escaping, and maps Rich styles to inline CSS.


# rich/jupyter.py – _render_segments

def _render_segments(segments):
    def escape(text):  # HTML‑escape

        return text.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
    fragments = []
    theme = DEFAULT_TERMINAL_THEME
    for text, style, control in Segment.simplify(segments):
        if control:
            continue
        text = escape(text)
        if style:
            rule = style.get_html_style(theme)
            text = f'<span style="{rule}">{text}</span>' if rule else text
            if style.link:
                text = f'<a href="{style.link}" target="_blank">{text}</a>'
        fragments.append(text)
    code = "".join(fragments)
    return JUPYTER_HTML_FORMAT.format(code=code)

The function uses Style.get_html_style(theme) from rich/style.py to translate Rich's style attributes into CSS rules based on the DEFAULT_TERMINAL_THEME. Links are preserved as HTML anchor tags. The final markup is wrapped in a <pre> element (via JUPYTER_HTML_FORMAT) to preserve whitespace and use monospace fonts.

The Low-Level display() Helper

For scenarios where you already have a list of Segment objects, rich.jupyter.display() provides direct access to the rendering pipeline without requiring a renderable object.


# rich/jupyter.py – display()

def display(segments, text):
    html = _render_segments(segments)
    jupyter_renderable = JupyterRenderable(html, text)
    from IPython.display import display as ipython_display
    ipython_display(jupyter_renderable)

This function creates a JupyterRenderable instance (a thin wrapper carrying HTML and text representations) and passes it to IPython's display system. If IPython is not installed, the function silently returns without raising errors, guarding against environments where force_jupyter=True but the IPython library is absent.

Practical Implementation Examples

Automatic Detection in Notebooks

The standard usage pattern relies on Rich's automatic detection. You instantiate a Console and print renderables normally; the output automatically appears as styled HTML in notebook cells.

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

console = Console()  # Automatically detects Jupyter

table = Table(title="Demo")
table.add_column("Name")
table.add_column("Age")
table.add_row("Alice", "30")
table.add_row("Bob", "25")

console.print(table)  # Renders as HTML in Jupyter, ANSI in terminal

Manual Segment Rendering

For advanced use cases, you can manually render content to segments and display them explicitly using the low-level API.

from rich.console import Console
from rich.jupyter import display

console = Console()
segments = list(console.render("Hello, [bold magenta]World![/]"))
display(segments, "Hello, World!")  # Explicit call to Jupyter display

Forcing Terminal Mode

To override the automatic detection and force standard terminal rendering (ANSI escape codes) even inside a notebook, set force_jupyter=False.

from rich.console import Console

# Force ANSI output even in Jupyter

console = Console(force_jupyter=False)
console.print("[red]This appears as raw ANSI escape codes[/]")

Summary

  • Detection logic resides in rich/console.py via the _is_jupyter() function, which checks for ZMQInteractiveShell, Google Colab classes, and Databricks environment variables.
  • Rendering protocol is implemented in rich/jupyter.py through JupyterMixin._repr_mimebundle_(), which returns MIME bundles containing both HTML and plain text representations.
  • HTML generation occurs in _render_segments(), which converts Segment objects (from rich/segment.py) into HTML using CSS styles from Style.get_html_style() (defined in rich/style.py).
  • Fallback mechanism ensures that if IPython is unavailable or force_jupyter=False, Rich degrades gracefully to standard terminal output using ANSI escape sequences.

Frequently Asked Questions

How does Rich distinguish between Jupyter and plain IPython?

Rich examines the IPython shell type via get_ipython().__class__.__name__. It returns True only for ZMQInteractiveShell (Jupyter's kernel protocol), specific Google Colab class names, or Databricks runtimes. It explicitly returns False for TerminalInteractiveShell, which indicates a standard IPython terminal session that should receive ANSI codes rather than HTML.

What MIME types does Rich return to Jupyter?

According to the JupyterMixin._repr_mimebundle_() implementation in rich/jupyter.py, Rich returns a dictionary containing two keys: "text/plain" containing the raw text representation, and "text/html" containing the styled HTML markup. Jupyter uses the HTML version for visual display while preserving the plain text for clipboard operations or text-only contexts.

Can I force Rich to use HTML rendering outside of Jupyter?

Yes. While Rich automatically detects the environment, you can force Jupyter-style rendering by setting force_jupyter=True when creating a Console instance. However, this requires IPython to be installed in the environment, as the display() function and _repr_mimebundle_ protocol depend on IPython's display system to actually render the HTML.

When processing segments in _render_segments(), Rich checks for the style.link attribute. If a link is present, it wraps the styled text in an HTML anchor tag with target="_blank" using the format f'<a href="{style.link}" target="_blank">{text}</a>'. This ensures clickable hyperlinks function correctly in the notebook's HTML output while remaining plain text in terminal fallback mode.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →