Back to Blog
Python

Python Click Groups, Subcommands, and Prompts

python click groups subcommands and prompts: Learn how to structure Click CLIs with groups, subcommands, and interactive prompts, including context sharing and validat...

ClickCLIPythonSubcommandsPrompts
Diagram showing a Click command group with subcommands and a prompt input field

When building a command-line tool with Click, the combination of groups, subcommands, and prompts covers most interactive workflows. This article explains how to structure python click groups subcommands and prompts effectively, including context sharing and validation.

Building a Group with Subcommands

A Click group is a command that can contain other commands. The simplest structure uses @click.group() to create a root, then decorates each subcommand with @click.command() and registers it with the group using @group.command(). Here is a minimal example:

import click @click.group() def cli(): """A simple CLI with subcommands.""" pass @cli.command() def init(): """Initialize the project.""" click.echo("Initialized") @cli.command() def build(): """Build the project.""" click.echo("Built") if __name__ == "__main__": cli()

When you run cli.py init, Click dispatches to the init function. The group itself can also accept options or arguments, which are passed to the group function and become available to subcommands via context.

Sharing State with pass_context

Subcommands often need configuration or shared state from the group. Click provides @click.pass_context to inject the Context object into a command function. The context holds the group's parameters and can be used to pass data down.

import click @click.group() @click.option("--verbose", is_flag=True) @click.pass_context def cli(ctx, verbose): """A CLI with shared verbosity.""" ctx.ensure_object(dict) ctx.obj["verbose"] = verbose @cli.command() @click.pass_context def status(ctx): """Show status.""" if ctx.obj.get("verbose"): click.echo("Verbose output enabled") click.echo("Status: OK")

Here, ctx.ensure_object(dict) initializes a dictionary to store shared data. Subcommands access it via ctx.obj. This pattern keeps the group and subcommands decoupled while allowing consistent configuration.

Adding Prompts to Subcommands

Prompts are useful when a subcommand needs input that is not suitable as a command-line argument, such as a secret or a choice that depends on runtime state. Use prompt=True in an option to ask interactively when the value is not provided.

@cli.command() @click.option("--name", prompt="Your name", help="Your name.") def greet(name): """Greet the user.""" click.echo(f"Hello, {name}!")

If the user runs cli.py greet without --name, Click prompts for it. The prompt text can be customized, and hide_input=True turns it into a password prompt. For confirmation, use confirmation_prompt=True to ask twice.

@cli.command() @click.option("--password", prompt=True, hide_input=True, confirmation_prompt=True) def login(password): """Log in with a password.""" click.echo("Password accepted")

Prompts integrate with Click's type system. You can specify a type such as click.IntRange or click.Choice to validate input immediately.

Validating Prompt Input

Click validates prompt input based on the option's type. For custom validation, use a callback or type=click.types.FuncParamType. A common pattern is to combine a prompt with a type that enforces a pattern.

def validate_email(ctx, param, value): if "@" not in value: raise click.BadParameter("Email must contain @") return value @cli.command() @click.option("--email", prompt=True, callback=validate_email) def subscribe(email): """Subscribe with an email.""" click.echo(f"Subscribed {email}")

The callback runs after the prompt, and raising BadParameter causes Click to re-prompt. This keeps validation logic in one place and prevents the same checks from being duplicated across commands.

Nested Groups and Command Chaining

Groups can be nested to create a hierarchy of subcommands. This is useful for tools with multiple domains, such as git remote add. Each nested group is itself a command that can have its own options and subcommands.

@click.group() def remote(): """Manage remotes.""" pass @remote.command() @click.argument("name") @click.argument("url") def add(name, url): """Add a remote.""" click.echo(f"Added remote {name}: {url}") cli.add_command(remote)

When nesting, remember that pass_context works across all levels. The context object is shared, so you can store data at the root and access it in deeply nested subcommands. However, avoid over-nesting; a flat structure is often easier to maintain.

Choosing Between Prompts and Arguments

Arguments are positional and required by default; prompts are interactive and optional unless forced. The decision depends on how the command is used. If a value is always needed and can be provided in a script, use an argument. If the value is sensitive or rarely provided, a prompt is better.

Use caseArgumentPrompt
Scriptable and requiredYesNo
Interactive and optionalNoYes
Secret or sensitiveNoYes
Multiple valuesYesNo

Prompts can also be used with default to make them non-blocking when a default exists. For example, prompt=True, default="world" will show a prompt with the default and accept an empty input.

Keeping the CLI Maintainable

As the number of subcommands grows, consistent naming and structure become critical. Use clear group names and avoid deep nesting unless the domain requires it. Group common options in a decorator factory or use @click.group with invoke_without_command to run setup code only when a subcommand is invoked.

def common_options(func): func = click.option("--verbose", is_flag=True)(func) func = click.option("--config", type=click.Path(exists=True))(func) return func @cli.command() @common_options def deploy(verbose, config): """Deploy the app.""" click.echo(f"Deploying with config {config}")

This avoids repeating options across commands. For prompts, keep the input logic inside the command function rather than in a shared helper, so each command's behavior remains explicit. When a prompt becomes complex, consider moving it to a separate function that returns the value, but keep the validation close to the prompt.

Finally, test commands with Click's CliRunner to ensure prompts and subcommands behave as expected. The runner can simulate input and capture output, which is essential for verifying interactive behavior in a CI pipeline.

python click groups subcommands and prompts: Practical Usage | RYUSLOG DEV