Typer renders help with Rich: boxed sections, aligned columns, colours on a capable terminal. Out of the box everything lands in two boxes — "Options" and "Commands" — which is fine for a handful of entries and hard to scan for twenty. Typer lets you split both into named help panels, use Rich markup inside help strings to highlight what matters, add an epilog with links, and control how uncaught exceptions are displayed. A few lines of configuration turn a long, flat help screen into one users can navigate. This guide covers panels, markup modes, epilogs and pretty exceptions, notes what changes when output is not a terminal, and shows how to test the layout. It belongs to the Typer vs Click topic, complementing the content advice in writing help text users actually read.
Prerequisites
- Typer 0.12+ (examples checked with 0.27), which includes Rich.
- The
Annotatedstyle from using Annotated options in Typer.
The recipe
# src/mytool/cli.py
from typing import Annotated
import typer
app = typer.Typer(
rich_markup_mode="rich",
help="[bold]mytool[/bold] — deploy and inspect services.",
epilog="Docs: [link=https://example.com/mytool]example.com/mytool[/link]",
)
@app.callback()
def main() -> None:
pass
@app.command(rich_help_panel="Deploying")
def deploy(
service: Annotated[str, typer.Argument(help="Service to deploy, e.g. [cyan]api[/cyan].")],
env: Annotated[str, typer.Option(help="Target environment.", rich_help_panel="Target")] = "staging",
region: Annotated[str, typer.Option(help="Region code.", rich_help_panel="Target")] = "eu-west-1",
dry_run: Annotated[bool, typer.Option(
"--dry-run", help="Show the plan [yellow]without[/yellow] changing anything.",
rich_help_panel="Safety")] = False,
yes: Annotated[bool, typer.Option(
"--yes", "-y", help="Skip the confirmation prompt.", rich_help_panel="Safety")] = False,
) -> None:
"""Deploy [bold]SERVICE[/bold] to an environment.
Rolls out the current build. Use [green]--dry-run[/green] first.
"""
typer.echo("ok")
@app.command(rich_help_panel="Deploying")
def rollback(service: str) -> None:
"""Roll back to the previous release."""
@app.command(rich_help_panel="Inspecting")
def status() -> None:
"""Show service status."""
@app.command(rich_help_panel="Inspecting")
def logs(service: str) -> None:
"""Tail service logs."""
The top-level help now groups commands by what users want to do:
╭─ Deploying ──────────────────────────────────────────────────────────╮
│ deploy Deploy SERVICE to an environment. │
│ rollback Roll back to the previous release. │
╰──────────────────────────────────────────────────────────────────────╯
╭─ Inspecting ─────────────────────────────────────────────────────────╮
│ status Show service status. │
│ logs Tail service logs. │
╰──────────────────────────────────────────────────────────────────────╯
Docs: example.com/mytool
And the command's own help separates targeting options from safety switches:
╭─ Target ─────────────────────────────────────────────────────────────╮
│ --env <str> Target environment. [default: staging] │
│ --region <str> Region code. [default: eu-west-1] │
╰──────────────────────────────────────────────────────────────────────╯
╭─ Safety ─────────────────────────────────────────────────────────────╮
│ --dry-run Show the plan without changing anything. │
│ --yes -y Skip the confirmation prompt. │
╰──────────────────────────────────────────────────────────────────────╯
Help panels
rich_help_panel= on a command or option names the box it appears in. Panels appear in the order they are first used, after the default "Options" or "Commands" box, so declare the most important group first. Parameters and commands without a panel stay in the default box, which is a good home for --help and the completion options. Panels are presentation only: parsing, defaults and validation are unchanged.
Markup modes
rich_markup_mode decides how help text is interpreted:
"rich"(the default in current Typer) — Rich console markup:[bold],[green],[link=…]…[/link], and so on. Square brackets that are not markup need escaping as\[, which matters for help that mentions[default]or shows an option like--format [json|csv]."markdown"— help strings and docstrings are rendered as Markdown:**bold**,`code`, bullet lists, links. Natural if your docs are Markdown anyway.None— plain text, no markup interpretation, the safest choice when help text is generated from data you do not control.
Pick one mode for the whole application, and keep markup light: a highlighted flag or a warning colour helps; a rainbow does not.
Epilogs and docstrings
The epilog appears after all panels — the place for a docs link, a pointer to --help of subcommands, or a short example. In command docstrings, the first line becomes the one-line summary shown in the parent's command list, and the rest becomes the command's description, which is why deploy's second paragraph only appears in its own help. Longer examples are better in the epilog than the docstring, as discussed in adding examples and epilogs to help output.
Panels for sub-applications
Larger CLIs are assembled from several Typer apps with add_typer, and the same parameter places a whole command group into a panel:
app.add_typer(users_app, name="users", rich_help_panel="Administration")
app.add_typer(audit_app, name="audit", rich_help_panel="Administration")
app.add_typer(services_app, name="services", rich_help_panel="Operations")
That keeps the top-level help short — a handful of panels with a few groups each — while each group's own --help lists its commands in detail. It also works well with automatically registered command modules, as in registering commands from modules automatically: a module can declare its panel next to its COMMAND_NAME, and the registration loop passes it through.
Pretty exceptions
Typer also installs a Rich-based exception handler, which prints uncaught exceptions as a formatted, shortened traceback. Three settings on typer.Typer control it:
app = typer.Typer(
pretty_exceptions_enable=True, # Rich tracebacks for uncaught errors
pretty_exceptions_short=True, # hide frames from Typer and Click internals
pretty_exceptions_show_locals=False, # never print local variables
)
Keep pretty_exceptions_show_locals=False, which is the current default: local variables in a CLI frequently include tokens, passwords from prompts and file contents, and a traceback pasted into an issue tracker would publish them. Better still, catch expected errors and print a one-line message, reserving tracebacks for genuine bugs — the approach in friendly error messages and tracebacks. Setting the environment variable TYPER_STANDARD_TRACEBACK=1 switches back to plain Python tracebacks (older releases used _TYPER_STANDARD_TRACEBACK, which is still honoured), which is handy when debugging or when a log collector parses tracebacks.
When output is not a terminal
Rich adapts to where output goes. Under CliRunner, in a pipe, or with NO_COLOR set, the boxes remain but colours and styles are dropped, and markup tags are stripped rather than printed — which is why the examples above show Deploy SERVICE without the [bold] tags. Width follows the terminal, or COLUMNS when set. Very narrow terminals wrap the panels heavily; if many users run your tool in narrow CI logs, test help at 80 columns, as in adapting output to terminal width.
UX considerations
- Group by task, not by type. "Target", "Safety", "Output" beat "String options" and "Flags".
- Three to five panels at most. More boxes than that is a different kind of wall.
- Keep the most dangerous switches visible. A "Safety" panel with
--dry-runand--yesnear the end of the help is easy to find and hard to miss. - Never encode meaning in colour alone. Colour is lost in pipes, logs and for colour-blind users; the words must carry the message.
- Escape literal brackets in
richmode, or switch that string to plain text.
Testing the behaviour
Pin the layout by rendering help at a fixed width and checking the panel order and placement:
# tests/test_help_panels.py
import re
from typer.testing import CliRunner
from mytool.cli import app
runner = CliRunner()
ENV = {"COLUMNS": "72", "NO_COLOR": "1"}
def panels(text: str) -> list[str]:
return re.findall(r"╭─ (.+?) ─", text)
def test_command_panels_in_order():
assert panels(runner.invoke(app, ["--help"], env=ENV).output) == [
"Options", "Deploying", "Inspecting"]
def test_option_panels_for_deploy():
output = runner.invoke(app, ["deploy", "--help"], env=ENV).output
assert panels(output) == ["Arguments", "Options", "Target", "Safety"]
safety = output.split("╭─ Safety")[1]
assert "--dry-run" in safety and "--yes" in safety
def test_markup_is_stripped_not_printed():
output = runner.invoke(app, ["deploy", "--help"], env=ENV).output
assert "[bold]" not in output and "Deploy SERVICE to an environment." in output
Asserting on panel titles rather than whole screens keeps the tests stable across small Typer formatting changes, while still catching an option that drifted into the wrong box. A full snapshot, as in snapshot testing CLI output, is the stricter alternative.
Conclusion
A few keyword arguments make Typer's help scannable: rich_help_panel to group commands and options by task, one consistent rich_markup_mode with light, meaningful markup, an epilog for links and examples, and pretty exceptions configured never to show locals. Remember that colour disappears outside a terminal so words must carry the meaning, and test panel order and placement at a fixed width so the structure survives future changes.
Frequently asked questions
Can I use help panels with plain Click?
Not natively; panels are a Typer feature built on Rich. The rich-click package adds similar grouping to Click applications, as covered in Rich-formatted help with rich-click.
How do I turn off Rich formatting entirely?
Set rich_markup_mode=None for plain-text help strings. Typer still draws boxes when Rich is installed; for completely plain help, many teams keep the Rich layout for terminals and rely on Rich's automatic simplification in pipes and logs.
Why do my square brackets disappear from help?
In rich mode they are parsed as markup tags. Escape them with a backslash (\[default]) or move that text to an epilog in a different mode.
Do panels affect shell completion?
No. Completion sees the same commands and options regardless of how help groups them.
Should options be sorted alphabetically within a panel?
Typer keeps declaration order, which is usually better than alphabetical: put the option most people need first and related options next to each other. Alphabetical order helps only when a panel has many similar options, such as a long list of output toggles.