Skip to main content

Organizing Subcommands and Groups

In this codebase, organizing complex command-line interfaces is achieved by composing multiple Typer instances. This modular approach allows you to define subcommands in separate modules and then assemble them into a single, unified hierarchy using the add_typer() method.

Building Command Hierarchies

The Typer class in typer/main.py serves as the primary container for commands and sub-apps. When you create a Typer instance, you are essentially creating a command group that can hold its own commands (via @app.command()) and other Typer instances (via app.add_typer()).

Nesting Sub-apps

To create a subcommand that itself contains multiple commands, you nest one Typer instance inside another. By providing a name to add_typer(), you define the command name that will trigger the sub-app.

import typer

# Define a sub-app for user management
users_app = typer.Typer()

@users_app.command()
def create(name: str):
print(f"Creating user: {name}")

# Define the main app
app = typer.Typer()

# Nest the users_app under the "users" command
app.add_typer(users_app, name="users")

In this structure, the command to create a user becomes python main.py users create --name "Jane".

Merging vs. Nesting

The behavior of add_typer() changes significantly depending on whether the name parameter is provided:

  1. Nesting (with name): The sub-app is treated as a distinct subcommand group. Its callback (if defined) will be executed when any of its subcommands are called.
  2. Merging (without name): The commands from the sub-app are "unpacked" and added directly to the parent Typer instance.

As seen in typer/main.py, if you merge a sub-app that has its own callback, Typer will issue a warning because that callback will be ignored:

# From typer/main.py
if not sub_group.name:
if sub_group.callback:
warnings.warn(
"The 'callback' parameter is not supported by Typer when using `add_typer` without a name",
stacklevel=5,
)
for sub_command_name, sub_command in sub_group.commands.items():
commands[sub_command_name] = sub_command

The TyperGroup Class

Underneath every Typer instance is a TyperGroup, located in typer/core.py. This class inherits from click.Group and provides several enhancements that improve the user experience of the generated CLI.

Command Order Preservation

Unlike standard Click groups which sort subcommands alphabetically in help menus, TyperGroup preserves the order in which commands were registered. This is implemented in the list_commands method:

# From typer/core.py
def list_commands(self, ctx: click.Context) -> list[str]:
"""Returns a list of subcommand names.
In Typer, we wish to maintain the original order of creation."""
return [n for n, c in self.commands.items()]

Command Suggestions

TyperGroup includes built-in support for suggesting the correct command when a user makes a typo. It uses get_close_matches to compare the input against available commands:

# From typer/core.py
def resolve_command(self, ctx: click.Context, args: list[str]):
try:
return super().resolve_command(ctx, args)
except click.UsageError as e:
if self.suggest_commands:
available_commands = list(self.commands.keys())
if available_commands and args:
typo = args[0]
matches = get_close_matches(typo, available_commands)
if matches:
suggestions = ", ".join(f"{m!r}" for m in matches)
e.message = f"{e.message}. Did you mean {suggestions}?"
raise

Help Text Resolution

When nesting apps, help text can be defined in multiple places. Typer resolves which text to display in the help menu based on the following priority (from highest to lowest):

  1. The help parameter passed directly to add_typer(..., help="...").
  2. The help parameter in the sub-app's @app.callback(help="...").
  3. The help parameter in the sub-app's constructor typer.Typer(help="...").
  4. The docstring of the function decorated by the sub-app's @app.callback().

This resolution logic ensures that the parent app can override the help text of a sub-app if necessary, while allowing the sub-app to provide its own defaults.

Customizing the Group Class

For advanced use cases, you can provide a custom class to the Typer constructor using the cls parameter. This class must be a subclass of TyperGroup. This is useful if you need to override Click's underlying group behavior beyond what Typer provides by default.

from typer.core import TyperGroup

class CustomGroup(TyperGroup):
# Custom logic here
pass

app = typer.Typer(cls=CustomGroup)