The context and sharing state

Click · Practical · 11 min

Subcommands usually need shared setup like config, clients or a database handle, without global variables.

Each invocation creates a context (click.Context) holding the command, the parsed parameters, the parent context (for subcommands) and a free slot, ctx.obj, for your own data. Contexts form a chain from the root group down to the running command.

Get the current context with @click.pass_context, which passes ctx as the first argument. The typical pattern: the group builds shared state and stores it in ctx.obj; subcommands read it.

@click.group()
@click.pass_context
def cli(ctx):
    ctx.obj = Config(...)

Subcommands then use @click.pass_obj to receive ctx.obj directly, or @click.make_pass_decorator(Config) to find the nearest object of that type in the chain.

Other context features:

  • ctx.ensure_object(dict) creates ctx.obj if missing, convenient when the root may be called from tests with or without an object.
  • ctx.invoke(other_command, x=1) and ctx.forward(other_command) call another command's callback.
  • ctx.call_on_close(fn) and ctx.with_resource(cm) register cleanup that runs when the context ends: close a database connection opened in the group callback, for example.
  • ctx.exit(code) and ctx.abort() end the program; ctx.fail("msg") reports a usage error.
  • ctx.meta is a dict shared across the whole chain, for extensions.
  • click.get_current_context() retrieves the context from anywhere during execution.

obj= can also be passed to main() or CliRunner.invoke(), which is handy for injecting fakes in tests.

Example

import click

class Config:
    def __init__(self, env):
        self.env = env
        self.log = []

@click.group()
@click.option("--env", default="dev")
@click.pass_context
def cli(ctx, env):
    ctx.obj = Config(env)
    ctx.call_on_close(lambda: click.echo(f"closing ({len(ctx.obj.log)} actions)"))

@cli.command()
@click.argument("service")
@click.pass_obj
def deploy(cfg, service):
    cfg.log.append(service)
    click.echo(f"Deploying {service} to {cfg.env}")

cli.main(["--env", "prod", "deploy", "api"], standalone_mode=False)
# Deploying api to prod
# closing (1 actions)

Remember

  • ctx.obj carries shared state from groups to subcommands
  • @pass_context gives the context; @pass_obj gives ctx.obj
  • call_on_close and with_resource clean up when the command ends

Watch out for

  • Using module-level globals for configuration, which leaks state between test runs.

Check yourself

  1. Which decorator passes ctx.obj directly as the first argument of a subcommand?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: click.palletsprojects.com/en/stable/complex/