ZetCode

Python click

last modified October 30, 2026 Python 3.15

In this article we show how to create command line interfaces with the click module.

Python click

The click module is used to create command line (CLI) applications. It is an easy-to-use alternative to the standard argparse module. It allows arbitrary nesting of commands, generates help pages automatically, validates and converts input values, and supports prompts, environment variables, and colours. It was created for the Flask web framework.

Commands are the basic building blocks of an application. They are defined with the click.command decorator. Values are passed to commands via options and arguments. Options are added with the click.option decorator and arguments with click.argument. Options are named and usually optional; arguments are positional.

The module is installed with the following command. The examples use click 8.5.

$ pip install click

Simple example

The following is a trivial CLI example.

simple.py
import click

@click.command()
def hello():
    click.echo('Hello there')

if __name__ == '__main__':
    hello()

The example creates a command that outputs a message.

@click.command()
def hello():
    click.echo('Hello there')

The decorator turns the function into a command. Click uses echo instead of print; it handles bytes and strings consistently and removes colour codes when the output is not a terminal.

Click adds a --help option automatically and rejects unknown options with an error message and exit code 2.

$ python simple.py
Hello there
$ python simple.py --help
Usage: simple.py [OPTIONS]

Options:
  --help  Show this message and exit.
$ python simple.py --verbose
Usage: simple.py [OPTIONS]
Try 'simple.py --help' for help.

Error: No such option '--verbose'.

Arguments

Arguments are added with the click.argument decorator. They are passed to the function as parameters of the same name and may have default values.

argument.py
import click

@click.command()
@click.argument('name', default='guest')
def hello(name):
    click.echo(f'Hello {name}')

if __name__ == '__main__':
    hello()

The example builds a message with the given argument value. If there is no argument, the default guest is used.

$ python argument.py Peter
Hello Peter
$ python argument.py
Hello guest

Argument types

The type parameter sets the type of a value. Click converts the value and reports an error if the conversion fails. The default type is str.

argument_type.py
import click

@click.command()
@click.argument('name')
@click.argument('age', type=int)
def hello(name, age):
    click.echo(f'{name} is {age} years old')

if __name__ == '__main__':
    hello()

In the example, we have two arguments: name and age. They are taken positionally.

@click.argument('age', type=int)

The age argument is converted to an integer. Arguments without a default are required.

$ python argument_type.py Peter 34
Peter is 34 years old
$ python argument_type.py Peter old
Usage: argument_type.py [OPTIONS] NAME AGE
Try 'argument_type.py --help' for help.

Error: Invalid value for 'AGE': 'old' is not a valid integer.
$ python argument_type.py Peter
Usage: argument_type.py [OPTIONS] NAME AGE
Try 'argument_type.py --help' for help.

Error: Missing argument 'AGE'.

Variable number of arguments

With nargs=-1, an argument takes any number of values, which are passed as a tuple.

variable_args.py
import click
from math import prod

@click.command()
@click.argument('vals', type=int, nargs=-1, required=True)
def process(vals):
    click.echo(f'The values are {vals}')
    click.echo(f'The sum is {sum(vals)}')
    click.echo(f'The product is {prod(vals)}')

if __name__ == '__main__':
    process()

The process command calculates the sum and the product of the given integers.

@click.argument('vals', type=int, nargs=-1, required=True)

Each value is converted to an integer. An argument with nargs=-1 accepts zero values by default; with required=True at least one value must be given.

$ python variable_args.py 1 2 3 4 5
The values are (1, 2, 3, 4, 5)
The sum is 15
The product is 120
$ python variable_args.py
Usage: variable_args.py [OPTIONS] VALS...
Try 'variable_args.py --help' for help.

Error: Missing argument 'VALS...'.

Options

Options are added with the click.option decorator. They usually have a short name with one dash and a long name with two dashes.

dots.py
import click

@click.command()
@click.option('-n', '--count', type=int, default=1, show_default=True,
              help='Number of dots to print.')
@click.option('-c', '--char', default='.', help='Character to use.')
def dots(count, char):
    """Print a line of dots."""
    click.echo(char * count)

if __name__ == '__main__':
    dots()

The command prints a line of characters.

@click.option('-n', '--count', type=int, default=1, show_default=True,
              help='Number of dots to print.')

The help text appears on the help page, and show_default adds the default value to it.

def dots(count, char):
    """Print a line of dots."""

The docstring of the function becomes the description of the command.

The value can follow the option name as a separate word, directly after a short name (-n5), or after an equals sign (--char=#).

$ python dots.py
.
$ python dots.py --count 17
.................
$ python dots.py -n 5 -c '*'
*****
$ python dots.py -n5 --char=#
#####
$ python dots.py --help
Usage: dots.py [OPTIONS]

  Print a line of dots.

Options:
  -n, --count INTEGER  Number of dots to print.  [default: 1]
  -c, --char TEXT      Character to use.
  --help               Show this message and exit.

Option names

The name of the function parameter is derived from the longest option name; dashes are replaced with underscores. An explicit name can be given as an argument without dashes.

option_names.py
import click

@click.command()
@click.option('-s', '--string')
@click.option('--max-size', type=int)
@click.option('-o', 'output_file')
def show(string, max_size, output_file):
    click.echo(f'{string=} {max_size=} {output_file=}')

if __name__ == '__main__':
    show()

We define three options.

@click.option('--max-size', type=int)
@click.option('-o', 'output_file')

The --max-size option is passed as max_size. The -o option is passed as output_file. Options that are not given are None.

$ python option_names.py -s sky --max-size 10 -o out.txt
string='sky' max_size=10 output_file='out.txt'
$ python option_names.py --string cloud
string='cloud' max_size=None output_file=None

Required options and tuples

An option can be made mandatory with required=True. A tuple type makes an option take several values.

required.py
import click

@click.command()
@click.option('--name', required=True)
@click.option('--data', type=(str, int))
def output(name, data):
    click.echo(f'name={name} data={data}')

if __name__ == '__main__':
    output()

The --name option is required, and the --data option takes a string and an integer.

@click.option('--data', type=(str, int))

The values become a Python tuple with converted types.

$ python required.py --name Peter --data Lucy 23
name=Peter data=('Lucy', 23)
$ python required.py --data Lucy 23
Usage: required.py [OPTIONS]
Try 'required.py --help' for help.

Error: Missing option '--name'.

Choices and ranges

The click.Choice type restricts a value to a set of options, and IntRange and FloatRange restrict numbers to a range.

choices.py
import click

@click.command()
@click.option('--format', 'fmt', type=click.Choice(['json', 'csv', 'xml'],
              case_sensitive=False), default='json', show_default=True)
@click.option('--level', type=click.IntRange(1, 9), default=6)
@click.option('--ratio', type=click.FloatRange(0, 1, clamp=True), default=0.5)
def export(fmt, level, ratio):
    click.echo(f'format={fmt} level={level} ratio={ratio}')

if __name__ == '__main__':
    export()

We define an export command with three validated options.

type=click.Choice(['json', 'csv', 'xml'], case_sensitive=False)

The choice is case-insensitive, and the value is normalized to the listed form; CSV becomes csv.

@click.option('--ratio', type=click.FloatRange(0, 1, clamp=True), default=0.5)

With clamp=True, values outside the range are clamped to the nearest boundary instead of being rejected.

$ python choices.py --format CSV --level 9 --ratio 1.7
format=csv level=9 ratio=1
$ python choices.py --format yaml
Usage: choices.py [OPTIONS]
Try 'choices.py --help' for help.

Error: Invalid value for '--format': 'yaml' is not one of 'json', 'csv', 'xml'.
$ python choices.py --level 12
Usage: choices.py [OPTIONS]
Try 'choices.py --help' for help.

Error: Invalid value for '--level': 12 is not in the range 1<=x<=9.

Flags

Flags are boolean options. They are defined as a pair of names separated by a slash, or with is_flag=True. A counting option records how many times it was given.

flags.py
import click

@click.command()
@click.argument('word')
@click.option('--shout/--no-shout', default=False, help='Print in uppercase.')
@click.option('--reverse', is_flag=True, help='Reverse the word.')
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
def output(word, shout, reverse, verbose):
    if reverse:
        word = word[::-1]
    if shout:
        word = word.upper()
    if verbose:
        click.echo(f'verbosity level {verbose}')
    click.echo(word)

if __name__ == '__main__':
    output()

We define three kinds of flags.

@click.option('--shout/--no-shout', default=False, help='Print in uppercase.')

The --shout flag sets the value to True and --no-shout to False. The last one given wins.

@click.option('--reverse', is_flag=True, help='Reverse the word.')
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')

The --reverse flag is True when present. With count=True, -vvv gives the value 3.

$ python flags.py sky
sky
$ python flags.py --shout sky
SKY
$ python flags.py --shout --no-shout sky
sky
$ python flags.py --reverse --shout -vvv sky
verbosity level 3
YKS

Multiple options

With multiple=True, an option can be given several times. The values are collected in a tuple.

multiples.py
import click

@click.command()
@click.option('--word', '-w', multiple=True)
def words(word):
    click.echo(word)
    click.echo(', '.join(word))

if __name__ == '__main__':
    words()

We collect words from the --word/-w options. Without the option, the tuple is empty.

$ python multiples.py -w sky --word forest --word rock -w cloud
('sky', 'forest', 'rock', 'cloud')
sky, forest, rock, cloud
$ python multiples.py
()

Prompts

With the prompt parameter, click asks the user for a value that was not given on the command line.

prompt.py
import click

@click.command()
@click.option('--name', prompt='Your name', help='Provide your name.')
@click.option('--age', prompt=True, type=int)
def hello(name, age):
    click.echo(f'Hello {name}, you are {age}')

if __name__ == '__main__':
    hello()

The command asks for a name and an age.

@click.option('--name', prompt='Your name', help='Provide your name.')
@click.option('--age', prompt=True, type=int)

The prompt text is given explicitly or, with prompt=True, derived from the option name. When the options are given on the command line, there is no prompt.

In the second run, we feed the answers through a pipe. Piped input is not echoed, so the answers do not appear in the output. The invalid age is rejected and the prompt is repeated.

$ python prompt.py --name Peter --age 34
Hello Peter, you are 34
$ printf 'Lucy\nabc\n23\n' | python prompt.py
Your name: Age: Error: 'abc' is not a valid integer.
Age: Hello Lucy, you are 23

Environment variables

Option values can be read from environment variables with the envvar parameter. A value on the command line takes precedence.

env_var.py
import click

@click.command()
@click.option('--user', envvar='APP_USER', show_envvar=True)
@click.option('--token', envvar='APP_TOKEN', required=True)
def login(user, token):
    click.echo(f'user={user} token={token[:4]}...')

if __name__ == '__main__':
    login()

The command takes a user name and a token, which should not be typed on the command line where it would end up in the shell history.

@click.option('--user', envvar='APP_USER', show_envvar=True)

The show_envvar parameter adds the variable name to the help page.

$ APP_USER=jdoe APP_TOKEN=s3cr3t-value python env_var.py
user=jdoe token=s3cr...
$ APP_TOKEN=s3cr3t-value python env_var.py --user admin
user=admin token=s3cr...
$ python env_var.py --help
Usage: env_var.py [OPTIONS]

Options:
  --user TEXT   [env var: APP_USER]
  --token TEXT  [required]
  --help        Show this message and exit.

The click.File type

The click.File type opens a file for reading or writing. The file is closed automatically when the command finishes. The special name - stands for standard input or output.

words.txt
sky
cloud
water
forest
rock
moon
falcon
lake

We work with this text file.

head.py
import click

@click.command()
@click.argument('file', type=click.File('r'))
@click.option('-n', '--lines', default=10, show_default=True)
def head(file, lines):
    for i, line in enumerate(file):
        if i == lines:
            break
        click.echo(line, nl=False)

if __name__ == '__main__':
    head()

We create an equivalent of the Linux head command.

@click.argument('file', type=click.File('r'))

The function receives an open file object. A missing file is reported as an error before the function is called.

$ python head.py words.txt -n 4
sky
cloud
water
forest
$ printf 'one\ntwo\nthree\n' | python head.py - -n 2
one
two
$ python head.py missing.txt
Usage: head.py [OPTIONS] FILE
Try 'head.py --help' for help.

Error: Invalid value for 'FILE': 'missing.txt': No such file or directory

The click.Path type

The click.Path type validates a path but does not open the file. It is used when the program needs the name rather than the content, or opens the file itself.

path_type.py
import pathlib
import click

@click.command()
@click.argument('src', type=click.Path(exists=True, dir_okay=False,
                                       path_type=pathlib.Path))
def info(src):
    click.echo(f'{src.name}: {src.stat().st_size} bytes')
    click.echo(f'{len(src.read_text().splitlines())} lines')

if __name__ == '__main__':
    info()

The command prints the size and the number of lines of a file.

type=click.Path(exists=True, dir_okay=False, path_type=pathlib.Path)

The path must exist and must not be a directory. With path_type, the value is converted to a Path object.

$ python path_type.py words.txt
words.txt: 45 bytes
8 lines
$ python path_type.py .
Usage: path_type.py [OPTIONS] SRC
Try 'path_type.py --help' for help.

Error: Invalid value for 'SRC': File '.' is a directory.

Coloured output

The style function wraps text in ANSI escape codes for colours and styles; secho combines style and echo.

styling.py
import click

text = click.style('Hello there', fg='blue', bold=True)

print(repr(text))
print(repr(click.unstyle(text)))

click.echo(text)
click.secho('Error!', fg='red', err=True)

We style a message and print it.

print(repr(text))
print(repr(click.unstyle(text)))

The repr shows the escape codes for blue, bold, and reset. The unstyle function removes them.

click.echo(text)
click.secho('Error!', fg='red', err=True)

When the output is not a terminal, as here, echo strips the codes automatically, so redirected output stays clean. The err=True option writes to standard error, which is why the second run without stderr does not show it.

$ python styling.py
'\x1b[34m\x1b[1mHello there\x1b[0m'
'Hello there'
Hello there
Error!
$ python styling.py 2>/dev/null
'\x1b[34m\x1b[1mHello there\x1b[0m'
'Hello there'
Hello there

Command groups

Commands can be organized in groups, created with the click.group decorator. Each subcommand has its own options and help.

groups.py
import click

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    """Manage a list of tasks."""
    ctx.obj = {'debug': debug, 'tasks': ['buy milk', 'write report']}

@cli.command(name='list')
@click.pass_obj
def list_tasks(obj):
    """List all tasks."""
    for i, task in enumerate(obj['tasks'], 1):
        click.echo(f'{i}. {task}')

@cli.command()
@click.argument('title')
@click.pass_obj
def add(obj, title):
    """Add a new task."""
    obj['tasks'].append(title)
    if obj['debug']:
        click.echo(f'[debug] tasks: {obj["tasks"]}')
    click.echo(f'Added: {title}')

if __name__ == '__main__':
    cli()

We create a small task manager with list and add commands.

@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
    ctx.obj = {'debug': debug, 'tasks': ['buy milk', 'write report']}

The group function runs before any subcommand. It stores shared state in the context object ctx.obj.

@cli.command(name='list')
@click.pass_obj
def list_tasks(obj):

The name parameter sets the command name, so the function does not have to shadow the built-in list. The pass_obj decorator passes ctx.obj to the function.

For a mistyped command, click suggests the closest match.

$ python groups.py --help
Usage: groups.py [OPTIONS] COMMAND [ARGS]...

  Manage a list of tasks.

Options:
  --debug / --no-debug
  --help                Show this message and exit.

Commands:
  add   Add a new task.
  list  List all tasks.
$ python groups.py list
1. buy milk
2. write report
$ python groups.py --debug add 'call John'
[debug] tasks: ['buy milk', 'write report', 'call John']
Added: call John
$ python groups.py lst
Usage: groups.py [OPTIONS] COMMAND [ARGS]...
Try 'groups.py --help' for help.

Error: No such command 'lst'. Did you mean 'list'?

Confirmation

Destructive commands should ask for confirmation. The confirmation_option decorator adds a prompt and a --yes option to skip it.

confirm.py
import click

@click.command()
@click.argument('name')
@click.confirmation_option(prompt='Are you sure you want to drop the table?')
def drop(name):
    click.echo(f'Dropped table {name}')

if __name__ == '__main__':
    drop()

The command asks before dropping a table. Any answer other than yes aborts the command with exit code 1.

$ echo y | python confirm.py users
Are you sure you want to drop the table? [y/N]: Dropped table users
$ echo n | python confirm.py users
Are you sure you want to drop the table? [y/N]: Aborted!
$ python confirm.py users --yes
Dropped table users

Testing commands

The CliRunner class invokes commands in-process and captures their output and exit code, which makes CLI code easy to test.

test_cli.py
from click.testing import CliRunner
from flags import output

runner = CliRunner()

result = runner.invoke(output, ['--shout', 'sky'])
print(result.exit_code, repr(result.output))

result = runner.invoke(output, [])
print(result.exit_code)
print(result.output)

We test the output command from the flags example.

result = runner.invoke(output, ['--shout', 'sky'])
print(result.exit_code, repr(result.output))

The arguments are passed as a list, as they would appear on the command line. A successful run has exit code 0.

result = runner.invoke(output, [])

The missing argument gives exit code 2 and the usual error message. In a test suite, these checks become assert statements.

$ python test_cli.py
0 'SKY\n'
2
Usage: output [OPTIONS] WORD
Try 'output --help' for help.

Error: Missing argument 'WORD'.

Source

Python click documentation

In this article we have created command line interfaces with the Python click module.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.