mrkeyoor.com_
Thu 06 Aug 15:43 UTC
PyPIUtilsupdated 06 Aug 2026

rich-click

rich-click makes Click's help output look good by rendering it with Rich. It re-exports the entire Click API under its own name, so replacing `import click` with `import rich_click as click` is the whole migration: your decorators, options and commands keep working, and only the help and error screens change. What you get is colored option names, wrapped help text in bordered panels, grouped options and subcommands, styled error messages, and over 100 themes that either you or the person running your CLI can pick. It also installs a `rich-click` command that reformats the help of any Click program you did not write, and can dump that help as HTML or SVG for documentation.

Verdict

The cheapest possible upgrade to an existing Click CLI: one import line for help output that people can actually read, with panels and themes if you want to go further. Treat the config surface as a moving target, pin the version, and do not build snapshot tests around the rendered output.

API stability3/5The core promise (import rich_click as click) has held since the start and deprecated features stay supported, but 1.9.0 deprecated the groups API in favor of panels, 1.8.0 deprecated the use_rich_markup and use_markdown booleans, and the package still ships a Development Status 3 (Alpha) classifier.
Docs5/5The mkdocs site has task-based pages for panels, themes, markup, configuration, accessibility and the CLI, every example is a runnable snippet rendered to an SVG screenshot, and version-specific docs for the older groups API are still published.
Maintenance4/5Pushed July 2026 with 1.9.8 released in May 2026, 10 open issues (16 counting PRs), and a maintainer who responds; the caveat is that it is essentially one person's project and it has to track Click's internal formatter APIs.
Ecosystem4/5Around 11.5 million downloads a week and it works with anything built on Click, but it sits downstream of both Click and Rich, so a Click 9 release or a Typer version bump can leave it temporarily behind.

Use it if

  • You have an existing Click CLI with many options and the help screen has become an unreadable wall of text; the import swap is a one-line change with no behavior change
  • You want options and subcommands organised into named panels, which is what makes a CLI with 30 flags navigable
  • You want help text as SVG or HTML for your docs site, which the rich-click CLI generates with --output=svg or --output=html
  • You want end users to restyle your CLI themselves, since any rich-click program honors the RICH_CLICK_THEME environment variable unless you turn that off
Skip it if

Setup reality

pip install rich-click pulls click 8 or newer and rich 12 or newer, plus colorama on Windows, and it supports Python 3.8 upward. Swapping the import covers most codebases, but three things do not come along for free. Anything that imports Click symbols directly (`from click import Group`) or passes `cls=` a custom class keeps plain Click formatting, so those classes need to subclass RichCommand or RichGroup instead. Rich markup in help strings is off by default: square brackets are printed literally until you set text_markup to 'rich' or 'markdown', and once you do you have to backslash-escape any literal brackets in every help string. Configuration exists at three levels that override each other, namely the module globals under click.rich_click (THEME, TEXT_MARKUP and the STYLE_ constants), the @click.rich_config decorator taking a dict or a RichHelpConfiguration, and the RICH_CLICK_THEME environment variable that end users control, so decide early which layer owns what and set enable_theme_env_var=False if user themes are unwelcome. For testing, set TERMINAL_WIDTH and pass color=False to CliRunner, otherwise output width follows whatever terminal the test happened to run in.

Patterns

Convert an existing Click CLI in one linedrop-in-import

import rich_click as click

@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for _ in range(count):
        click.echo(f"Hello, {name}!")

if __name__ == "__main__":
    hello()

rich_click re-exports the whole Click API, including echo, Choice, Path and the exception classes, so nothing else in the file changes. Only help and error output differ; parsing, exit codes and behavior are Click's.

Keep importing click and opt in per commanddeclarative-classes

import click
from rich_click import RichCommand, RichGroup

@click.group(cls=RichGroup)
def cli():
    """My tool."""

@cli.command(cls=RichCommand)
def build():
    """Build the project."""

Use this when a shared library or plugin system already imports click and you cannot swap the import globally. Subcommands do not inherit the class from the group, so every command needs its own cls= or it renders with plain Click formatting.

Configure one command tree with rich_configrich-config

import rich_click as click

@click.group()
@click.rich_config({
    "text_markup": "rich",
    "show_arguments": True,
    "commands_before_options": True,
    "style_options_panel_border": "dim blue",
})
def cli():
    """My tool."""

# equivalent, with autocomplete and type checking
@click.group()
@click.rich_config(click.RichHelpConfiguration(theme="nord-slim"))
def cli2():
    """My tool."""

The config attaches to the command and is inherited by its subcommands, which is why applying it to the top-level group is usually enough. A dict is checked against the same fields as RichHelpConfiguration, so a typo raises rather than being ignored.

Split options into named panelsoption-panels

import rich_click as click

@click.command()
@click.option("--src", help="Source")
@click.option("--dest", help="Destination")
@click.option("--env", help="Environment")
@click.option("--log-level", help="Log level")
@click.version_option("1.2.3")
@click.option_panel("Main", options=["--src", "--dest"])
@click.option_panel("Extra", options=["--env", "--log-level", "--help", "--version"])
def move_item(src, dest, env, log_level):
    """Move an item from src to dest."""

Options are listed by their flag string, and --help and --version have to be listed explicitly or they fall into the default panel. The same result can be had with panel="Main" on each @click.option, which is easier to keep in sync when options move around.

Group subcommands into panelscommand-panels

import rich_click as click

@click.group()
@click.command_panel("Items", commands=["move-item", "update-item"])
@click.command_panel("Users", commands=["create-user", "update-user"],
                     help="User management commands")
def cli():
    """CLI"""

@cli.command()
def move_item():
    """Move an item"""

Names in the commands list are the command names as Click sees them, so underscores in the function name become dashes. Panels replaced the old COMMAND_GROUPS dict in 1.9.0; mixing the two APIs produces unpredictable ordering, so migrate a CLI all at once.

Turn on Rich markup or Markdown in help texttext-markup

import rich_click as click

@click.command()
@click.rich_config({"text_markup": "rich"})
@click.option("--force", help="[red bold]Destructive[/]. Skips confirmation.")
def deploy(force):
    """Deploy the app.

    Use [cyan]--force[/] only in CI. Literal brackets need escaping: \\[default]
    """

The default is 'ansi', which prints square brackets literally and only interprets existing escape codes. Once you switch to 'rich', every unescaped bracket in every help string is parsed as markup, so a help line mentioning [default] disappears from the output.

Set a theme, and decide whether users can override itthemes

import rich_click as click

@click.group()
@click.rich_config({"theme": "nord-slim", "enable_theme_env_var": False})
def cli():
    """My tool."""

Theme names are palette-format pairs such as nord-slim or dracula-modern, with box, slim and modern among the formats. Unless enable_theme_env_var is False, anyone can set RICH_CLICK_THEME (or pass a whole JSON config in it) and change how your CLI looks, which is a feature for end users and a surprise for support screenshots.

Configure every command in the process with module globalsglobal-config

import rich_click as click

click.rich_click.THEME = "nord-slim"
click.rich_click.TEXT_MARKUP = "markdown"
click.rich_click.SHOW_ARGUMENTS = True
click.rich_click.STYLE_OPTIONS_PANEL_BORDER = "dim green"

@click.command()
def cli():
    """My tool."""

Globals are the lowest-precedence layer: a rich_config on a command wins over them, and they apply process-wide, so a library that sets them changes the look of every other rich-click CLI loaded in the same interpreter. Prefer rich_config in anything that is imported by other code.

Show positional arguments with help textarguments-in-help

import rich_click as click

@click.command()
@click.rich_config({"show_arguments": True, "group_arguments_options": False})
@click.argument("src", help="Where to read from")
@click.argument("dest", help="Where to write to")
def copy(src, dest):
    """Copy a file."""

Plain Click has no help argument on @click.argument at all; rich-click adds it, and setting it makes the arguments panel appear even without show_arguments. Set group_arguments_options to True instead if you want arguments listed inside the options panel rather than in their own.

Reformat someone else's Click CLI from the terminalformat-other-clis

# prefix any installed Click CLI
rich-click flask --help
rich-click --theme=star-slim celery --help

# point at a module or file when it is not installed as a script
rich-click path.to.my.cli:main --help
rich-click path/to/my/cli.py --help

# capture the help screen for docs
rich-click --output=svg myapp --help > docs/help.svg
rich-click --output=html myapp --help > docs/help.html

The module form imports the object and calls it; the file and bare-module forms execute the file with __name__ set to __main__, so anything at import time runs. This works on plain Click programs that have never heard of rich-click.

Richify Click commands defined in code you do not controlpatch-click-globally

from rich_click.patch import patch
from rich_click import RichHelpConfiguration

patch(RichHelpConfiguration(theme="nord-slim"))

# every click.command / click.group / click.Command created after this line
# is a rich-click type, including ones defined inside third-party plugins
import third_party_plugin  # noqa: E402

patch() rebinds click.command, click.group, click.Command, click.Group, click.Option and click.Argument process-wide, so it affects every Click CLI in the interpreter and must run before those modules are imported. There is a patch_typer() too, but the docs state it does not work with typer 0.26 or newer.

Make help-output tests deterministictest-help-output

import os
from click.testing import CliRunner
from myapp.cli import cli

def test_help(monkeypatch):
    monkeypatch.setenv("TERMINAL_WIDTH", "100")
    monkeypatch.delenv("RICH_CLICK_THEME", raising=False)
    monkeypatch.delenv("FORCE_COLOR", raising=False)
    result = CliRunner().invoke(cli, ["--help"], color=False)
    assert result.exit_code == 0
    assert "Usage:" in result.output

Width comes from the TERMINAL_WIDTH environment variable when set, otherwise from the real terminal, so unpinned tests wrap differently on a laptop and in CI. Color is forced on when FORCE_COLOR, PY_COLORS or GITHUB_ACTIONS is present, which is why the same assertion passes locally and fails on GitHub Actions with escape codes in the string.

Alternatives

PackageRegistryPick it when
typerPyPIYou are writing a new CLI and want commands derived from type hints, with Rich-formatted help already included.
clickPyPIYou want zero extra dependencies and no alpha-status formatting layer, and plain help output is good enough.
cycloptsPyPIYou want a modern type-hint-driven CLI framework with Rich help and are willing to leave the Click ecosystem behind.