Skip to main content

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:

  1. 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.
  2. Help Parameter: Use the help argument in @app.command(help="...") to explicitly set the help text.
  3. Short Help: Use short_help for a condensed version displayed in the command list.
  4. Epilog: Use epilog to 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 be sync-data. If you want to keep the underscore, you must explicitly name it: @app.command("sync_data").
  • Single Command Behavior: If your Typer app 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).