Help Customization

Cyclopts provides extensive customization options for help screen appearance and formatting through the help_formatter parameter available on both App and Group. These parameters accept formatters that follow the HelpFormatter protocol.

Setting Help Formatters

App-Level Formatting

The App class accepts a help_formatter parameter that controls the default formatting for all help output:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PlainFormatter

# Use a built-in formatter by name: {"default", "plain"}
app = App(help_formatter="plain")

# Or pass a formatter instance with custom configuration
app = App(
    help_formatter=DefaultFormatter(
        # Custom configuration options
    )
)

# Or use a completely custom formatter; see HelpFormatter protocol.
app = App(help_formatter=MyCustomFormatter())

Group-Level Formatting

Individual Group instances can have their own help_formatter that overrides the app-level default:

from cyclopts import App, Group
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE

# Create a group with custom formatting
advanced_group = Group(
    "Advanced Options",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            border_style="red",
            box=DOUBLE,
        )
    )
)

# The app can have a different default formatter
app = App(help_formatter="plain")

# Parameters in advanced_group will use the group's formatter,
# while other parameters use the app's formatter

Built-in Formatters

DefaultFormatter

The DefaultFormatter is the default help formatter that uses Rich for beautiful terminal output with colors, borders, and structured layouts.

from cyclopts import App

# Explicitly use the default formatter (same as not specifying)
app = App(help_formatter="default")

@app.default
def main(name: str, count: int = 1):
    """A simple greeting application.

    Parameters
    ----------
    name : str
        Person to greet.
    count : int
        Number of times to greet.
    """
    for _ in range(count):
        print(f"Hello, {name}!")

if __name__ == "__main__":
    app()

Output:

Usage: my-app [ARGS] [OPTIONS]

A simple greeting application.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                                    │
│ --version  Display application version.                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│ *  NAME --name    Person to greet. [required]                                │
│    COUNT --count  Number of times to greet. [default: 1]                     │
╰──────────────────────────────────────────────────────────────────────────────╯

PlainFormatter

The PlainFormatter provides accessibility-focused plain text output without colors or special characters, ideal for screen readers and simpler terminals.

from cyclopts import App

# Use plain text formatter for accessibility
app = App(help_formatter="plain")

@app.default
def main(name: str, count: int = 1):
    """A simple greeting application.

    Parameters
    ----------
    name : str
        Person to greet.
    count : int
        Number of times to greet.
    """
    for _ in range(count):
        print(f"Hello, {name}!")

if __name__ == "__main__":
    app()

Output:

Usage: demo.py [ARGS] [OPTIONS]

A simple greeting application.

Commands:
--help, -h: Display this message and exit.
--version: Display application version.

Parameters:
NAME, --name: Person to greet.
COUNT, --count: Number of times to greet.

Basic Customization

The DefaultFormatter accepts several customization options through its initialization parameters.

Panel Customization

The PanelSpec controls the outer panel appearance:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE

app = App(
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=DOUBLE,              # Use double-line borders
            border_style="blue",     # Blue border color
            padding=(1, 2),         # (vertical, horizontal) padding
            expand=True,            # Expand to full terminal width
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with custom panel styling."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Usage: demo.py [ARGS] [OPTIONS]

Process a file with custom panel styling.

╔═ Commands ═══════════════════════════════════════════════════════════╗
║                                                                      ║
--help -h  Display this message and exit.                           
--version  Display application version.                             
║                                                                      ║
╚══════════════════════════════════════════════════════════════════════╝
╔═ Parameters ═════════════════════════════════════════════════════════╗
║                                                                      ║
*  PATH --path                     [required]                       
VERBOSE --verbose  [default: False]                              
--no-verbose                                                   
║                                                                      ║
╚══════════════════════════════════════════════════════════════════════╝

Table Customization

The TableSpec controls the table styling within panels:

from cyclopts import App
from cyclopts.help import DefaultFormatter, TableSpec

app = App(
    help_formatter=DefaultFormatter(
        table_spec=TableSpec(
            show_header=True,  # Show column headers
            show_lines=True,  # Show lines between rows
            show_edge=False,  # Remove outer table border
            border_style="green",  # Green table elements
            padding=(0, 2, 0, 0),  # Extra right padding
            box=SQUARE,  # otherwise we won't see the lines
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with custom table styling."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Usage: test_table_custom.py [ARGS] [OPTIONS]

Process a file with custom table styling.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ Command    Description                                                      │
│ ───────────┼──────────────────────────────────────────────────────────────── │
│ --help -h  Display this message and exit.                                   │
│ ───────────┼──────────────────────────────────────────────────────────────── │
│ --version  Display application version.                                     │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│    Option             Description                                          │
│ ───┼───────────────────┼──────────────────────────────────────────────────── │
│ *  PATH --path        [required]                                           │
│ ───┼───────────────────┼──────────────────────────────────────────────────── │
│    VERBOSE --verbose  [default: False]                                     │
│      --no-verbose                                                          │
╰──────────────────────────────────────────────────────────────────────────────╯

Combining Customizations

You can combine both panel and table specifications:

from cyclopts import App
from cyclopts.help import DefaultFormatter, PanelSpec, TableSpec
from rich.box import ROUNDED

app = App(
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=ROUNDED,
            border_style="cyan",
            padding=(0, 1),
        ),
        table_spec=TableSpec(
            show_header=False,
            show_lines=False,
            padding=(0, 1),
        )
    )
)

@app.default
def main(path: str, verbose: bool = False):
    """Process a file with combined customizations."""
    print(f"Processing {path}")

if __name__ == "__main__":
    app()

Output:

Usage: my-app [ARGS] [OPTIONS]

Process a file with combined customizations.

╭─ Commands ──────────────────────────────────────────────────────────╮
--help -h  Display this message and exit.                           
--version  Display application version.                             
╰─────────────────────────────────────────────────────────────────────╯
╭─ Parameters ────────────────────────────────────────────────────────╮
*  PATH --path       [required]                                     
VERBOSE --verbose [default: False]                               
╰─────────────────────────────────────────────────────────────────────╯

Group-Level Formatting

Different parameter groups can have different formatting styles, allowing you to visually distinguish between different types of options:

from cyclopts import App, Group, Parameter
from cyclopts.help import DefaultFormatter, PanelSpec
from rich.box import DOUBLE, MINIMAL
from typing import Annotated

# Create groups with different styles
required_group = Group(
    "Required Options",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=DOUBLE,
            border_style="red bold",
        )
    )
)

optional_group = Group(
    "Optional Settings",
    help_formatter=DefaultFormatter(
        panel_spec=PanelSpec(
            box=MINIMAL,
            border_style="green",
        )
    )
)

app = App()

@app.default
def main(
    # Required parameters with red double border
    input_file: Annotated[str, Parameter(group=required_group)],
    output_dir: Annotated[str, Parameter(group=required_group)],

    # Optional parameters with green minimal border
    verbose: Annotated[bool, Parameter(group=optional_group)] = False,
    threads: Annotated[int, Parameter(group=optional_group)] = 4,
):
    """Process files with styled help groups."""
    print(f"Processing {input_file} -> {output_dir}")
    if verbose:
        print(f"Using {threads} threads")

if __name__ == "__main__":
    app()

Output:

Usage: test_group_formatting.py [ARGS] [OPTIONS]

Process files with styled help groups.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                                    │
│ --version  Display application version.                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
   Optional Settings                                                            
  VERBOSE --verbose  [default: False]                                           
    --no-verbose                                                                
  THREADS --threads  [default: 4]                                               
                                                                                
╔═ Required Options ═══════════════════════════════════════════════════════════╗
 *  INPUT-FILE --input-file  [required]                                       
 *  OUTPUT-DIR --output-dir  [required]                                       
╚══════════════════════════════════════════════════════════════════════════════╝

Custom Column Layout

For complete control over the help table layout, you can define custom columns using ColumnSpec:

from cyclopts import App, Group, Parameter
from cyclopts.help import DefaultFormatter, ColumnSpec, TableSpec
from typing import Annotated

# Define custom column renderers
def names_renderer(entry):
    """Combine parameter names and shorts."""
    names = " ".join(entry.names) if entry.names else ""
    shorts = " ".join(entry.shorts) if entry.shorts else ""
    return f"{names} {shorts}".strip()

def type_renderer(entry):
    """Show the parameter type."""
    from cyclopts.annotations import get_hint_name
    return get_hint_name(entry.type) if entry.type else ""

# Create custom columns
custom_group = Group(
    "Custom Layout",
    help_formatter=DefaultFormatter(
        table_spec=TableSpec(show_header=True),
        column_specs=(
            ColumnSpec(
                renderer=lambda e: "★" if e.required else " ",
                header="",
                width=2,
                style="yellow bold",
            ),
            ColumnSpec(
                renderer=names_renderer,
                header="Option",
                style="cyan",
                max_width=30,
            ),
            ColumnSpec(
                renderer=type_renderer,
                header="Type",
                style="magenta",
                justify="center",
            ),
            ColumnSpec(
                renderer="description",  # Use attribute name
                header="Description",
                overflow="fold",
            ),
        )
    )
)

app = App()

@app.default
def main(
    input_path: Annotated[str, Parameter(group=custom_group, help="Input file path")],
    output_path: Annotated[str, Parameter(group=custom_group, help="Output file path")],
    count: Annotated[int, Parameter(group=custom_group, help="Number of iterations")] = 1,
):
    """Demo custom column layout."""
    print(f"Processing {input_path} -> {output_path} ({count} times)")

if __name__ == "__main__":
    app()

Output:

Usage: test_custom_column.py [ARGS] [OPTIONS]

Demo custom column layout.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ --help -h  Display this message and exit.                                    │
│ --version  Display application version.                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Custom Layout ──────────────────────────────────────────────────────────────╮
│     Option                     Type  Description                             │
│    INPUT-PATH --input-path    str   Input file path                         │
│    OUTPUT-PATH --output-path  str   Output file path                        │
│     COUNT --count              int   Number of iterations                    │
╰──────────────────────────────────────────────────────────────────────────────╯

Dynamic Column Builders

For even more flexibility, you can create columns dynamically based on runtime conditions:

from cyclopts import App, Parameter
from cyclopts.help import DefaultFormatter, ColumnSpec
from typing import Annotated

def dynamic_columns(console, options, entries):
    """Build columns based on console width and entries."""
    columns = []

    # Only show required indicator if there are required params
    if any(e.required for e in entries):
        columns.append(ColumnSpec(
            renderer=lambda e: "*" if e.required else "",
            width=2,
            style="red",
        ))

    # Adjust name column width based on console size
    max_width = min(40, int(console.width * 0.3))
    columns.append(ColumnSpec(
        renderer=lambda e: " ".join(e.names + e.shorts),
        header="Option",
        max_width=max_width,
        style="cyan",
    ))

    # Always include description
    columns.append(ColumnSpec(
        renderer="description",
        header="Description",
        overflow="fold",
    ))

    return tuple(columns)

app = App(
    help_formatter=DefaultFormatter(
        column_specs=dynamic_columns
    )
)

@app.default
def main(
    input_file: str,
    output_file: str,
    verbose: bool = False,
):
    """Process files with dynamic columns."""
    print(f"Processing {input_file} -> {output_file}")

if __name__ == "__main__":
    app()

Output (adjusts based on terminal width):

Usage: test_dynamic_columns.py [ARGS] [OPTIONS]

Process files with dynamic columns.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ Option     Description                                                       │
│ --help -h  Display this message and exit.                                    │
│ --version  Display application version.                                      │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
│     Option                    Description                                    │
│ *   INPUT-FILE --input-file                                                  │
│ *   OUTPUT-FILE                                                              │
│     --output-file                                                            │
│     VERBOSE --verbose                                                        │
│     --no-verbose                                                             │
╰──────────────────────────────────────────────────────────────────────────────╯

Creating Custom Formatters

For complete control, you can implement your own formatter by following the HelpFormatter protocol. The formatter methods receive the console and options first, followed by the content to render:

from cyclopts import App
from cyclopts.help import HelpPanel
from rich.console import Console, ConsoleOptions
from rich.table import Table
from rich.panel import Panel

class MyCustomFormatter:
    """A custom formatter with unique styling."""

    def __call__(self, console: Console, options: ConsoleOptions, panel: HelpPanel) -> None:
        """Render a help panel with custom styling."""
        if not panel.entries:
            return

        # Create a custom table
        table = Table(show_header=True, header_style="bold magenta")
        table.add_column("Option", style="cyan", no_wrap=True)
        table.add_column("Description", style="white")

        for entry in panel.entries:
            name = " ".join(entry.names + entry.shorts)
            # Extract plain text from description (handles InlineText, etc)
            desc = ""
            if entry.description:
                if hasattr(entry.description, 'plain'):
                    desc = entry.description.plain
                elif hasattr(entry.description, '__rich_console__'):
                    # Render to plain text without styles
                    with console.capture() as capture:
                        console.print(entry.description, end="")
                    desc = capture.get()
                else:
                    desc = str(entry.description)
            table.add_row(name, desc)

        # Wrap in a custom panel
        panel_title = panel.title or "Options"
        styled_panel = Panel(
            table,
            title=f"[bold blue]{panel_title}[/bold blue]",
            border_style="blue",
        )

        console.print(styled_panel)

    def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
        """Render the usage line."""
        if usage:
            console.print(f"[bold green]Usage:[/bold green] {usage}")

    def render_description(self, console: Console, options: ConsoleOptions, description) -> None:
        """Render the description."""
        if description:
            console.print(f"\n[italic]{description}[/italic]\n")

# Use the custom formatter
app = App(help_formatter=MyCustomFormatter())

@app.default
def main(input_file: str, output_file: str, verbose: bool = False):
    """Process files with custom formatter."""
    print(f"Processing {input_file} -> {output_file}")

if __name__ == "__main__":
    app()

Output:

Usage: test_custom_formatter.py [ARGS] [OPTIONS]

Process files with custom formatter.

╭─ Commands ───────────────────────────────────────────────────────────────────╮
 ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓                               
 Option     Description                    
 ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩                               
 --help -h │ Display this message and exit. │                               
 --version │ Display application version.   │                               
 └───────────┴────────────────────────────────┘                               
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Parameters ─────────────────────────────────────────────────────────────────╮
 ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓                             
 Option                          Description 
 ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩                             
 INPUT-FILE --input-file        │             │                             
 OUTPUT-FILE --output-file      │             │                             
 VERBOSE --verbose --no-verbose │             │                             
 └────────────────────────────────┴─────────────┘                             
╰──────────────────────────────────────────────────────────────────────────────╯

Reference

For complete API documentation of help formatting components, see:

See also:

  • Help - General help system documentation

  • Groups - Organizing parameters into groups