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.
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.
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
- You are starting a new CLI: Typer gives you Rich-formatted help from type hints with no separate dependency, and Click on its own is perfectly usable for a handful of options
- You need a dependency that promises stability: PyPI still classifies this as Development Status 3 (Alpha), the groups API was deprecated in 1.9.0 in favor of panels, and the Typer patching path is documented as broken with typer 0.26 and newer
- You snapshot help output in tests: the rendering depends on terminal width, color support and the FORCE_COLOR, PY_COLORS, GITHUB_ACTIONS and TERMINAL_WIDTH environment variables, so golden-file tests that passed locally fail in CI until you pin the width
- You already use Typer: it renders Rich help itself, and layering rich-click on top through patch_typer() is explicitly experimental
- You dislike namespace shadowing: `import rich_click as click` means the name click in your file is not the click package, which confuses new contributors and static analysis tooling that special-cases Click
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.htmlThe 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: E402patch() 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.outputWidth 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
| Package | Registry | Pick it when |
|---|---|---|
| typer | PyPI | You are writing a new CLI and want commands derived from type hints, with Rich-formatted help already included. |
| click | PyPI | You want zero extra dependencies and no alpha-status formatting layer, and plain help output is good enough. |
| cyclopts | PyPI | You want a modern type-hint-driven CLI framework with Rich help and are willing to leave the Click ecosystem behind. |