Skip to content

v1.9 released!

Today we are excited to release version 1.9 of rich-click, our biggest release yet with lots of amazing new features!

The big features of the 1.9 release are:

  • Themes
  • rich-click CLI support for Typer CLIs
  • Panels
  • IDE autocomplete support

Themes

The biggest theme (pun intended) of rich-click 1.9 is themes. These can be set with an environment variable (as an end user) or in the @click.rich_config() (as a developer).

Here are some simple examples of themes:

export RICH_CLICK_THEME=nord-nu
python docs/code_snippets/themes/cli.py --help

export RICH_CLICK_THEME=dracula-modern
python docs/code_snippets/themes/cli.py --help

export RICH_CLICK_THEME=cargo-slim
python docs/code_snippets/themes/cli.py --help

export RICH_CLICK_THEME=forest-robo
python docs/code_snippets/themes/cli.py --help

As you can see, themes are not just colors, they can also completely change the formatting, too!

Tip

The ability to customise rich-click output as an end user like this is a big win for accessibility! See our Accessibility documentation for more information.

Themes are defined as {color_palette}-{format}. Overall, there are over 100 themes to choose from.

Our first adopter of rich-click themes is Litestar, the ASGI API framework. They have their own official color palette called star. (Of course, you can use this theme for your own CLI, too!) Here's what it looks like!

If you want to see all the options available to you, you can run the CLI command rich-click --themes:

Output of rich-click --themes

If you are interested in learning more about themes, read about them in the Themes docs.

rich-click CLI support for Typer CLIs

If you thought themes are cool in rich-click, wait until you see them in Typer!

The rich-click CLI now supports Typer, which also means that rich-click themes are available to Typer.

Code - Example Typer CLI
# /// script
# dependencies = ["rich-click>=1.9", "typer>=0.15"]
# ///

# This example comes from the Typer docs:
# https://typer.tiangolo.com/tutorial/commands/help/#help-panels-for-commands
import typer

app = typer.Typer(rich_markup_mode="rich")

@app.command()
def create(username: str):
    """[green]Create[/green] a new user. :sparkles:"""
    print(f"Creating user: {username}")

@app.command()
def delete(username: str):
    """[red]Delete[/red] a user. :fire:"""
    print(f"Deleting user: {username}")

@app.command(rich_help_panel="Utils and Configs")
def config(configuration: str):
    """[blue]Configure[/blue] the system. :wrench:"""
    print(f"Configuring the system with: {configuration}")

@app.command(rich_help_panel="Utils and Configs")
def sync():
    """[blue]Synchronize[/blue] the system or something fancy like that. :recycle:"""
    print("Syncing the system")

@app.command(rich_help_panel="Help and Others")
def help():
    """Get [yellow]help[/yellow] with the system. :question:"""
    print("Opening help portal...")

@app.command(rich_help_panel="Help and Others")
def report():
    """[yellow]Report[/yellow] an issue. :bug:"""
    print("Please open a new issue online, not a direct message")

if __name__ == "__main__":
    app()
Output - Example Typer CLI with rich-click

Even cooler, you can patch Typer directly in your code to use rich-click's themes. This means you don't need to rely on users to run rich-click your-cli-tool to get access to these themes.

In the below example, we patch typer to use the star-slim theme directly in the CLI code:

Code - Typer CLI with patch
# /// script
# dependencies = ["rich-click>=1.9", "typer>=0.15"]
# ///
from enum import Enum
import typer
from typing_extensions import Annotated

class LogLevel(str, Enum):
    debug = "debug"
    info = "info"
    warn = "warn"
    error = "error"

def main(
        id: Annotated[int, typer.Argument(min=0, max=1000)],
        age: Annotated[int, typer.Option(min=18)] = 20,
        score: Annotated[float, typer.Option(max=100)] = 0,
        log_level: Annotated[LogLevel, typer.Option(rich_help_panel="Logging")] = LogLevel.info,
        color: Annotated[bool, typer.Option(rich_help_panel="Logging")] = True,
) -> None:
    print(f"id={id} age={age} score={score} log_level={log_level} color={color}")

if __name__ == "__main__":
    from rich_click.patch import patch_typer
    import rich_click.rich_click as rc

    patch_typer()
    rc.THEME = "star-slim"

    typer.run(main)
Output - Typer CLI with patch

Under the hood, we are doing metaclass dark magic to prevent needing to preemptively import Typer, and to prevent requiring users to specify that a CLI is a Typer CLI. This keeps the runtime as slim and as fast as possible for non-Typer CLIs. (As of Typer version 0.17.0 released August 30, 2025, lazy loading Typer for improved performance is less necessary, but it's still nice to have.)

This feature is still experimental, so please let us know if you encounter any bugs.

Panels

Panels are a big new feature of rich-click, and bring along with it an intuitive and powerful API for customization.

Panels replace the old "groups" feature of previous rich-click versions. (Don't worry, your old code will still work just fine.) This is something I've wanted in rich-click for a long time, as I've always found the old groups API powerful but a little hard to use.

With the new API, defining a panel is as simple as adding a decorator.

import rich_click as click

@click.command()
@click.argument("src", help="Source", panel="Main")
@click.argument("dest", help="Destination", panel="Main")
@click.option("--env", "-e", help="Environment", panel="Extra")
@click.option("--log-level", "-l", help="Log level", panel="Extra")
@click.version_option("1.2.3", "--version", "-V", panel="Extra")
@click.help_option("--help", "-h", panel="Extra")
@click.option_panel("Main")
@click.option_panel("Extra")
@click.rich_config({"theme": "star-nu", "max_width": 80})
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()

You can also define the objects mapped to a panel in the panel itself. The below code is effectively equivalent to the above code:

import rich_click as click

@click.command(context_settings={"help_option_names": ["--help", "-h"]})
@click.argument("src", help="Source")
@click.argument("dest", help="Destination")
@click.option("--env", "-e", help="Environment")
@click.option("--log-level", "-l", help="Log level")
@click.version_option("1.2.3", "--version", "-V")
@click.help_option("--help", "-h")
@click.option_panel("Main",
                    options=["src", "dest"])
@click.option_panel("Extra",
                    options=["--env", "--log-level", "--version", "--help"])
@click.rich_config({"theme": "star-nu", "max_width": 80})
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()

The below is a fairly convoluted example of panels, which shows off the level of customizability available:

import rich_click as click

@click.command()
@click.argument("src", help="Source", panel="Main")
@click.argument("dest", help="Destination", panel="Main")
@click.option("--env", "-e", help="Environment", panel="Extra")
@click.option("--log-level", "-l", help="Log level", panel="Extra")
@click.version_option("1.2.3", "--version", "-V", panel="Extra")
@click.help_option("--help", "-h", panel="Extra")
@click.option_panel("Main",
                    title_style="bold red",
                    help="Required arguments",
                    help_style="dim red",
                    columns=["opt_all", "metavar", "help"],
                    panel_styles={"border_style": "dim red"})
@click.option_panel("Extra",
                    title_style="bold blue",
                    help="Optional settings",
                    help_style="dim blue",
                    columns=["opt_all", "metavar", "help"],
                    panel_styles={"border_style": "dim blue"})
@click.rich_config({"theme": "star-nu", "max_width": 80})
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()

Neat, huh? If you are interested in learning more, read about panels in the Panels docs.

IDE autocomplete support

I love Click! That's why we maintain this repo, after all. That said, one thing that is a nuisance with Click is that it doesn't have great type annotations for its core API, meaning your IDE doesn't autocomplete args for Click decorators.

Well, we have fixed this issue in rich-click 1.9.0:

We even typed the more opaque dicts, such as context_settings:

Everything should be type-hinted, and we've tested this against both pyright and mypy to good results.

Note

Adding a feature like this is a little outside the intended scope of rich-click. We are also aware of the maintenance nightmare that .pyi files create. But we feel it is such a huge quality of life upgrade for the developer experience that we'd be silly to not add it. You're welcome!

Other cool features

Help for arguments: @click.argument(..., help=)

That's right! We now support a help kwarg for arguments.

Aliases: @Group().command(..., aliases=[...])

rich-click commands can now be given aliases, similar to other Click extensions (click-aliases, click-extra, cloup).

rich-click CLI supports path/to/file.py

Now, all the following ways of invoking the rich-click CLI are supported:

  • rich-click hello:cli --help
  • rich-click hello.py --help
  • rich-click hello --help

In prior versions, only the first way worked.

Smarter newline handling in command help text

We are now a little more careful and practical with how we handle newlines in help text. You can see a demo of rich-click 1.9's newline handling below:

Code
# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

@click.command("newline-control")
def cli():
    """Newline
    control

    Double newlines are always preserved.
    But single newlines are usually not.

    There are however a few situations where we preserve single newlines.

    Indented lines with 4+ spaces are preserved:
        ```python
        from foo import bar
        \n\
        bar.action()
        ```

    Unordered lists are preserved:

    - like
    - this

    also:

    * newlines within unordered lists
      are collapsed
      down.
    * pretty neat!

    Last but not least, we preserve:

    > block
    > quotes
    """

if __name__ == "__main__":
    cli()
Output

Stderr help: context_settings={"help_to_stderr": True}

Print help text to stderr with a context setting!

Deferred help text rendering

Like normal Click, now help text is pushed to a buffer and rendered at the very end of the help text generation context. Most users won't notice a difference, but it makes help text rendering atomic (e.g. in the event of an error), and also assists with integrating into tools such as mkdocs-click.

Many other minor improvements

There have been so many tiny changes to rich-click over the past few months that I've lost count. We've added more than a dozen different config options, we've fixed a few minor bugs, we've improved performance slightly, and we've made everything more composable for the really advanced users who want to subclass rich-click's objects.

Dropped support

In rich-click 1.9, we finally pulled the plug on Python 3.7, Click 7, and Rich 10+11.

We are being a little more conservative than we perhaps need to: Click 8 came out in 2021, Rich 12 came out in 2022, and Python 3.8 came out in 2019 and hit end-of-life in 2024. That said, we don't believe there is a major cost to supporting these other than some slightly ugly backend code here and there, so we'll continue to be conservative with dropping support.

Additionally, there are a handful of additional pending deprecations introduced to config options:

  • show_metavars_column
  • append_metavars_help
  • option_envvar_first
  • use_markdown_emoji

Of these config options being deprecated, the most popular is the combination of append_metavars_help=True and show_metavars_column=False. We deprecated support for these because in 1.9, this is handled via declaring order of elements explicitly with the config options options_table_column_types and options_table_help_sections. An example of this approach can be seen below:

Code
# /// script
# dependencies = ["rich-click>=1.9"]
# ///
import rich_click as click

@click.command()
@click.argument("src")
@click.argument("dest")
@click.option("--env",
              help="Environment",
              default="dev", show_default=True,
              envvar="ENV", show_envvar=True,
              required=True)
@click.option("--log-level",
              help="Log level",
              default="INFO", show_default=True,
              envvar="LOG_LEVEL", show_envvar=True,
              deprecated=True)
@click.option_panel("Arguments")
@click.option_panel("Options")
@click.rich_config({
    "options_table_column_types": ["opt_long", "opt_short", "help"],
    "options_table_help_sections": ["metavar", "required", "help", "default", "envvar"]
})
def move_item(src, dest, env, log_level):
    """Move an item from a src location to a dest location"""
    pass

if __name__ == "__main__":
    move_item()
Output

Plans for the future

Our short term plan is to monitor issues aggressively over the next month to patch any problems people encounter with rich-click 1.9. Please submit issues on Github if you encounter any strange behavior! We appreciate your assistance in making rich-click as robust as it can be.

In the 1.8 release blog post, we said we anticipated 1.9 coming out by end of year 2024. Oops! Making promises about when things will happen is hard, especially when work and personal obligations get in the way. So, I won't make the same mistake again: we have no promises or hints for when a 1.10 (or a 2.0?) will release. We do have a roadmap in place for 1.10, so we have a good sense of what we want out of future releases. Will that be next year, or the year after? Will the roadmap change substantially when that time comes? It's tough to say!

The ultimate goal of rich-click is to become the most beautiful CLI help text generator out there. Nearing the end of development for 1.9, I couldn't help but think of all the ways we can continue to improve on that. For example, a big limitation of themes are that they mostly use ANSI colors; and the ones which use hex values do not detect the terminal background color. We'd love to have background color detection and intelligently apply colors so that beautiful themes like nord and dracula look good regardless of the terminal environment.

But, why spend so much time dwelling on the future, when the present state of things is so great and we've barely had time to enjoy it? We're extremely happy with where 1.9 leaves rich-click! And we hope you agree!

Thank you once again for using rich-click! ❤️❤️❤️❤️❤️