Registering Commands
To turn Python functions into CLI subcommands with custom help text and metadata, use the @app.command() decorator from a typer.Typer instance.
import typer
app = typer.Typer()
@app.command()
def create(username: str):
"""
Create a new user with USERNAME.
"""
print(f"Creating user: {username}")
@app.command(help="Delete a user with USERNAME.")
def delete(username: str):
print(f"Deleting user: {username}")
if __name__ == "__main__":
app()
Registering Commands
The primary way to register a command is by decorating a function with app.command(). By default, the CLI command name is derived from the function name. Typer automatically converts snake_case function names to kebab-case CLI commands (e.g., def sync_users() becomes sync-users).
You can override the command name by passing it as the first argument to the decorator:
@app.command("new-user")
def create(username: str):
print(f"Creating user: {username}")
Adding Help Text and Metadata
Typer provides multiple ways to define help text and metadata for your commands:
- Docstrings: The first line of the function's docstring is used as the short help, and the full docstring is used as the long help.
- Help Parameter: Use the
helpargument in@app.command(help="...")to explicitly set the help text. - Short Help: Use
short_helpfor a condensed version displayed in the command list. - Epilog: Use
epilogto add text at the very end of the help output.
@app.command(
help="Detailed explanation of the migration process.",
short_help="Run database migrations.",
epilog="Remember to backup your data before running this command.",
deprecated=True
)
def migrate():
print("Migrating...")
Organizing Commands with Rich Panels
If you have many commands, you can group them into logical sections in the help output using the rich_help_panel parameter. This requires the rich library to be installed and rich_markup_mode to be enabled on the Typer instance.
app = typer.Typer(rich_markup_mode="rich")
@app.command(rich_help_panel="User Management")
def create(username: str):
"""[green]Create[/green] a user."""
...
@app.command(rich_help_panel="User Management")
def delete(username: str):
"""[red]Delete[/red] a user."""
...
@app.command(rich_help_panel="System")
def status():
"""Check system [bold]status[/bold]."""
...
Defining Global Options with Callbacks
To handle global options (like --verbose or --version) that apply to all commands, use the @app.callback() decorator. This function runs before any subcommand is executed.
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", help="Enable verbose logging.")
):
if verbose:
print("Verbose mode is ON")
@app.command()
def run():
print("Running...")
Nesting Applications
For complex CLIs, you can nest Typer instances using app.add_typer(). This allows you to create sub-subcommands and organize your code into multiple modules.
# users.py
user_app = typer.Typer()
@user_app.command("create")
def user_create():
print("Creating user")
# main.py
app = typer.Typer()
app.add_typer(user_app, name="users", help="Manage users.")
if __name__ == "__main__":
app()
In this example, the command becomes python main.py users create.
Troubleshooting
- Command Name Conversion: If your function is named
sync_data, the CLI command will besync-data. If you want to keep the underscore, you must explicitly name it:@app.command("sync_data"). - Single Command Behavior: If your
Typerapp has exactly one command and no callback, Typer may execute that command directly without requiring the subcommand name. Adding a second command or an@app.callback()will restore standard subcommand behavior. - Hidden Commands: If you want a command to exist but not show up in the help menu, use
@app.command(hidden=True).