Skip to content

Fix: Rich Not Working — Live Display Issues, Color in CI, and Console Configuration

FixDevs · (Updated: )

Part of:  Python Errors

Quick Answer

How to fix Rich errors — colors not appearing in CI logs, Live display flickering, progress bar not updating, table column overflow, traceback install conflicts, and Console redirect issues.

The Error

You write Rich output and it looks great locally but breaks in CI:

from rich.console import Console
console = Console()
console.print("[bold red]Error![/]")
# Locally: Bold red text
# In CI logs: [bold red]Error![/]

Or a Live display flickers wildly:

from rich.live import Live
with Live() as live:
    for i in range(100):
        live.update(...)   # Screen flickers and re-draws constantly

Or progress bars print on every iteration instead of updating in place:

from rich.progress import track
for i in track(range(100)):
    process(i)
# Output:
# ⠋ Working...   1%
# ⠙ Working...   2%
# ⠹ Working...   3%   # New line every update — not in place

Or table columns truncate or wrap badly:

from rich.table import Table
table = Table("ID", "Name", "Description")
# Description column overflows; "ID" column too wide

Or install_rich_traceback() conflicts with another logging system:

from rich.traceback import install
install()
# Tracebacks look good, but sentry/loguru/structlog now break

Rich is the dominant library for terminal output in Python — pretty tables, progress bars, syntax-highlighted code, rich tracebacks. Used by Typer, Textual, pip, and most modern CLIs. It works beautifully in interactive terminals but produces specific failures in CI logs, redirected output, and limited-width environments. This guide covers each.

Why This Happens

Rich auto-detects terminal capabilities (color support, width, cursor positioning) via the Console. In an interactive terminal, all features work. When stdout is redirected (CI logs, piped to a file, captured by a tool), Rich falls back — strips colors, disables Live updates, and prints plain text. The behavior differs subtly across environments.

Live displays use cursor-control ANSI codes to redraw the same region. In an environment without cursor support (a CI log that just appends lines), each “update” creates a new line. Detecting and adapting to this is what console.is_terminal and force_terminal parameters control.

Fix 1: Color Output in CI

from rich.console import Console
console = Console()
console.print("[bold red]Error![/]")
# Local: red bold output
# CI: plain text [bold red]Error![/]

Rich strips formatting when it detects a non-interactive stdout — which is what CI logs look like. Force color on:

console = Console(force_terminal=True)

Or via environment variable (no code change needed):

FORCE_COLOR=1 python my_script.py

Detect what Rich sees:

console = Console()
print(f"is_terminal: {console.is_terminal}")
print(f"is_jupyter: {console.is_jupyter}")
print(f"color_system: {console.color_system}")   # 'truecolor', '256', 'standard', or None
print(f"width: {console.width}")

GitHub Actions specifically — set FORCE_COLOR=1 in the workflow:

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      FORCE_COLOR: "1"
    steps:
      - run: python my_rich_script.py

GitHub Actions logs support ANSI colors but doesn’t advertise as a TTY.

Common Mistake: Forcing color always (force_terminal=True) even when output is piped to a file or processed by another tool. ANSI codes in a file look like garbage:

mycli output > log.txt
# log.txt contains ^[[1;31mError!^[[0m instead of "Error!"

Better: only force color when you know the destination supports it (CI, specific env vars). Default Console() does the right thing for piped output.

Fix 2: Live Display Configuration

from rich.live import Live
from rich.table import Table
import time

table = Table("Iteration", "Status")
table.add_row("1", "starting")

with Live(table, refresh_per_second=4) as live:
    for i in range(10):
        time.sleep(1)
        # Update the table
        new_table = Table("Iteration", "Status")
        new_table.add_row(str(i), "running")
        live.update(new_table)

Key parameters:

ParameterMeaning
refresh_per_secondMax refresh rate (default 4); higher = smoother but more CPU
vertical_overflow"crop", "ellipsis", "visible" — how to handle content taller than terminal
auto_refreshIf True, Rich refreshes on schedule; if False, only live.refresh() triggers redraw
transientClear display when context exits (useful for one-shot status)

Flickering — usually means refresh_per_second is too high or the renderable is being rebuilt unnecessarily:

# WRONG — rebuilds table every iteration, then refreshes
with Live(refresh_per_second=20) as live:
    for i in range(1000):
        table = Table("ID", "Value")
        for j in range(i):
            table.add_row(str(j), str(j*2))
        live.update(table)
# Flickers because the table grows each frame

# CORRECT — update an existing table, low refresh rate
table = Table("ID", "Value")
with Live(table, refresh_per_second=4) as live:
    for i in range(1000):
        table.add_row(str(i), str(i*2))
        # Don't call live.update — auto-refresh picks up the change

No-flicker pattern for terminal dashboards:

from rich.live import Live
from rich.layout import Layout

layout = Layout()
layout.split_column(
    Layout(name="header", size=3),
    Layout(name="main"),
    Layout(name="footer", size=3),
)

with Live(layout, refresh_per_second=10, screen=True) as live:
    while True:
        layout["header"].update(get_header())
        layout["main"].update(get_main_content())
        layout["footer"].update(get_footer())
        time.sleep(0.1)

screen=True uses the alternate screen buffer (like vim or less) — restores the previous terminal contents on exit.

Fix 3: Progress Bars

from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn

with Progress(
    SpinnerColumn(),
    TextColumn("[progress.description]{task.description}"),
    BarColumn(),
    TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
    TimeRemainingColumn(),
) as progress:
    task1 = progress.add_task("Downloading...", total=100)
    task2 = progress.add_task("Processing...", total=50)

    while not progress.finished:
        progress.update(task1, advance=0.5)
        progress.update(task2, advance=0.25)
        time.sleep(0.02)

Simple iteration helper:

from rich.progress import track

for item in track(items, description="Processing..."):
    do_work(item)

Why progress bars print new lines instead of updating in placetrack and Progress need cursor control. When stdout isn’t a TTY (CI, piped output), Rich falls back to line-by-line output.

For CI environments where you want minimal output:

from rich.progress import Progress, BarColumn

with Progress(BarColumn(), TextColumn("{task.percentage:>3.0f}%"),
              disable=not sys.stdout.isatty()) as progress:
    task = progress.add_task("Loading", total=100)
    for i in range(100):
        progress.update(task, advance=1)

disable=True entirely suppresses the progress bar — useful when piping to a log file.

Pro Tip: Always use track() for simple iteration over a fixed-length iterable. Reach for Progress(...) only when you have multiple parallel tasks, custom columns, or need to update progress descriptions mid-iteration. The simpler API covers 90% of cases.

Fix 4: Tables — Column Widths and Overflow

from rich.table import Table
from rich import box

table = Table(
    "ID", "Name", "Description",
    box=box.ROUNDED,
    show_lines=True,
)

table.add_row("1", "Alice", "A very long description that might overflow the column width")

Control column behavior:

from rich.table import Table, Column

table = Table(
    Column("ID", style="cyan", width=4),
    Column("Name", style="bold", min_width=10),
    Column("Description", overflow="fold"),   # fold | crop | ellipsis
    expand=True,   # Expand to full terminal width
)

Overflow options:

OptionBehavior
"fold"Wrap to next line
"crop"Cut off (default)
"ellipsis"Show … for cut content
"ignore"Allow column to extend beyond width

Sortable columns for interactive analysis:

from rich.table import Table

def make_table(rows, sort_by="value"):
    table = Table("Name", "Value", "Status")
    sorted_rows = sorted(rows, key=lambda r: r[sort_by])
    for row in sorted_rows:
        table.add_row(row["name"], str(row["value"]), row["status"])
    return table

Convert pandas DataFrame to Rich table:

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

def df_to_table(df, title=None):
    table = Table(title=title)
    for col in df.columns:
        table.add_column(str(col))
    for _, row in df.iterrows():
        table.add_row(*[str(v) for v in row])
    return table

console = Console()
console.print(df_to_table(df, title="Sales by Region"))

Fix 5: Syntax Highlighting

from rich.console import Console
from rich.syntax import Syntax

console = Console()

code = '''
def hello(name):
    return f"Hello, {name}"
'''

syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)

Themes — bundled options:

Syntax(code, "python", theme="monokai")
Syntax(code, "python", theme="dracula")
Syntax(code, "python", theme="github-dark")
Syntax(code, "python", theme="solarized-dark")
Syntax(code, "python", theme="vs")   # Light theme

Full Pygments theme list works since Rich uses Pygments under the hood.

Highlight specific lines:

Syntax(
    code, "python",
    line_numbers=True,
    highlight_lines={3, 5, 7},   # Highlight rows 3, 5, 7
    word_wrap=True,
)

From a file:

syntax = Syntax.from_path("example.py", line_numbers=True)
console.print(syntax)

Fix 6: Pretty Printing and Inspect

from rich import print as rprint
from rich.pretty import pprint

data = {"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}], "count": 2}

# Plain print — single line, hard to read
print(data)

# Rich print — colored, multi-line, syntax-aware
rprint(data)

# Pretty print with indentation control
pprint(data, indent_guides=True, max_string=80)

inspect() for objects — see methods, attributes, docstrings:

from rich import inspect

import requests
inspect(requests, methods=True, help=True)
# Beautiful table of all attributes, methods, and docs

inspect() is incredibly useful for exploring unfamiliar libraries — much faster than dir() + help().

Customize the global Rich print:

from rich import print
from rich.console import Console

console = Console(width=120, color_system="truecolor")
print = console.print   # Override built-in print globally (use with care)

Fix 7: Rich Traceback

from rich.traceback import install

install(show_locals=True)   # Replace default traceback handler

# Any unhandled exception now uses Rich's formatter
raise ValueError("something broke")

show_locals=True displays variable values at each stack frame — amazing for debugging but DON’T USE in production: it leaks sensitive data into logs:

install(
    show_locals=False,         # Don't show variables (safer)
    suppress=["sqlalchemy", "click"],   # Hide frames from these libraries
    max_frames=10,              # Limit traceback depth
)

Common Mistake: Calling rich.traceback.install() without show_locals=False in a server or CLI that runs in production. If an exception occurs, all local variables — including DB connection strings, API keys, raw passwords from request bodies — get logged. Either disable show_locals or only install Rich’s traceback during development.

Per-environment setup:

import os
from rich.traceback import install

if os.environ.get("ENV") != "production":
    install(show_locals=True)   # Dev only

Conflict with Loguru’s diagnose=True — both want to format exceptions. Pick one:

# If using Loguru with diagnose=True, skip rich.traceback.install()
from loguru import logger
logger.add(sys.stderr, backtrace=True, diagnose=True)

# Or use Rich and turn off Loguru's diagnose
import rich.traceback
rich.traceback.install()
logger.add(sys.stderr, diagnose=False, backtrace=False)

For Loguru traceback configuration, see Loguru not working.

Fix 8: Markdown, JSON, and Logging Integration

Render Markdown in terminal:

from rich.console import Console
from rich.markdown import Markdown

console = Console()
md = """
# Hello World

This is **bold** and *italic*.

- Item 1
- Item 2

```python
print("code block")

""" console.print(Markdown(md))


**Pretty-print JSON:**

```python
from rich.console import Console
from rich.json import JSON

console = Console()
console.print(JSON('{"name": "Alice", "age": 30}'))
console.print_json(data={"name": "Alice", "age": 30})

Logging handler:

import logging
from rich.logging import RichHandler

logging.basicConfig(
    level=logging.INFO,
    format="%(message)s",
    datefmt="[%X]",
    handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
)

log = logging.getLogger("rich")
log.info("Hello, [bold green]World[/]!")
log.error("Something went wrong")

try:
    1 / 0
except ZeroDivisionError:
    log.exception("Math is broken")   # Rich traceback in log

Mixing Rich and Loguru — use Rich’s handler with stdlib logging, then intercept stdlib to Loguru:

from rich.logging import RichHandler
from loguru import logger
import logging
import sys

# Rich for development pretty output
if sys.stderr.isatty():
    handler = RichHandler()
    logging.basicConfig(handlers=[handler], level=logging.INFO, format="%(message)s")
else:
    # CI / file output — plain text via loguru
    logger.add(sys.stderr, format="{time} | {level} | {message}")

Platform Differences: Terminals, CI, and Cross-OS Quirks

Rich detects terminal capabilities at Console() instantiation — and the answers it gets vary wildly by platform. The same script can produce gorgeous output in Windows Terminal and complete garbage in cmd.exe.

Windows Terminal vs cmd.exe vs PowerShell ISE. Windows Terminal (the modern Microsoft Store app) supports truecolor, Unicode emoji, and full ANSI cursor control. Classic cmd.exe only supports 16 colors and lacks proper ANSI handling until Windows 10 1809+. PowerShell ISE doesn’t support ANSI at all — Rich falls back to plain text. The console always uses cp1252 by default in Windows, which mangles Unicode unless you set PYTHONIOENCODING=utf-8 and chcp 65001.

WSL2. Output goes through the Windows Terminal renderer when launched from there, so colors work. The catch: terminal width detection breaks if you exit and re-enter WSL — Rich caches the initial size. Use console = Console(width=os.get_terminal_size().columns) to re-detect at runtime.

macOS Terminal.app vs iTerm2 vs Alacritty. Terminal.app supports 256 colors but not truecolor by default; iTerm2 and Alacritty both support truecolor. Check what Rich detected:

from rich.console import Console
console = Console()
print(console.color_system)
# 'truecolor', '256', 'standard', '8bit', or None

If you get '256' on macOS Terminal.app and want truecolor, switch to iTerm2 or set TERM=xterm-truecolor.

Linux terminals. Almost all modern emulators (GNOME Terminal, Konsole, Alacritty, Kitty, foot) support truecolor and Unicode out of the box. SSH sessions over tmux or screen sometimes downgrade colors — set set -g default-terminal "tmux-256color" in .tmux.conf to preserve them.

CI environment detection per platform:

from rich.console import Console
import os

# Common CI env vars Rich checks
ci_vars = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "JENKINS_URL", "BUILDKITE"]
in_ci = any(os.environ.get(v) for v in ci_vars)

console = Console(force_terminal=in_ci, no_color=False)

GitHub Actions. Logs support ANSI colors but sys.stdout.isatty() returns False. Set FORCE_COLOR=1 in workflow env: to make Rich emit colors. The default log width is ~155 characters; explicitly set Console(width=120) for predictable wrapping. Live displays and progress bars fall back to line-by-line output — track() prints one summary line at the end.

GitLab CI. Similar to GitHub. Set FORCE_COLOR=1. GitLab additionally supports collapsible sections — combine Rich’s Panel with GitLab’s \x1b[0Ksection_start:... escape codes if you want both.

Jenkins. Older Jenkins versions strip ANSI by default. Install the AnsiColor plugin and wrap the build step with ansiColor('xterm') for colors to survive.

rich vs colorama vs blessed. Colorama only handles ANSI-to-Win32 color translation — it makes print("\033[31mred") work on old cmd.exe but doesn’t draw tables, progress bars, or live displays. Blessed is closer to Rich in scope but uses curses under the hood, which doesn’t work on Windows. Rich is the only library that produces equivalent output across Windows, macOS, and Linux without OS-specific code.

Capture for testing per OS. When testing Rich output, use Console(file=io.StringIO(), force_terminal=True) to capture formatted bytes. On Windows, the captured output uses \r\n line endings if you write to a real file; force \n with newline='' when opening the file.

console.export_* formats for cross-platform sharing:

console.save_html("output.html")       # Browser-viewable, preserves colors
console.save_svg("output.svg", title="Report")   # Embeddable image
console.export_text(clear=False)        # Plain text for grep / piping

The HTML and SVG exports render identically regardless of the OS that generated them — useful for CI artifacts that need to be readable everywhere.

Still Not Working?

Rich vs Plain Text Output for CI

For tools that run in both interactive and CI contexts, conditional output is cleaner than always using Rich:

import sys
from rich.console import Console

if sys.stdout.isatty():
    console = Console()
    console.print("[bold green]Success![/]")
else:
    print("Success!")

Or use a single Console with smart fallback:

console = Console(force_terminal=False, no_color=not sys.stdout.isatty())

Rich and Click/Typer Integration

Typer uses Rich for help output and tracebacks by default. For Typer-specific patterns that interact with Rich’s behavior, see Typer not working.

To customize Click’s output similarly:

import click
from rich.console import Console

console = Console()

@click.command()
def greet(name):
    console.print(f"[bold]Hello[/], [cyan]{name}[/]")

Width Detection in Containers

Docker and Kubernetes containers often report no width (returns 80 by default). Force a wider console:

console = Console(width=160)   # Force fixed width for containers

Or detect via env var:

import os
console = Console(width=int(os.environ.get("COLUMNS", 120)))

Performance with Large Tables

Rich tables build all rows in memory before rendering. For tables with 100k+ rows, this is slow and uses huge memory. Alternative: paginate the rendering:

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

def stream_table(rows, page_size=50):
    console = Console()
    for i in range(0, len(rows), page_size):
        table = Table("ID", "Name")
        for row in rows[i:i + page_size]:
            table.add_row(str(row["id"]), row["name"])
        console.print(table)
        if i + page_size < len(rows):
            input("Press enter for next page...")

For testing patterns with Rich-based output capture, see pytest fixture not found. For pre-commit hooks that validate Rich-formatted help text, see pre-commit not working.

Live Display Not Updating Under nohup or as a Service

Live displays need a TTY for cursor positioning. When you run a Rich-based script via nohup, systemd, or any service supervisor that closes stdin/stdout, the live region freezes on first frame. Switch to periodic console.print() calls or use Console(force_terminal=False) and a static progress log format. For long-running services, log via the RichHandler to a file with record=True so you can replay state with console.save_html().

Unicode Box-Drawing Chars Showing as Question Marks

Rich’s table borders use Unicode box-drawing characters. If your terminal font doesn’t include them, they render as ? or boxes. The fix is font-side: install a Powerline-aware monospace font (Cascadia Code, JetBrains Mono, Fira Code). On Windows cmd.exe, also run chcp 65001 to switch the active code page to UTF-8 — otherwise even the right font won’t help. Alternative: pass box=rich.box.ASCII to fall back to plain ASCII borders that work everywhere.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles