CLI Options with Help¶
You already saw how to add a help text for CLI arguments with the help parameter.
Let's now do the same for CLI options:
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "",
formal: Annotated[bool, typer.Option(help="Say hi formally.")] = False,
):
"""
Say hi to 'name', optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: str = typer.Option("", help="Last name of person to greet."),
formal: bool = typer.Option(False, help="Say hi formally."),
):
"""
Say hi to 'name', optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
The same way as with typer.Argument(), we can put typer.Option() inside of Annotated.
We can then pass the help keyword parameter:
lastname: Annotated[str, typer.Option(help="this option does this and that")] = ""
...to create the help for that CLI option.
The same way as with typer.Argument(), Typer also supports the old style using the function parameter default value:
lastname: str = typer.Option(default="", help="this option does this and that")
Copy that example from above to a file main.py.
Test it:
$ uv run python main.py --help
Usage: main.py [OPTIONS] {name}
Say hi to 'name', optionally with a --lastname.
If --formal is used, say hi very formally.
Arguments:
name [required]
Options:
--lastname <str> Last name of person to greet.
--formal / --no-formal Say hi formally. [default: no-formal]
--help Show this message and exit.
// Now you have a help text for the --lastname and --formal CLI options 🎉
CLI Options help panels¶
The same as with CLI arguments, you can put the help for some CLI options in different panels to be shown with the --help option.
Using Rich, you can set the rich_help_panel parameter to the name of the panel you want for each CLI option:
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: Annotated[str, typer.Option(help="Last name of person to greet.")] = "",
formal: Annotated[
bool,
typer.Option(
help="Say hi formally.", rich_help_panel="Customization and Utils"
),
] = False,
debug: Annotated[
bool,
typer.Option(
help="Enable debugging.", rich_help_panel="Customization and Utils"
),
] = False,
):
"""
Say hi to 'name', optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
import typer
app = typer.Typer()
@app.command()
def main(
name: str,
lastname: str = typer.Option("", help="Last name of person to greet."),
formal: bool = typer.Option(
False, help="Say hi formally.", rich_help_panel="Customization and Utils"
),
debug: bool = typer.Option(
False, help="Enable debugging.", rich_help_panel="Customization and Utils"
),
):
"""
Say hi to 'name', optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
print(f"Good day Ms. {name} {lastname}.")
else:
print(f"Hello {name} {lastname}")
if __name__ == "__main__":
app()
Now, when you check the --help option, you will see a default panel named "Options" for the CLI options that don't have a custom rich_help_panel.
And below you will see other panels for the CLI options that have a custom panel set in the rich_help_panel parameter:
$ uv run python main.py --help
<b> </b><font color="#F4BF75"><b>Usage: </b></font><b>main.py [OPTIONS] {name} </b>
<b> </b>
Say hi to 'name', optionally with a <font color="#A1EFE4"><b>--lastname</b></font>.
If <font color="#6B9F98"><b>--formal</b></font><font color="#A5A5A1"> is used, say hi very formally. </font>
<font color="#A5A5A1">â•─ Arguments ───────────────────────────────────────────────────────╮</font>
<font color="#A5A5A1">│ </font><font color="#F92672">*</font> name <font color="#F4BF75"><b><str></b></font> <font color="#A6194C">[required]</font> │
<font color="#A5A5A1">╰───────────────────────────────────────────────────────────────────╯</font>
<font color="#A5A5A1">â•─ Options ─────────────────────────────────────────────────────────╮</font>
<font color="#A5A5A1">│ </font><font color="#A1EFE4"><b>--lastname</b></font> <font color="#F4BF75"><b><str></b></font> Last name of person to greet. │
<font color="#A5A5A1">│ </font><font color="#A1EFE4"><b>--help</b></font> <font color="#F4BF75"><b> </b></font> Show this message and exit. │
<font color="#A5A5A1">╰───────────────────────────────────────────────────────────────────╯</font>
<font color="#A5A5A1">â•─ Customization and Utils ─────────────────────────────────────────╮</font>
<font color="#A5A5A1">│ </font><font color="#A1EFE4"><b>--formal</b></font> <font color="#AE81FF"><b>--no-formal</b></font> Say hi formally. │
<font color="#A5A5A1">│ [default: no-formal] │</font>
<font color="#A5A5A1">│ </font><font color="#A1EFE4"><b>--debug</b></font> <font color="#AE81FF"><b>--no-debug</b></font> Enable debugging. │
<font color="#A5A5A1">│ [default: no-debug] │</font>
<font color="#A5A5A1">╰───────────────────────────────────────────────────────────────────╯</font>
Here we have a custom CLI options panel named "Customization and Utils".
Align option and argument columns across panels¶
By default, each panel sizes its own columns, so the columns don't line up between panels.
You can make typer.Typer() give every panel the same fixed column widths with align_panel_columns=True:
from typing import Annotated
import typer
app = typer.Typer(align_panel_columns=True, add_completion=False)
@app.command()
def main(
source: Annotated[str, typer.Argument(help="Source path.")],
dest: Annotated[str, typer.Argument(help="Destination path.")] = "",
short_flag: Annotated[
bool, typer.Option("-a", help="Short flag.", rich_help_panel="Short")
] = False,
short_value: Annotated[
str, typer.Option("-b", help="Short value.", rich_help_panel="Short")
] = "",
mixed_value: Annotated[
str,
typer.Option(
"--mixed-long", "-m", help="Mixed value.", rich_help_panel="Mixed"
),
] = "",
mixed_flag: Annotated[
bool,
typer.Option("--mixed-flag", "-f", help="Mixed flag.", rich_help_panel="Mixed"),
] = False,
long_value: Annotated[
str, typer.Option("--alpha", help="Long value.", rich_help_panel="Long")
] = "",
long_flag: Annotated[
bool, typer.Option("--beta", help="Long flag.", rich_help_panel="Long")
] = False,
) -> None:
pass
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
import typer
app = typer.Typer(align_panel_columns=True, add_completion=False)
@app.command()
def main(
source: str = typer.Argument(help="Source path."),
dest: str = typer.Argument("", help="Destination path."),
short_flag: bool = typer.Option(
False, "-a", help="Short flag.", rich_help_panel="Short"
),
short_value: str = typer.Option(
"", "-b", help="Short value.", rich_help_panel="Short"
),
mixed_value: str = typer.Option(
"", "--mixed-long", "-m", help="Mixed value.", rich_help_panel="Mixed"
),
mixed_flag: bool = typer.Option(
False, "--mixed-flag", "-f", help="Mixed flag.", rich_help_panel="Mixed"
),
long_value: str = typer.Option(
"", "--alpha", help="Long value.", rich_help_panel="Long"
),
long_flag: bool = typer.Option(
False, "--beta", help="Long flag.", rich_help_panel="Long"
),
) -> None:
pass
if __name__ == "__main__":
app()
Now CLI arguments, CLI options and every CLI options panel share the same grid. The names of the CLI arguments line up with the long names of the CLI options, and the required marker (*) column is shared too, reserved whenever any CLI argument or CLI option is required.
Here the command has a required CLI argument and an optional one, one panel has only short options, another mixes short and long options, and the last has only long options:
$ uv run python main.py --help
Usage: main.py [OPTIONS] {source} [dest]
â•─ Arguments ──────────────────────────────────────────────────────────────────╮
│ * source <str> Source path. [required] │
│ dest <str> Destination path. │
╰──────────────────────────────────────────────────────────────────────────────╯
â•─ Options ────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
â•─ Short ──────────────────────────────────────────────────────────────────────╮
│ -a Short flag. │
│ -b <str> Short value. │
╰──────────────────────────────────────────────────────────────────────────────╯
â•─ Mixed ──────────────────────────────────────────────────────────────────────╮
│ --mixed-long -m <str> Mixed value. │
│ --mixed-flag -f Mixed flag. │
╰──────────────────────────────────────────────────────────────────────────────╯
â•─ Long ───────────────────────────────────────────────────────────────────────╮
│ --alpha <str> Long value. │
│ --beta Long flag. │
╰──────────────────────────────────────────────────────────────────────────────╯
A complete alignment example¶
The following example puts it all together. It has a required CLI argument, an optional one and a variadic one, plus CLI options covering every shape that changes the help layout: short-only, long-only, an alias, mixed, negative long-only, negative short-only, negative long and short, a custom metavar, an enum, a numeric range, a count option, a default, a custom default string, an environment variable, and a hidden option:
from enum import Enum
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer(align_panel_columns=True, add_completion=False)
class Color(str, Enum):
red = "red"
green = "green"
@app.command()
def main(
source: Annotated[str, typer.Argument(help="Source directory.")],
dest: Annotated[str, typer.Argument(help="Destination directory.")] = "dist",
extras: Annotated[list[str], typer.Argument(help="Extra files.")] = (),
token: Annotated[
str, typer.Option("--token", help="API token.", rich_help_panel="Required")
] = ...,
verbose: Annotated[
bool, typer.Option("-v", help="Verbose.", rich_help_panel="Basic flags")
] = False,
alpha: Annotated[
str,
typer.Option(
"--alpha",
"--aleph",
help="Long value with an alias.",
rich_help_panel="Basic flags",
),
] = "",
mixed: Annotated[
str,
typer.Option(
"--mixed-long", "-m", help="Mixed value.", rich_help_panel="Basic flags"
),
] = "",
force: Annotated[
bool,
typer.Option(
"--force/--no-force",
"-f",
help="Force.",
rich_help_panel="Negative flags",
),
] = False,
pretty: Annotated[
bool,
typer.Option("-p/-P", help="Pretty.", rich_help_panel="Negative flags"),
] = False,
formal: Annotated[
bool,
typer.Option(
"--formal/--no-formal", help="Formal.", rich_help_panel="Negative flags"
),
] = False,
output: Annotated[
Path,
typer.Option(
"--path", metavar="PATH", help="Output path.", rich_help_panel="Values"
),
] = Path("."),
color: Annotated[
Color, typer.Option("--color", help="Color.", rich_help_panel="Values")
] = Color.red,
level: Annotated[
int,
typer.Option("--level", min=0, max=5, help="Level.", rich_help_panel="Values"),
] = 1,
count: Annotated[
int,
typer.Option(
"--count", "-c", count=True, help="Count.", rich_help_panel="Values"
),
] = 0,
timeout: Annotated[
int,
typer.Option(
"--timeout",
help="Timeout.",
show_default=True,
rich_help_panel="Defaults",
),
] = 30,
mode: Annotated[
str,
typer.Option(
"--mode", help="Mode.", show_default="auto", rich_help_panel="Defaults"
),
] = "",
home: Annotated[
str,
typer.Option(
"--home",
envvar="HOME",
show_envvar=True,
help="Home.",
rich_help_panel="Defaults",
),
] = "",
secret: Annotated[str, typer.Option("--secret", hidden=True, help="Hidden.")] = "",
) -> None:
"""Build the project."""
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from enum import Enum
from pathlib import Path
import typer
app = typer.Typer(align_panel_columns=True, add_completion=False)
class Color(str, Enum):
red = "red"
green = "green"
@app.command()
def main(
source: str = typer.Argument(..., help="Source directory."),
dest: str = typer.Argument("dist", help="Destination directory."),
extras: list[str] = typer.Argument([], help="Extra files."),
token: str = typer.Option(
..., "--token", help="API token.", rich_help_panel="Required"
),
verbose: bool = typer.Option(
False, "-v", help="Verbose.", rich_help_panel="Basic flags"
),
alpha: str = typer.Option(
"",
"--alpha",
"--aleph",
help="Long value with an alias.",
rich_help_panel="Basic flags",
),
mixed: str = typer.Option(
"", "--mixed-long", "-m", help="Mixed value.", rich_help_panel="Basic flags"
),
force: bool = typer.Option(
False,
"--force/--no-force",
"-f",
help="Force.",
rich_help_panel="Negative flags",
),
pretty: bool = typer.Option(
False, "-p/-P", help="Pretty.", rich_help_panel="Negative flags"
),
formal: bool = typer.Option(
False, "--formal/--no-formal", help="Formal.", rich_help_panel="Negative flags"
),
output: Path = typer.Option(
Path("."),
"--path",
metavar="PATH",
help="Output path.",
rich_help_panel="Values",
),
color: Color = typer.Option(
Color.red, "--color", help="Color.", rich_help_panel="Values"
),
level: int = typer.Option(
1, "--level", min=0, max=5, help="Level.", rich_help_panel="Values"
),
count: int = typer.Option(
0, "--count", "-c", count=True, help="Count.", rich_help_panel="Values"
),
timeout: int = typer.Option(
30, "--timeout", help="Timeout.", show_default=True, rich_help_panel="Defaults"
),
mode: str = typer.Option(
"", "--mode", help="Mode.", show_default="auto", rich_help_panel="Defaults"
),
home: str = typer.Option(
"",
"--home",
envvar="HOME",
show_envvar=True,
help="Home.",
rich_help_panel="Defaults",
),
secret: str = typer.Option("", "--secret", hidden=True, help="Hidden."),
) -> None:
"""Build the project."""
if __name__ == "__main__":
app()
Even with all of those, every panel shares one grid, so the argument names, option long names, short names, negative names and metavars all line up, and the hidden option does not appear:
$ uv run python main.py --help
Usage: main.py [OPTIONS] {source} [dest] [extras]...
Build the project.
â•─ Arguments ──────────────────────────────────────────────────────────────────────────────────────╮
│ * source <str> Source directory. [required] │
│ dest <str> Destination directory. │
│ [default: dist] │
│ extras <str> Extra files. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Required ───────────────────────────────────────────────────────────────────────────────────────╮
│ * --token <str> API token. [required] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Basic flags ────────────────────────────────────────────────────────────────────────────────────╮
│ -v Verbose. │
│ --alpha,--aleph <str> Long value with an alias. │
│ --mixed-long -m <str> Mixed value. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Negative flags ─────────────────────────────────────────────────────────────────────────────────╮
│ --force -f --no-force Force. [default: no-force] │
│ -p -P Pretty. [default: P] │
│ --formal --no-formal Formal. [default: no-formal] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Values ─────────────────────────────────────────────────────────────────────────────────────────╮
│ --path PATH Output path. [default: .] │
│ --color <red|green> Color. [default: red] │
│ --level <int range> [0<=x<=5] Level. [default: 1] │
│ --count -c <int> Count. [default: 0] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
â•─ Defaults ───────────────────────────────────────────────────────────────────────────────────────╮
│ --timeout <int> Timeout. [default: 30] │
│ --mode <str> Mode. [default: (auto)] │
│ --home <str> Home. [env var: HOME] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
Help with style using Rich¶
In a future section you will see how to use custom markup in the help for CLI options when reading about Commands - Command Help.
If you are in a hurry you can jump there, but otherwise, it would be better to continue reading here and following the tutorial in order.
Hide default from help¶
You can tell Typer to not show the default value in the help text with show_default=False:
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(fullname: Annotated[str, typer.Option(show_default=False)] = "Wade Wilson"):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
import typer
app = typer.Typer()
@app.command()
def main(fullname: str = typer.Option("Wade Wilson", show_default=False)):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
And it will no longer show the default value in the help text:
$ uv run python main.py
Hello Wade Wilson
// Show the help
$ uv run python main.py --help
Usage: main.py [OPTIONS]
Options:
--fullname <str>
--help Show this message and exit.
// Notice there's no [default: Wade Wilson] 🔥
Custom default string¶
You can use the same show_default to pass a custom string (instead of a bool) to customize the default value to be shown in the help text:
from typing import Annotated
import typer
app = typer.Typer()
@app.command()
def main(
fullname: Annotated[
str, typer.Option(show_default="Deadpoolio the amazing's name")
] = "Wade Wilson",
):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
🤓 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
import typer
app = typer.Typer()
@app.command()
def main(
fullname: str = typer.Option(
"Wade Wilson", show_default="Deadpoolio the amazing's name"
),
):
print(f"Hello {fullname}")
if __name__ == "__main__":
app()
And it will be used in the help text:
$ uv run python main.py
Hello Wade Wilson
// Show the help
$ uv run python main.py --help
Usage: main.py [OPTIONS]
Options:
--fullname <str> [default: (Deadpoolio the amazing's name)]
--help Show this message and exit.
// Notice how it shows "(Deadpoolio the amazing's name)" instead of the actual default of "Wade Wilson"