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
- /guides/argparse-guide/ — the standard library alternative for simpler CLIs
- /guides/env-variables/ — handling configuration via environment variables in CLIs
- /guides/subprocess-guide/ — running external commands from Python