Command Resolution and Suggestions
Typer enhances the standard command resolution process of the underlying Click library by providing fuzzy-matching suggestions when a user enters a command that does not exist. This feature improves the developer experience by guiding users toward the correct command name when they make minor typos.
The Role of TyperGroup in Resolution
The core logic for command resolution and suggestions resides in the TyperGroup class within typer/core.py. As a subclass of click.Group, TyperGroup is responsible for matching user input against registered subcommands.
Typer overrides the resolve_command method to intercept cases where Click would normally fail with a "No such command" error. When a click.UsageError is caught, Typer checks if command suggestions are enabled and attempts to find close matches among the available commands.
# typer/core.py
class TyperGroup(click.core.Group):
# ...
def resolve_command(
self, ctx: click.Context, args: list[str]
) -> tuple[str | None, click.Command | None, 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)
message = e.message.rstrip(".")
e.message = f"{message}. Did you mean {suggestions}?"
raise
Fuzzy Matching with difflib
The suggestion mechanism relies on Python's standard difflib.get_close_matches function. This function compares the mistyped string (the first element in args) against the list of keys in self.commands. If matches are found, Typer appends a "Did you mean...?" string to the original error message before re-raising the exception.
This approach ensures that the suggestion logic only triggers when resolution fails, maintaining the performance of successful command lookups.
Configuration and Control
The suggestion behavior is controlled by the suggest_commands parameter, which is enabled by default. This setting is exposed through the main Typer class constructor in typer/main.py and passed down to the underlying TyperGroup.
# typer/main.py
class Typer:
def __init__(
self,
# ...
suggest_commands: Annotated[
bool,
Doc(
"""
As of version 0.20.0, Typer provides support for mistyped command names by printing helpful suggestions.
You can turn this setting off with `suggest_commands`.
"""
),
] = True,
# ...
):
self.suggest_commands = suggest_commands
# ...
Developers can disable this feature globally for an application or for specific sub-apps added via add_typer.
Example: Enabled vs. Disabled Suggestions
In a standard Typer application, a typo like crate instead of create results in a helpful suggestion:
import typer
from typer.testing import CliRunner
app = typer.Typer()
@app.command()
def create():
typer.echo("Creating...")
runner = CliRunner()
result = runner.invoke(app, ["crate"])
# Output: Error: No such command 'crate'. Did you mean 'create'?
If suggestions are explicitly disabled, the output reverts to the standard Click error:
app = typer.Typer(suggest_commands=False)
@app.command()
def create():
typer.echo("Creating...")
result = runner.invoke(app, ["crate"])
# Output: Error: No such command 'crate'.
Implementation Tradeoffs and Constraints
The implementation of command suggestions in Typer involves several design decisions:
- Level-Specific Resolution: Suggestions are only generated for the current command group level. If a user types a command that exists in a different sub-app or a different level of the command hierarchy, Typer will not find it unless the typo occurs at that specific level. This is because
resolve_commandonly has access toself.commandsfor the currentTyperGroup. - Dependency on UsageError: The logic is tightly coupled to Click's exception hierarchy. It specifically intercepts
click.UsageError. If a different error occurs during the resolution process (such as a custom exception in a callback), the suggestion logic will not be triggered. - Fuzzy Match Threshold: By using the default settings of
difflib.get_close_matches, Typer inherits the standard similarity threshold (0.6). This means very distant typos might not trigger any suggestions, preventing the CLI from providing irrelevant or confusing "Did you mean...?" prompts for completely unrelated input. - Command Ordering: While
resolve_commandhandles the lookup,TyperGroupalso overrideslist_commandsto ensure that the order of commands in help output matches the order of registration, rather than being sorted alphabetically as in standard Click. This helps maintain the developer's intended logical grouping when users are looking for the correct command.
# 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 (cf Issue #933)"""
return [n for n, c in self.commands.items()]