Subcommands with groups
Build git-style multi-subcommand CLIs with @click.group() and pass shared state cleanly through Click's context object.
- Explain the group pattern and how it enables git-style subcommands
- Understand how the Click context object passes shared state between a group and its subcommands
- Design a clean command hierarchy for a multi-purpose tool
A single-command CLI is a function with flags. A multi-command CLI is a tree:
a top-level entry point that dispatches to one of several subcommands, each
with its own options. git commit, git push, git log — each is a separate
command under the git group.
Click's @click.group() decorator is the mechanism for this pattern.
The group decorator
Turning a command into a group changes it from a leaf into a dispatcher:
import click
@click.group()
def cli():
"""A data management tool."""
pass
@cli.command()
@click.argument("source")
def fetch(source):
"""Download data from SOURCE."""
click.echo(f"Fetching from {source}")
@cli.command()
@click.option("--format", default="table")
def report(format):
"""Print a report of fetched data."""
click.echo(f"Reporting in {format} format")
if __name__ == "__main__":
cli()Running python tool.py --help lists the subcommands. Running
python tool.py fetch --help shows the fetch command's help. Each subcommand
is a fully independent function; the group connects them.
Sharing state with context
Often subcommands need common configuration: the path to a config file, a database connection, a verbosity setting. Passing these as global variables defeats the purpose of having clean, testable functions.
Click's context object is the solution. Attach shared state to the context in
the group; subcommands retrieve it via @click.pass_context:
import click
@click.group()
@click.option("--config", default="config.toml", help="Path to config file.")
@click.pass_context
def cli(ctx, config):
"""My tool."""
ctx.ensure_object(dict)
ctx.obj["config"] = config
@cli.command()
@click.pass_context
def status(ctx):
"""Show current status."""
click.echo(f"Using config: {ctx.obj['config']}")ctx.ensure_object(dict) initialises ctx.obj to an empty dict if it is not
already set. Subcommands that decorate with @click.pass_context receive the
same context object, and therefore the same ctx.obj dict, without any global
state.
Nesting groups
A group can contain other groups, creating deeper hierarchies:
mytool
db
migrate
rollback
status
serve
config
show
set@click.group()
def db():
"""Database commands."""
pass
@db.command()
def migrate():
click.echo("Running migrations...")
cli.add_command(db) # attach the db group to the top-level groupNesting beyond two levels tends to confuse users. If you find yourself with
mytool a b c d, consider whether the inner levels could be flags instead.
The design principle
Groups are namespaces for related commands. Use them to group commands that
operate on the same resource or concept: db, config, cache. Avoid groups
that are just catch-alls. If all your subcommands belong to one group, you
probably do not need the group — a flat command list is simpler.
Click's invoke_without_command=True on a group makes it behave like a
command when called with no subcommand — useful if you want mytool alone
to do something (show a dashboard, print a status summary) rather than just
printing help.
Where to go next
Next: groups in practice — building a two-subcommand tool with shared context and a nested subgroup in a runnable demo.