Click is a powerful and flexible Python framework for building command-line interfaces (CLI). It simplifies development with command nesting, parameter validation, and automatic help text generation, making CLI applications more intuitive and maintainable.
This article covers the fundamentals of Click and demonstrates how to build a modular, reusable CLI.
Prerequisites
Before proceeding with the rest of this article, ensure you have a recent version of Python (3.13 or newer) and pip
installed locally on your machine. This article assumes you are familiar with basic Python concepts and have some understanding of command-line interfaces.
Getting started with Click
To get the most out of this tutorial, create a new Python project to try out the concepts we will discuss. Start by creating a new directory and setting up a virtual environment:
mkdir click-cli && cd click-cli
python3 -m venv venv
Activate the virtual environment:
source venv/bin/activate
Now, install the latest version of Click:
pip install click
Create a new file called cli.py
in the root of your project directory, and populate it with the following contents:
import click
@click.command()
def hello():
"""Simple program that says hello."""
click.echo("Hello, world!")
if __name__ == "__main__":
hello()
The snippet defines a Click command that prints "Hello, world!" to the console. It imports click
, decorates a function with @click.command()
, and uses click.echo()
for output. When run directly, the script executes the hello()
function as a CLI command.
Run the script to see the output:
python cli.py
Hello, world!
You can also check the automatically generated help text:
python cli.py --help
Usage: cli.py [OPTIONS]
Simple program that says hello.
Options:
--help Show this message and exit.
As you can see, Click automatically generates help text based on the function's docstring. This is just one of the many conveniences that Click provides out of the box.
Adding options and arguments to commands
CLI tools are most useful when they can accept user input. Click provides two primary ways to take input: options and arguments.
Options
Options are named parameters that can be specified in any order. They're typically prefixed with one or two hyphens (e.g., -v
or --verbose
).
Modify your example to include an option:
import click
@click.command()
@click.option("--name", "-n", default="World", help="Who to greet")
def hello(name):
"""Simple program that greets NAME."""
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
hello()
The updated example introduces the --name
option (with the short form -n
), which accepts a string value and defaults to "World".
It includes help text in the command's help output, and the provided option value is passed as an argument to the function.
Let's try different ways of invoking this command:
python cli.py
Hello, World!
python cli.py --name Alice
Hello, Alice!
You can also see the updated help text:
python cli.py --help
Usage: cli.py [OPTIONS]
Simple program that greets NAME.
Options:
-n, --name TEXT Who to greet
--help Show this message and exit.
Arguments
Unlike options, arguments are positional parameters that must be provided in order. They're typically used for required inputs.
Add an argument to your command:
import click
@click.command()
@click.argument("name")
@click.option("--count", "-c", default=1, help="Number of greetings")
def hello(name, count):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
hello()
The updated example introduces a required argument, NAME
, which must be provided when running the command. Additionally, the --count
(-c
) option allows users to specify how many times the greeting should be printed.
Both the argument and option values are passed to the function, which loops accordingly to display the greeting multiple times.
Let's try different ways of invoking this command:
python cli.py Alice
Hello, Alice!
python cli.py Alice --count 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
Using the --help
flag, you can see the updated usage pattern:
python cli.py --help
Usage: cli.py [OPTIONS] NAME
Simple program that greets NAME for a total of COUNT times.
Options:
-c, --count INTEGER Number of greetings
--help Show this message and exit.
Building command groups for composable CLIs
One of Click's most powerful features is its support for command groups, which allow you to create hierarchical command structures similar to those found in tools like git
or docker
. This is where Click's composability shines.
Let's create a more complex CLI with multiple commands organized into a group:
import click
@click.group()
def cli():
"""Command line tool for managing a simple database."""
pass
@cli.command()
@click.argument('name')
def create(name):
"""Create a new entry with NAME."""
click.echo(f"Creating entry: {name}")
@cli.command()
@click.argument('name')
def delete(name):
"""Delete the entry named NAME."""
click.echo(f"Deleting entry: {name}")
@cli.command()
def list():
"""List all entries."""
click.echo("Listing all entries")
if __name__ == "__main__":
cli()
This example defines a command-line tool with multiple commands using @click.group()
. The cli()
function serves as the main command group, with subcommands added using the @cli.command()
decorator.
The create
and delete
commands accept a required NAME
argument, while the list
command displays all entries. When the script runs, Click routes user input to the appropriate command based on the provided arguments.
Let's try invoking the different commands:
Create an entry:
python cli.py create myentry
Creating entry: myentry
Delete an entry:
python cli.py delete myentry
Deleting entry: myentry
List all entries:
python cli.py list
Listing all entries
The help system adapts to show both the available commands and the specific help for each command:
python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Command line tool for managing a simple database.
Options:
--help Show this message and exit.
Commands:
create Create a new entry with NAME.
delete Delete the entry named NAME.
list List all entries.
To get detailed help for a specific command, such as create
, use:
python cli.py create --help
Usage: cli.py create [OPTIONS] NAME
Create a new entry with NAME.
Options:
--help Show this message and exit.
Nesting command groups
You can further organize your CLI by nesting command groups. This is useful for complex applications with multiple related command sets:
import click
@click.group()
def cli():
"""Command line tool for managing a simple database."""
pass
@cli.group()
def users():
"""Manage users."""
pass
@users.command()
@click.argument('username')
def create(username):
"""Create a new user with USERNAME."""
click.echo(f"Creating user: {username}")
@users.command()
@click.argument('username')
def delete(username):
"""Delete the user with USERNAME."""
click.echo(f"Deleting user: {username}")
@cli.group()
def items():
"""Manage items."""
pass
@items.command()
@click.argument('name')
def create(name):
"""Create a new item with NAME."""
click.echo(f"Creating item: {name}")
@items.command()
@click.argument('name')
def delete(name):
"""Delete the item with NAME."""
click.echo(f"Deleting item: {name}")
if __name__ == "__main__":
cli()
In this example, you've organized commands into two subgroups: users
and items
. Each subgroup has its own create
and delete
commands.
Let's try invoking some of these nested commands:
python cli.py users create alice
Creating user: alice
Delete an item from the database:
python cli.py items delete myitem
Deleting item: myitem
The help system accommodates the nested structure:
python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Command line tool for managing a simple database.
Options:
--help Show this message and exit.
Commands:
items Manage items.
users Manage users.
To see available commands within a group, use:
python cli.py users --help
Usage: cli.py users [OPTIONS] COMMAND [ARGS]...
Manage users.
Options:
--help Show this message and exit.
Commands:
create Create a new user with USERNAME.
delete Delete the user with USERNAME.
This hierarchical command structure allows you to create complex CLIs that are still intuitive to use and understand.
Parameter types and validation
Click supports various parameter types and validation mechanisms to ensure that user input is correct before it's processed.
Built-in parameter types
Click provides several built-in parameter types, such as str
, int
, float
, bool
, and click.File
. Here's an example using various parameter types:
import click
@click.command()
@click.option('--count', type=int, default=1, help='Number of greetings')
@click.option('--name', prompt='Your name', help='The person to greet')
@click.option('--verbose', is_flag=True, help='Enables verbose mode')
@click.option('--output', type=click.File('w'), default='-', help='Output file')
def hello(count, name, verbose, output):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
greeting = f"Hello, {name}!"
if verbose:
greeting = f"VERBOSE: {greeting}"
click.echo(greeting, file=output)
if __name__ == "__main__":
hello()
The --count
option, defined as an integer, controls how many times the greeting is printed. If the --name
option is not provided, Click prompts the user to enter a name interactively.
The --verbose
flag modifies the output by adding a prefix, indicating that verbose mode is active.
Finally, the --output
option allows users to specify a file where the greeting should be written, defaulting to standard output if not provided.
Let's see what happens when we provide an invalid value for --count
:
python cli.py --count abc
Usage: cli.py [OPTIONS]
Try 'cli.py --help' for help.
Error: Invalid value for '--count': 'abc' is not a valid integer.
Click automatically validates the input based on the specified type and provides a helpful error message.
Custom parameter types
You can create custom parameter types for more complex validation needs. Here's an example that validates a date string:
import click
from datetime import datetime
class DateType(click.ParamType):
name = "date"
def convert(self, value, param, ctx):
try:
return datetime.strptime(value, "%Y-%m-%d").date()
except ValueError:
self.fail(f"'{value}' is not a valid date in YYYY-MM-DD format", param, ctx)
DATE = DateType()
@click.command()
@click.option('--start-date', type=DATE, help='Start date (YYYY-MM-DD)')
@click.option('--end-date', type=DATE, help='End date (YYYY-MM-DD)')
def report(start_date, end_date):
"""Generate a report for the specified date range."""
if end_date and start_date > end_date:
click.echo("Error: End date must be after start date.")
return
click.echo(f"Generating report from {start_date} to {end_date}")
if __name__ == "__main__":
report()
This implementation extends Click’s built-in validation by introducing a custom DateType
that ensures dates are properly formatted. If a user enters an invalid date, Click provides a clear error message.
Additionally, the script checks that the end date is not earlier than the start date, further enhancing validation.
A valid input should successfully generate a report:
python cli.py --start-date 2024-01-01 --end-date 2024-01-31
Generating report from 2024-01-01 to 2024-01-31
Adding context to your CLI
Click provides a powerful context system that allows you to share state between commands and callbacks. This is especially useful for passing configuration settings or database connections throughout your application.
Using the context object
The context object (ctx
) is available to every command and can be used to store and retrieve values:
import click
@click.group()
@click.option('--debug/--no-debug', default=False, help='Enable debug mode')
@click.pass_context
def cli(ctx, debug):
"""Command line tool with shared context."""
# Ensure that ctx.obj exists and is a dict
ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug
@cli.command()
@click.pass_context
def sync(ctx):
"""Synchronize data."""
debug = ctx.obj['DEBUG']
if debug:
click.echo("Debug mode is on")
click.echo("Syncing data...")
@cli.command()
@click.pass_context
def process(ctx):
"""Process data."""
debug = ctx.obj['DEBUG']
if debug:
click.echo("Debug mode is on")
click.echo("Processing data...")
if __name__ == "__main__":
cli(obj={})
In this example, @click.pass_context
is used to pass the context object to each command, allowing them to access shared values. The debug
flag is stored in the context object, making it available across all commands.
To ensure the context object behaves as expected, ctx.ensure_object(dict)
initializes it as a dictionary if it doesn't already exist.
When executing the script, an empty dictionary is passed as the initial context object with cli(obj={})
, ensuring proper setup and consistent state management throughout the CLI.
Let's try invoking the commands with and without debug mode:
python cli.py sync
Syncing data...
Run the following command with --debug
:
python cli.py --debug sync
Debug mode is on
Syncing data...
Final thoughts
This article explored Python CLI development with Click, focusing on validation, command nesting, and context sharing. Click’s flexibility and intuitive API make it a powerful choice for building simple and complex CLI applications.
To explore Click’s capabilities, refer to the official documentation.
Thanks for reading and happy coding!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github