Exceptions, exit codes and standalone mode

Click · Expert · 11 min

Scripts and CI rely on exit codes, and users rely on clean error messages instead of tracebacks.

In standalone mode (the default when you call cli()), Command.main wraps everything:

  • click.ClickException and subclasses are printed nicely (Error: ...) and exit with their exit_code: 1 for ClickException, 2 for UsageError and BadParameter (which also print the usage line and a "Try --help" hint).
  • click.Abort (raised on Ctrl+C at a prompt or when a confirmation is declined) prints Aborted! and exits 1.
  • click.exceptions.Exit(code) / ctx.exit(code) exits with that code silently.
  • KeyboardInterrupt becomes Abort, and EOFError at a prompt too.
  • A broken pipe on stdout (mytool | head) is handled quietly.
  • Any other exception propagates with a normal traceback.

So raise click.ClickException("config file is invalid") for expected errors, and subclass it to set a custom exit_code (for example 3 for "not found") or to override show().

With standalone_mode=False, Click does not print or exit: main() returns the command's return value, and exceptions propagate (except Exit, whose code is returned). This is how you embed a Click command in another program or test the raw behaviour. UsageError still carries .ctx and format_message() for your own reporting.

Command callbacks' return values are ignored in standalone mode unless you use result_callback on groups, which receives the subcommands' return values, useful for pipelines with chain=True.

Example

import click

class NotFound(click.ClickException):
    exit_code = 3

@click.command()
@click.argument("key")
def get(key):
    data = {"a": 1}
    if key not in data:
        raise NotFound(f"no such key: {key}")
    return data[key]

for args in (["a"], ["zzz"], []):
    try:
        get.main(args, prog_name="get")            # standalone mode
    except SystemExit as e:
        print("exit code", e.code)
# exit code 0
# Error: no such key: zzz                          (stderr)
# exit code 3
# Usage: get [OPTIONS] KEY ... Error: Missing argument 'KEY'.   (stderr)
# exit code 2

print(get.main(["a"], standalone_mode=False))     # 1  (return value, no exit)

Remember

  • ClickException exits 1, UsageError/BadParameter exit 2, Abort prints 'Aborted!'
  • Subclass ClickException to set custom exit codes
  • standalone_mode=False returns values and propagates exceptions

Watch out for

  • Calling sys.exit or printing tracebacks for expected user errors instead of raising ClickException.

Check yourself

  1. What exit code does a missing required argument produce in standalone mode?

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

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