Skip to content

Parity with Python rich

The point of this project is that output is byte-identical to Python rich 15.0.0. Not "similar", not "inspired by" — the same bytes.

How much of the port is done

Last verified 2026-09-24 against Python rich 15.0.0, at the 0.0.11 release test (core 0.0.7).

Port status by upstream module Of 65 upstream modules, 8 are complete, 52 are partial and 5 are not started. Each bar segment and each row links to that section of the parity table. Complete: 8 modules (12%) Complete 8 Partial: 52 modules (80%) Partial 52 Not started: 5 modules (8%) 65 upstream modules mapped · 8 complete · 52 partial · 5 not started Rendering core Rendering core: 16 of 17 started, 4 complete 16/17 Widgets & layout Widgets & layout: 14 of 14 started, 3 complete 14/14 Live & progress Live & progress: 5 of 5 started, 0 complete 5/5 Content renderers Content renderers: 7 of 8 started, 0 complete 7/8 Utilities & platform Utilities & platform: 8 of 11 started, 1 complete 8/11 rich-cli rich-cli: 10 of 10 started, 0 complete 10/10
60 of 65 upstream modules (92%) have a working implementation. Click any bar to jump to that part of the module status table.

"Partial" is the honest majority, and it is not a synonym for unfinished. A module is marked partial while any part of upstream's surface is unported, even when everything the CLI exercises is byte-identical. Seventeen modules are marked complete, and only because a differential sweep or the golden fixtures say so:

Module Measurement
cells.rs 0 mismatches across 127,754 code points, plus emoji clusters
wrap.rs 0 mismatches across a 30,680-case wrap matrix
cell_widths.rs all 21 upstream Unicode tables, selectable via UNICODE_VERSION
control.rs, box.rs, styled.rs full upstream surface, golden-tested
progress_bar.rs full upstream surface, including the pulse animation, ASCII and no-colour bars, golden-tested (progress_bar.tsv)
theme.rs full upstream surface, including the theme stack and theme files, golden-tested (themes.tsv, theme_stack.tsv)
padding.rs, panel.rs, align.rs, constrain.rs, table.rs, layout.rs, screen.rs, bar.rs, ratio.rs full upstream surface but Layout's custom splitters (§40), golden-tested (parity_widgets.tsv and the earlier fixtures)

End to end, the current CLI renders 0 mismatches across 138 document cases (Markdown, JSON and syntax at six widths each) against Python rich 15.0.0.

What the percentage does not mean

92% of modules is not 92% of upstream's behaviour, and neither is a promise about your document. It means most modules have a working implementation; the honest per-area detail is in Module status, and the known gaps are below.

How parity is enforced

Fixtures are captured from the real Python library and asserted in CI:

pip install "rich==15.0.0"
python scripts/capture_golden.py       # writes crates/rich/tests/golden/*.tsv
cargo test -p rs-rich --test golden    # asserts byte equality

The capture script renders each case through Python rich with a pinned console (force_terminal=True, color_system="truecolor", legacy_windows=False, safe_box=False) and records the exact escape sequences. The Rust tests rebuild the same case and compare bytes.

CI regenerates the fixtures from upstream on every run and fails if they drift, so the parity claim is checked continuously rather than asserted once.

Never install rich-cli beside rich

rich-cli 1.8.1 requires rich<13, so installing it downgrades rich from 15.0.0 to 12.6.0 — the library this port measures itself against. pip prints no error, the fixtures still run, and every comparison afterwards is silently against the wrong upstream. Keep the CLI oracle in its own virtualenv, and check the version before trusting a parity result:

python -c "from importlib.metadata import version; print(version('rich'))"

Differential fuzzing

Golden fixtures pin cases someone thought of. scripts/diff_rich.py generates cases nobody thought of: markup, styled text with every overflow and justify mode, panels, tables (column justify, no_wrap, min/max widths, ratios, edges, lines, titles), rules, padding and alignment, at random widths and colour systems. Since 0.0.22 it also generates the document renderers, about as often as all of those together: Markdown (headings, lists, quotes, tables, inline markup, links and images; not code blocks, which syntect colours, see DIVERGENCES §18), trees, columns, Progress frames (description, bar and percentage), Pretty over random Python values and JSON with its options. It renders each case through the pinned Python rich and through the Rust diff_render example, then compares the bytes. Upstream's random hyperlink id= is taken out of the Python side first (DIVERGENCES §20).

  • Pull requests replay the small checked-in corpus (scripts/fixtures/diff_rich_cases.jsonl), so CI stays fast and every case there must match.
  • Nightly (.github/workflows/nightly-parity.yml, main only) compares 20,000 generated cases with a fresh seed. It fails loudly, printing shrunk, replayable corpus lines to the log and the job summary.
  • Known divergences it has found live in scripts/fixtures/diff_rich_known.jsonl, one line per case, each naming its issue. 0.0.11 fixed the first eight families (#442–#449). The document kinds' first runs (5,000 cases over five seeds) found six Markdown families, fixed in 0.0.22; one remains there, an empty link as the only content of an ordered item under justify="full".

The Python side renders each colour system in its own interpreter. rich memoises a Style's escape codes on the instance whatever the colour system, so a shared interpreter reports mismatches that are the oracle's own. Markup uses the strict parser on both sides, because upstream raises MarkupError.

Triage a red nightly

  1. Take the seed from the log (Replay with: … --seed N), or a shrunk line from the summary, and reproduce it locally in a virtualenv holding only the pinned rich:

    echo '<shrunk line>' > case.jsonl
    env -u NO_COLOR TERM=xterm-256color PYTHONUTF8=1 \
      python scripts/diff_rich.py --corpus case.jsonl
    
  2. Check Divergences and the known list. If the case belongs to a known family, there is nothing new; the issue it names tracks it.

  3. Otherwise file a type:bug issue with the shrunk line, both outputs and the upstream code path, and add the case to diff_rich_known.jsonl with its issue number.
  4. When fixing an issue, move its known cases into diff_rich_cases.jsonl in the same PR, so the pull-request corpus keeps the fix.

Useful flags: --generate N --seed S for a generated run, --no-shrink, and --max-shrink N to limit how many failures are shrunk (one per case kind first), --write-failures PATH to append shrunk cases as corpus JSONL, and --self-test-mutation to prove the harness detects a mismatch.

What this buys you

If you know Python rich, you already know this library — the same markup, the same style syntax, the same box styles, the same colour downgrade behaviour on a 16-colour terminal.

It also means upstream's quirks are reproduced deliberately. An unknown markup tag renders as a no-op rather than an error, because that is what upstream does.

Where it stops

Three kinds of gap, kept separate on purpose:

  • Not ported yet

    Listed per module in Module status. Notably: Windows legacy console, Jupyter integration, and Python-object inspect.

  • Deliberately different

    Documented with reasons in Divergences. The big one: syntax highlighting uses syntect, not Pygments, so highlighted code is not byte-identical.

  • Impossible

    Some things cannot match. Upstream puts a random id= in OSC 8 hyperlinks; reproducing it would mean reproducing Python's RNG.

See Known issues for the ones you might actually hit.

Reporting a parity bug

The most useful bug report shows both sides:

from rich.console import Console
c = Console(force_terminal=True, color_system="truecolor",
            legacy_windows=False, safe_box=False, no_color=False, width=40)
c.print("[bold]your case here[/]")

Paste the exact bytes (repr() in Python, {:?} in Rust) rather than a screenshot — an escape sequence is invisible otherwise. Check Divergences first in case it is intentional.