# Creating composable CLIs with click in Python

[Click](https://click.palletsprojects.com/) 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](https://www.python.org/downloads/) (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:

```command
mkdir click-cli && cd click-cli
```

```command
python3 -m venv venv
```

Activate the virtual environment:


```command
source venv/bin/activate
```

Now, install the latest version of Click:

```command
pip install click
```

Create a new file called `cli.py` in the root of your project directory, and populate it with the following contents:

```python
[label cli.py]
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:

```command
python cli.py
```

```text
[output]
Hello, world!
```

You can also check the automatically generated help text:

```command
python cli.py --help
```

```text
[output]
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:

```python
[label cli.py]
import click

@click.command()
[highlight]
@click.option("--name", "-n", default="World", help="Who to greet")
def hello(name):
    """Simple program that greets NAME."""
    click.echo(f"Hello, {name}!")
[/highlight]

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:

```command
python cli.py
```

```text
[output]
Hello, World!
```

```command
python cli.py --name Alice
```

```text
[output]
Hello, Alice!
```
You can also see the updated help text:

```command
python cli.py --help
```

```text
[output]

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:

```python
[label cli.py]
import click

@click.command()
[highlight]
@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}!")
[/highlight]

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:

```command
python cli.py Alice
```

```text
[output]
Hello, Alice!
```

```command
python cli.py Alice --count 3
```

```text
[output]
Hello, Alice!
Hello, Alice!
Hello, Alice!
```

Using the `--help` flag, you can see the updated usage pattern:

```command
python cli.py --help
```

```text
[output]
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:

```python
[label cli.py]
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:

```command
python cli.py create myentry
```

```text
[output]
Creating entry: myentry
```

Delete an entry:

```command
python cli.py delete myentry
```

```text
[output]
Deleting entry: myentry
```

List all entries:

```command
python cli.py list
```

```text
[output]
Listing all entries
```

The help system adapts to show both the available commands and the specific help for each command:

```command
python cli.py --help
```

```text
[output]
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:

```command
python cli.py create --help
```

```text
[output]
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:

```python
[label cli.py]
import click

@click.group()
def cli():
    """Command line tool for managing a simple database."""
    pass

[highlight]
@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}")
[/highlight]

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:

```command
python cli.py users create alice
```

```text
[output]
Creating user: alice
```

Delete an item from the database:

```command
python cli.py items delete myitem
```

```text
[output]
Deleting item: myitem
```

The help system accommodates the nested structure:

```command
python cli.py --help
```

```text
[output]
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:

```command
python cli.py users --help
```

```text
[output]
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:

```python
[label cli.py]
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`:

```command
python cli.py --count abc
```

```text
[output]
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:

```python
[label cli.py]
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:

```command
python cli.py --start-date 2024-01-01 --end-date 2024-01-31
```
```text
[output]
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:

```python
[label cli.py]
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:

```command
python cli.py sync
```

```text
[output]
Syncing data...
```

Run the following command with `--debug`:

```command
python cli.py --debug sync
```

```text
[output]
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](https://click.palletsprojects.com/).

Thanks for reading and happy coding!