pyguides

Building Command-Line Interfaces with Click in Python

Click is a Python library for building command-line interfaces. It handles the boring parts — option parsing, help generation, error handling — so you can focus on what your tool actually does. You write decorators, Click handles the wiring.

Installation

pip install click

Click requires Python 3.7 or later.

Your First Command

import click

@click.command()
@click.option('--count', default=1, help='How many times to greet')
@click.option('--name', default='World', help='Who to greet')
def hello(count, name):
    """Print a greeting COUNT times."""
    for _ in range(count):
        click.echo(f'Hello, {name}!')

if __name__ == '__main__':
    hello()

Run it:

$ python hello.py --count=3 --name Alice
Hello, Alice!
Hello, Alice!
Hello, Alice!

The --help flag works automatically:

$ python hello.py --help
Usage: hello.py [OPTIONS]

  Print a greeting COUNT times.

Options:
  --count INTEGER  How many times to greet
  --name TEXT      Who to greet
  --help           Show this message and exit

Click generates help output from the decorator metadata you already wrote: the function docstring becomes the command description, option names and help strings populate the options table, and default values are displayed next to each parameter. You never need to write an argparse-style formatter or maintain a separate help string. The help screen also respects terminal width automatically, wrapping long option descriptions to fit the user’s console instead of producing ragged output at an arbitrary column count.

Options vs Arguments

Options start with -- (or - for short form) and are optional by default. Arguments are positional and required by default.

@click.command()
@click.option('--verbose', '-v', is_flag=True)       # --verbose or -v
@click.argument('filename')                          # positional, required
def process(verbose, filename):
    if verbose:
        click.echo(f'Processing {filename}')

Arguments are for things the user must provide. Options are for modifying behavior. Click enforces the argument count by default; if the user omits a required argument, Click prints a targeted error message that names the missing argument rather than a generic “too few arguments” notice. This distinction between options and arguments mirrors how most Unix tools work: a filename is an argument because the tool cannot function without it, while a verbose flag is an option because it changes behavior but is not required for the tool to do its job.

Common Option Patterns

Boolean Flags

@click.option('--verbose', '-v', is_flag=True)
@click.option('--quiet', '-q', is_flag=True)

When present on the command line, the value is True. Otherwise it’s False. You can also invert the sense of a flag by setting is_flag=True, flag_value='disabled' and providing a separate --enable counterpart, which is helpful for toggles that default to on but should be switched off for a specific invocation. Flags do not consume a value from the argument list, so --verbose does not expect a following token; just placing the flag on the command line is enough.

Options with Defaults

@click.option('--port', default=8080, type=int)
@click.option('--host', default='localhost')

The user can override: --port 9000.

Choice Constraints

@click.option('--format', type=click.Choice(['json', 'yaml', 'csv']))

Click validates and shows valid choices in the error message. If a user types an unsupported value, Click prints the list of accepted options directly in the error output, which saves the user from having to run the tool again with —help. For large choice sets, you can provide the choices dynamically at runtime by passing a callable that returns a list rather than a static list, which lets you populate the valid options from a configuration file or API response.

Counting (Multiple Flags)

@click.option('-v', '--verbose', count=True)

Each -v on the command line increments the count:

$ python app.py -vvv
# verbose = 3

Password Prompt

@click.option('--password', prompt=True, hide_input=True)

Click prompts the user interactively if --password isn’t provided on the command line.

Arguments

Arguments are positional and generally required:

@click.command()
@click.argument('input_file')
@click.argument('output_file')
def convert(input_file, output_file):
    """Convert INPUT_FILE to OUTPUT_FILE."""
    click.echo(f'Converting {input_file} to {output_file}')
$ python convert.py data.csv result.json
# input_file = 'data.csv'
# output_file = 'result.json'

Use nargs=N for a fixed number of arguments, or nargs=-1 for variadic:

@click.argument('files', nargs=-1)
def process_multiple(files):
    # files is a tuple
    for f in files:
        process(f)
$ python script.py file1.txt file2.txt file3.txt
# files = ('file1.txt', 'file2.txt', 'file3.txt')

Variadic arguments with nargs=-1 collect every remaining positional token into a tuple, which is the same pattern that many Unix tools use for accepting a list of filenames at the end of a command line. If you need to combine variadic arguments with options, place the variadic argument last in the decorator chain so Click knows where argument parsing stops and option parsing can resume. You can also set a required minimum number of values by passing a positive integer to nargs, which causes Click to reject invocations that provide too few arguments with a clear error message.

Validation

Click doesn’t have built-in validation, but you can add it in the function body:

@click.command()
@click.option('--age', type=int)
def check_age(age):
    if age is not None and age < 0:
        raise click.ClickException('Age cannot be negative')
    click.echo(f'Age: {age}')

For argument validation, use click.ParamType:

class AgeType(click.ParamType):
    name = 'AGE'

    def convert(self, value, param, ctx):
        if not isinstance(value, str) or not value.isdigit():
            self.fail(f'{value!r} is not a valid age', param, ctx)
        age = int(value)
        if age > 150:
            self.fail(f'{age} is unreasonably old', param, ctx)
        return age

@click.command()
@click.argument('age', type=AgeType())
def person(age):
    click.echo(f'Age: {age}')

Prompts for Interactive Input

@click.command()
@click.option('--name', prompt=True)
@click.option('--confirm', prompt=True, confirmation_prompt=True)
def setup(name, confirm):
    click.echo(f'Name: {name}, Confirmed: {confirm}')

If --name isn’t provided, Click pauses and asks. The confirmation_prompt=True asks twice and checks they match.

Multi-Command CLIs

Click supports nested command groups. This is useful for tools like git:

@click.group()
def cli():
    """Main entry point."""
    pass

@cli.command()
@click.option('--verbose', '-v', is_flag=True)
def build(verbose):
    """Build the project."""
    click.echo('Building...' if verbose else 'Done')

@cli.command()
def test():
    """Run tests."""
    click.echo('Running tests...')

if __name__ == '__main__':
    cli()
$ python cli.py build --verbose
Building...
$ python cli.py test
Running tests...
$ python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

Each subcommand decorated with @cli.command() becomes a distinct entry in the help output, and Click automatically inserts a Commands section listing every available subcommand along with the first line of its docstring. This same pattern scales to deeper nesting: you can create a subgroup within a group by decorating another @click.group() function with @cli.group(), which produces a level of command hierarchy that mirrors the organization of complex tools. The help system adjusts accordingly, showing subgroup commands when the user asks for help on the parent group.

Using add_command

You can also build groups programmatically:

@click.command('deploy')
def deploy():
    click.echo('Deploying...')

@click.command('rollback')
def rollback():
    click.echo('Rolling back...')

cli = click.Group()
cli.add_command(deploy)
cli.add_command(rollback)

Command Chains

By default, a Click group executes exactly one subcommand. Use invoke_without_command=True to allow the group itself to run:

@click.group(invoke_without_command=True)
@click.option('--version', is_flag=True)
def cli(version):
    if version:
        click.echo('mytool v1.0.0')
    elif click.invoked_subcommand is None:
        click.echo('Run --help for usage')

@cli.command()
def start():
    click.echo('Starting...')

Reading Files

Click handles file paths elegantly with click.File:

@click.command()
@click.argument('input', type=click.File('r'))
@click.argument('output', type=click.File('w'))
def transform(input, output):
    """Transform INPUT to OUTPUT."""
    content = input.read()
    output.write(content.upper())
$ python transform.py input.txt output.txt
# Or with stdin/stdout:
$ cat input.txt | python transform.py - output.txt

Use 'rb'/'wb' for binary mode. Click also handles Path-like objects automatically. The special filename - maps to stdin or stdout depending on the file mode, which means your tool automatically supports Unix pipeline workflows without any extra code. Click opens and closes the file handles for you, including proper cleanup when errors occur during processing, so you do not need to manage file lifecycle manually in a try-finally block. For use cases where you need the filename string rather than an open file handle, use click.Path instead; it validates that the path exists and is readable or writable without actually opening the file.

Styling Output

@click.command()
def status():
    click.secho('Success!', fg='green')
    click.secho('Warning:', fg='yellow', err=True)
    click.echo(click.style('Bold text', bold=True))

Colors work in terminals that support them. The err=True option sends output to stderr.

Progress Bars

import time

@click.command()
def download():
    with click.progressbar(length=100, label='Downloading') as bar:
        for i in range(100):
            time.sleep(0.05)
            bar.update(1)

Testing Click Commands

Click ships with a testing utilities module:

from click.testing import CliRunner

def test_hello():
    runner = CliRunner()
    result = runner.invoke(hello, ['--name', 'Alice', '--count', '2'])
    assert result.exit_code == 0
    assert 'Hello, Alice!' in result.output

runner.invoke() runs the command and returns a Result object with output, exit_code, and exception. The runner can also simulate file inputs:

def test_transform():
    runner = CliRunner()
    with runner.isolated_filesystem():
        with open('input.txt', 'w') as f:
            f.write('hello')
        result = runner.invoke(transform, ['input.txt', 'output.txt'])
        assert result.exit_code == 0
        assert open('output.txt').read() == 'HELLO'

Common Pitfalls

Options Are Keyword-Only

Click options become function parameters. Don’t use positional args in your function:

# Wrong
@click.command()
@click.option('--name')
def hello(name, extra):  # extra isn't a parameter
    pass

# Right
@click.command()
@click.option('--name')
def hello(name):
    pass

@click.group() vs @click.command()

Decorators chain. A group uses @click.group() then @group.command() for subcommands. A standalone command uses @click.command(). Mixing them up causes cryptic errors.

Mixing Options and Arguments

Put all @click.argument decorators before @click.option decorators in your function. Click reorders them for parsing, but the decorator order in the function signature matters for readability:

# Arguments first, then options
@click.argument('input')
@click.option('--verbose')
def process(input, verbose):
    pass

Exit Codes on Exceptions

Click catches exceptions and shows error messages, but the exit code is 1 by default. To control exit codes, use click.Abort() or raise SystemExit with a specific code:

@click.command()
def fail():
    click.secho('Fatal error', fg='red', err=True)
    raise SystemExit(1)

See Also