Large CLIs need fast startup and extensibility, which come from controlling how commands are found.
A click.Group finds subcommands through two methods:
list_commands(ctx)returns the names shown in help and completion,get_command(ctx, name)returns theCommandfor a name, orNone.
The default implementation reads a dict of registered commands. Overriding these methods changes discovery without touching the rest of Click.
Lazy loading: map names to import paths ("deploy": "mytool.commands.deploy:cli") and import the module only in get_command. Then mytool --help and mytool status no longer import the heavy dependencies of every other command, which can cut startup from seconds to milliseconds. Note that --help on the group calls get_command for every listed command to fetch short help, so keep command modules themselves light, or provide summaries separately.
Plugins: discover commands from installed packages via importlib.metadata.entry_points(group="mytool.plugins") in list_commands/get_command.
Aliases and prefixes: get_command can resolve ci to commit, or accept unique prefixes, by matching against list_commands.
Use a custom class with @click.group(cls=LazyGroup, ...); extra keyword arguments go to its __init__. Other override points include format_commands for help output, resolve_command for parsing, and Command.invoke or Context subclasses (context_class) for cross-cutting behaviour such as timing every command.
Example
import importlib
import click
class LazyGroup(click.Group):
def __init__(self, *args, lazy=None, **kwargs):
super().__init__(*args, **kwargs)
self.lazy = lazy or {} # name -> "module:attr"
def list_commands(self, ctx):
return sorted([*super().list_commands(ctx), *self.lazy])
def get_command(self, ctx, name):
if name in self.lazy:
module, attr = self.lazy[name].split(":")
return getattr(importlib.import_module(module), attr) # imported on demand
matches = [c for c in self.list_commands(ctx) if c.startswith(name)]
return super().get_command(ctx, matches[0]) if len(matches) == 1 else None
@click.group(cls=LazyGroup, lazy={"pretty": "json:tool"}) # demo target: any importable object
def cli():
pass
@cli.command()
def status():
click.echo("all good")
print(cli.list_commands(None)) # ['pretty', 'status']
cli.main(["stat"], standalone_mode=False) # all good (unique prefix)Remember
- Override list_commands and get_command to control discovery
- Lazy loading imports a subcommand's module only when it is used
- Plugins, aliases and prefix matching are all get_command logic
Watch out for
- Making the lazily loaded modules themselves import everything, which defeats the purpose.