Building CLIs with Click
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
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.
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.
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.
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')
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]...
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.
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