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:
- Obtains the global console via
rich.get_console() - Renders the object to a list of
Segmentobjects usingconsole.render(self, console.options) - Converts segments to HTML via
_render_segments()and to plain text viaconsole._render_buffer() - 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("&","&").replace("<","<").replace(">",">")
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.pyvia the_is_jupyter()function, which checks forZMQInteractiveShell, Google Colab classes, and Databricks environment variables. - Rendering protocol is implemented in
rich/jupyter.pythroughJupyterMixin._repr_mimebundle_(), which returns MIME bundles containing both HTML and plain text representations. - HTML generation occurs in
_render_segments(), which convertsSegmentobjects (fromrich/segment.py) into HTML using CSS styles fromStyle.get_html_style()(defined inrich/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.
How does Rich handle links in Jupyter output?
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →