cheat sheet

click

Package-level reference for the click CLI toolkit on PyPI — install, version policy, extras, and alternatives.

#pip#package#cli#terminalupdated 05-31-2026

click

What it is

click is a Python package for building composable command-line interfaces using decorators. Originally extracted from the Flask ecosystem by Armin Ronacher and now maintained by the Pallets project, it has become the dominant decorator-based CLI library on PyPI.

Reach for click when you want fine-grained control over command groups, parameter parsing, and prompt UX. Reach for typer instead when you would rather declare arguments via type hints and let the framework generate the click layer for you.

Install

bash
pip install click

Output: (none — exits 0 on success)

bash
uv add click

Output: dependency resolved + added to pyproject.toml

bash
poetry add click

Output: updated lockfile + virtualenv install

Versioning & Python support

  • Current stable line is the 8.x series. 8.0 (2021) was a major rewrite of the parser internals; 8.1 and 8.2 are additive.
  • Supports Python 3.7+ on the 8.x line. The 7.x series held Python 2.7 compat and is end-of-life.
  • Strict semver in practice — minor releases are additive, majors are breaking. Pin upper-bounds (click>=8,<9) if you depend on internal helpers.
  • click.testing.CliRunner API has been stable since 7.x, so test suites usually survive minor upgrades.

Package metadata

  • Maintainer: Pallets (pallets org on GitHub)
  • Project home: github.com/pallets/click
  • Docs: click.palletsprojects.com
  • PyPI: pypi.org/project/click
  • License: BSD-3-Clause
  • Governance: Pallets Project (community foundation behind Flask, Jinja, Werkzeug)
  • First released: 2014
  • Downloads: consistently in the top-20 PyPI packages by monthly downloads — pulled in transitively by flask, black, pip-tools, dagster, prefect, and most modern CLI tools.

Optional dependencies & extras

click is a single package with zero required runtime dependencies beyond the standard library. There are no published extras (pip install "click[…]" is not supported).

Two adjacent Pallets packages are commonly paired with click but installed separately:

  • click-completion (third-party) — extra shell-completion backends. Largely obsolete now that click 8 ships native completion for bash, zsh, and fish.
  • click-plugins — entry-point-driven plugin discovery for CLI sub-commands. Useful for tools that load commands from other installed packages.
  • colorama — auto-imported on Windows for ANSI color support; declared as an environment-conditional dependency, not an extra.

Alternatives

PackageTrade-off
typerDecorators replaced by Python type hints. Generates click internally, so behaviour is identical at the CLI level. Use when type hints feel more natural than decorators.
argparse (stdlib)Zero-dependency, in the stdlib. Use for very small scripts or when you cannot add a third-party package. Much more boilerplate for sub-commands.
docoptParse the help string itself to generate the CLI. Elegant but unmaintained for years.
fireAuto-generate CLIs from any Python object via reflection. Good for ad-hoc exposure of existing code; weaker for polished UX.
cleoSymfony-style commands and prompts; used by Poetry. Use when you want richer formatters than click ships.

Common gotchas

  1. Decorator order matters. Put @click.command() or @click.group() first (outermost), then @click.option(...) / @click.argument(...) below it. Reverse order works but produces confusing error messages about missing parameters.
  2. @click.command() vs @click.group(). A command is a leaf; a group is a parent that holds sub-commands via @group.command() or @group.add_command(). You cannot attach sub-commands to a plain command — it will run as a leaf and ignore the children.
  3. Options vs arguments semantics. @click.option("--name") is keyword-style (--name alice); @click.argument("name") is positional (alice). Arguments have no --help description; document them in the docstring instead.
  4. ctx.obj for shared state, not module globals. Pass shared config between a group and its commands via @click.pass_context and ctx.obj, not module-level variables. Otherwise tests with CliRunner cross-contaminate.
  5. standalone_mode=False for embedding. When invoking commands from inside another Python program, set standalone_mode=False on command.main() or runner.invoke() — otherwise click calls sys.exit() and aborts your host process.
  6. UTF-8 locale required warnings on macOS / Linux. Surface when LANG/LC_ALL are unset. Set LANG=C.UTF-8 (or en_US.UTF-8) in the shell that runs the CLI, or call click.disable_unicode_literals_warning = True (deprecated workaround).
  7. CliRunner does not capture writes outside click's echo. Tests asserting on result.output will miss anything printed via plain print() or written directly to sys.stdout. Use click.echo() exclusively inside commands to keep test output predictable.

Real-world recipes

Worked patterns that come up repeatedly in production click code. Each one is the smallest skeleton that captures the technique — drop into a real app and grow from there.

Subcommands with a shared --verbose and context object

A canonical "tool with several subcommands" layout. The group parses shared flags once, stores them on ctx.obj, and every leaf command receives the context. This is how git, kubectl, and most polished CLIs are built.

python
import click
import logging

@click.group()
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v, -vv).")
@click.option("--config", type=click.Path(), default="config.toml", show_default=True)
@click.pass_context
def cli(ctx, verbose, config):
    """myapp — Alice's deployment tool."""
    ctx.ensure_object(dict)
    level = logging.WARNING - 10 * verbose
    logging.basicConfig(level=max(level, logging.DEBUG))
    ctx.obj.update(verbose=verbose, config=config)

@cli.command()
@click.argument("target")
@click.pass_obj
def deploy(obj, target):
    """Deploy to TARGET."""
    click.echo(f"[{obj['config']}] deploy -> {target}")

@cli.command()
@click.argument("name")
@click.pass_obj
def rollback(obj, name):
    """Roll back deployment NAME."""
    click.echo(f"[{obj['config']}] rollback -> {name}")

Output: myapp -v deploy prod and myapp rollback rev-42 share verbose/config wiring without each subcommand re-declaring it.

Custom ParamType for domain validation

When the built-in click.Choice / click.IntRange aren't enough — for example, validating a custom URL shape — subclass click.ParamType. Click calls convert() for every parsed value and fail() produces an idiomatic error.

python
import click
import re

class SemverType(click.ParamType):
    name = "semver"
    _re = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")

    def convert(self, value, param, ctx):
        if self._re.match(value):
            return value
        self.fail(f"{value!r} is not a valid semver string.", param, ctx)

SEMVER = SemverType()

@click.command()
@click.argument("version", type=SEMVER)
def tag(version):
    """Tag the current commit with VERSION."""
    click.echo(f"tagged {version}")

Output: tag 1.2.3 succeeds; tag v1.2 errors with Invalid value for 'VERSION': 'v1.2' is not a valid semver string.

Plugin-style command loading via click-plugins

For apps that should accept third-party subcommands installed from PyPI, use click-plugins to discover commands registered under an entry-point group. The host CLI never imports plugin code directly — it picks up whatever is installed.

python
# host_app/cli.py
import click
from click_plugins import with_plugins
from importlib.metadata import entry_points

@with_plugins(entry_points(group="myapp.commands"))
@click.group()
def cli():
    """Host CLI — discovers plugin subcommands at runtime."""
toml
# plugin_pkg/pyproject.toml — declares a subcommand
[project.entry-points."myapp.commands"]
hello = "plugin_pkg.cli:hello"

Output: after pip install plugin_pkg, myapp --help lists hello automatically; the host doesn't change. This is how pip, awscli, and dagster extend themselves.

Streaming progress with click.progressbar

The canonical pattern for showing a progress UI on a long iteration. The bar wraps any iterable, handles ANSI fallback when redirected, and integrates with CliRunner (the bar collapses to nothing under tests, so assertions on stdout aren't polluted).

python
import click
import time

@click.command()
@click.argument("count", type=int, default=20)
def crunch(count):
    items = range(count)
    with click.progressbar(items, label="Crunching") as bar:
        for _ in bar:
            time.sleep(0.05)

Output: Crunching [####################################] 100% — drawn to stderr by default so it doesn't pollute stdout pipes.

CLI distribution patterns

A click CLI inside a repo is not the same as a CLI that ships to users. Wiring [project.scripts], completion scripts, and a release path lifts a personal helper into a real tool installable with pipx or pip install.

[project.scripts] entry point

Modern Python packaging exposes a console script via pyproject.toml. The mapping is script-name = "module.path:callable"; pip wires it into PATH on install. Hatchling, setuptools, and uv all consume the same key.

toml
# pyproject.toml
[project]
name = "alice-deploy"
version = "0.3.0"
dependencies = ["click>=8,<9"]
requires-python = ">=3.10"

[project.scripts]
alice-deploy = "alice_deploy.cli:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

Output: pip install . adds alice-deploy to the active venv's bin/; pipx install . installs it into a dedicated venv usable from any shell.

Shell completion (bash, zsh, fish)

Click 8 ships native completion for bash, zsh, and fish via an environment-variable trigger. The pattern is to eval the output of the CLI invoked with _<APPNAME>_COMPLETE=<shell>_source; users add the snippet to their rc file.

bash
# bash — append to ~/.bashrc
eval "$(_ALICE_DEPLOY_COMPLETE=bash_source alice-deploy)"

# zsh — append to ~/.zshrc
eval "$(_ALICE_DEPLOY_COMPLETE=zsh_source alice-deploy)"

# fish — write a file in ~/.config/fish/completions/
_ALICE_DEPLOY_COMPLETE=fish_source alice-deploy > ~/.config/fish/completions/alice-deploy.fish

Output: TAB after alice-deploy lists subcommands; TAB after --config triggers path completion.

The CLI itself doesn't change — Click looks at the magic env var, prints completion candidates, and exits. Document the snippet in your README so users can opt in.

Single-file CLIs with shiv, PEX, or pyoxidizer

For distribution to users who don't have a Python toolchain, bundle the CLI into a single executable. The three relevant tools have different trade-offs.

ToolOutputRuntime requirementNote
shivA zipapp (.pyz)Python 3 on PATHSmallest, fastest build; reads [project.scripts].
PEXA .pex zipappPython 3 on PATHMore flexible (multi-platform, hermetic interpreters).
pyoxidizerA native binaryNone — embedded interpreterLargest binary; full Python embedded; complex build.
bash
pip install shiv
shiv -e alice_deploy.cli:cli -o alice-deploy .
./alice-deploy --help

Output: alice-deploy is a self-contained ~5 MB zipapp; copy onto any host with Python 3 and run.

pipx-friendly metadata

pipx installs CLIs into isolated venvs. The only requirement is a [project.scripts] entry point — but a few metadata refinements make pipx happier:

  • Declare requires-python so pipx skips on incompatible interpreters.
  • Avoid declaring click with an upper bound that conflicts with another CLI the user has installed.
  • Put any non-pure-Python deps behind extras so the lean install path stays fast.
  • Add [project.optional-dependencies] completion = ["click-completion"] only if you need legacy bash 3 support; modern click ships native completion.

Version migration guide

Click's major-version churn is rare but sharp. The 7.x → 8.x jump was the most significant in years — it dropped Python 2 support, changed the parser internals, and reworked CliRunner. Most leaf-application code survives unchanged; plugins and extensions need attention.

click 7.x8.x:

python
# Click 7 — deprecated patterns
import click

@click.command()
@click.option("--foo")
def cmd(foo):
    click.echo(foo)

if __name__ == "__main__":
    cmd(prog_name="cmd")  # prog_name positional was tolerated
python
# Click 8 — explicit, type-safe
import click

@click.command()
@click.option("--foo")
def cmd(foo: str | None) -> None:
    click.echo(foo)

if __name__ == "__main__":
    cmd(prog_name="cmd")  # still works; positional usage is unchanged here

Notable break points to audit:

  • autocomplete callbacks renamed to shell_complete. The signature changed — old autocomplete=fn parameters silently no-op on 8.x.
  • CliRunner.invoke(..., mix_stderr=False) is the new way to separate stdout/stderr in tests. The default still mixes them.
  • click.get_terminal_size() removed — use shutil.get_terminal_size() from the stdlib.
  • click.exceptions.Exit is the canonical way to exit with a code from inside a callback; raising bare SystemExit bypasses Click's exception hooks.
  • Native bash/zsh/fish completion replaced the third-party click-completion package for most use cases. If you used click-completion on 7.x, the migration is to remove it and document the new _<APPNAME>_COMPLETE env-var pattern instead.
  • Python 2 helpers removed. click.disable_unicode_literals_warning, click._unicodefun, and adjacent internals were dropped — vendored fixes in old plugins need to go.

For plugin authors: re-test under click 8.2 against python -W error::DeprecationWarning; warnings flagged today are likely removals in 9.x.

Testing strategies

Click ships click.testing.CliRunner precisely so CLIs can be tested without forking subprocesses. The runner invokes the command in-process, captures stdout/stderr, and produces a Result object — orders of magnitude faster than spawning python -m mytool in pytest.

python
from click.testing import CliRunner
from alice_deploy.cli import cli

def test_deploy_happy_path():
    runner = CliRunner()
    result = runner.invoke(cli, ["deploy", "prod"])
    assert result.exit_code == 0
    assert "deploy -> prod" in result.output

def test_missing_argument_errors_cleanly():
    runner = CliRunner()
    result = runner.invoke(cli, ["deploy"])
    assert result.exit_code == 2  # Click's "usage error" exit code
    assert "Missing argument" in result.output

def test_env_var_fallback():
    runner = CliRunner()
    result = runner.invoke(cli, ["deploy", "prod"],
                            env={"MYAPP_CONFIG": "/etc/prod.toml"})
    assert "/etc/prod.toml" in result.output

def test_isolated_filesystem_for_writes():
    runner = CliRunner()
    with runner.isolated_filesystem():
        from pathlib import Path
        Path("notes.txt").write_text("seed")
        result = runner.invoke(cli, ["scan", "notes.txt"])
    assert result.exit_code == 0

Output: pytest -q runs all four in under 100 ms — no subprocess, no astro check-style cold start cost.

Key patterns:

  • mix_stderr=False to separate stdout (machine output) from stderr (logs). Assert on each independently.
  • isolated_filesystem() wraps the test in a temp dir + chdir. Anything written during the test is auto-cleaned, and other tests can't see it.
  • input="…" feeds stdin — useful for click.prompt() flows: runner.invoke(cli, ["init"], input="alicedev\nyes\n").
  • catch_exceptions=False re-raises exceptions instead of swallowing them into result.exception. Useful when debugging a flaky test.

Troubleshooting common errors

Real error messages and the fix that resolves them.

SymptomCauseFix
Got unexpected extra argumentCalling a command (leaf) like a group — extra args land after the parser exhausted parameters.Convert the function to a @click.group() and attach subcommands, or accept the trailing args with @click.argument("rest", nargs=-1).
Missing argument 'NAME' even when value suppliedArgument decorators applied in reverse order; the @click.command() decorator must be outermost (top-most).Move @click.command() to the line directly above def fn(...).
Error: Invalid value for '--PATH': Path 'X' does not exist.type=click.Path(exists=True) validates eagerly; symlinks pointing nowhere also fail.Either accept non-existent paths (exists=False) or run mkdir -p in a setup step.
RuntimeError: There is no active click contextCalling click.get_current_context() outside of a command's execution (e.g. at module import time).Move the call inside the function body, or use @click.pass_context for explicit threading.
Test asserts on result.output but sees empty stringCommand used print() instead of click.echo(); CliRunner only captures click.echo() writes.Replace all print() with click.echo().
CLI exits silently when called from another Python processcommand.main() calls sys.exit() by default.Pass standalone_mode=False: cli.main(args, standalone_mode=False).
UnicodeEncodeError on Windows when echoing emojiLC_ALL / PYTHONIOENCODING not set; default codepage can't encode the glyph.Set PYTHONIOENCODING=utf-8, or call click.echo(s, err=False) after sys.stdout.reconfigure(encoding='utf-8').
click.exceptions.UsageError: No such option: --foo even though declaredTwo @click.command() decorators on adjacent functions, with options between them — Click attaches options to the first command it finds above.Each command + its options must be a contiguous decorator stack.

See also