Code of the Day
IntermediateClick and Subcommands

Subcommands with groups

Build git-style multi-subcommand CLIs with @click.group() and pass shared state cleanly through Click's context object.

UtilitiesIntermediate6 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 group

Nesting 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.

Finished reading? Mark it complete to track your progress.

On this page