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)