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:
- 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. - Merging (without
name): The commands from the sub-app are "unpacked" and added directly to the parentTyperinstance.
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):
- The
helpparameter passed directly toadd_typer(..., help="..."). - The
helpparameter in the sub-app's@app.callback(help="..."). - The
helpparameter in the sub-app's constructortyper.Typer(help="..."). - 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)