Intermediate
You need to write a Python script that accepts arguments from the command line. You reach for argparse, spend 20 minutes writing boilerplate, and end up with 40 lines of setup code before your actual logic even starts. There is a better way. Click is a Python library that turns regular functions into CLI commands with a single decorator. The same script that took 40 lines of argparse takes 10 lines with Click, and it generates better help text automatically.
Click was created by the Pallets team (the same people who make Flask) and is installed with pip install click. It handles argument parsing, type validation, help text generation, and nested subcommands out of the box. Flask, Black, and dozens of other widely used tools build their CLIs on top of Click.
This article covers installing Click and writing your first command, using options and arguments with type validation, setting default values and prompting users for input, grouping commands into subcommand CLIs, and building a real file-processing tool. By the end you will have the full toolkit to replace argparse in any project.
Click in Python: Quick Example
Here is a complete, runnable Click CLI in under 15 lines. Save it as hello.py and run it from the terminal:
# hello.py
import click
@click.command()
@click.option("--name", default="World", help="Who to greet.")
@click.option("--count", default=1, type=int, help="How many times to greet.")
def greet(name, count):
"""A simple greeting command."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == "__main__":
greet()
Output:
$ python hello.py
Hello, World!
$ python hello.py --name Alice --count 3
Hello, Alice!
Hello, Alice!
Hello, Alice!
$ python hello.py --help
Usage: hello.py [OPTIONS]
A simple greeting command.
Options:
--name TEXT Who to greet.
--count INTEGER How many times to greet.
--help Show this message and exit.
Notice that --help is generated automatically from the function docstring and the help= parameters you pass to each option. With argparse, you would have written parser = argparse.ArgumentParser(description=...), then parser.add_argument("--name", ...), then parser.add_argument("--count", ...), then args = parser.parse_args(), then called your function manually. Click collapses all of that into decorator declarations.
What Is Click and How Does It Work?
Click (Command Line Interface Creation Kit) is a decorator-based CLI library. The core concept is simple: you write a normal Python function, add @click.command() to mark it as a CLI entry point, add @click.option() or @click.argument() decorators to declare the inputs, and Click handles everything else — parsing, type conversion, validation, and help text.
The distinction between options and arguments matters in Click. Options are named parameters preceded by --, like --output file.txt. Arguments are positional parameters that appear without a name, like the filename in cat myfile.txt. Options are optional by default (you can set defaults); arguments are required by default.
| Feature | argparse | Click |
|---|---|---|
| Setup boilerplate | ~10 lines before any logic | Just decorators |
| Help text | Manual via help= | From docstring + help= |
| Type validation | Via type= | Via type=, richer built-ins |
| Subcommands | Subparsers (verbose) | @click.group() (clean) |
| Password prompts | Manual + getpass | Built-in with hide_input=True |
| File handling | Manual open/close | Built-in click.File type |
| Progress bars | Third-party | Built-in click.progressbar |
Options and Arguments
Options and arguments are the two building blocks of any CLI. Understanding when to use each one leads to interfaces that feel intuitive to users.
Use arguments for required, positional inputs — the thing the command operates on. Use options for modifiers that change how the command behaves. The Unix convention is: cp source destination uses arguments, cp -r uses an option. Click encourages this same pattern.
# file_tool.py
import click
@click.command()
@click.argument("source")
@click.argument("destination")
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output.")
@click.option("--dry-run", is_flag=True, help="Show what would happen without doing it.")
def copy_file(source, destination, verbose, dry_run):
"""Copy SOURCE to DESTINATION."""
if verbose:
click.echo(f"Copying {source} -> {destination}")
if dry_run:
click.echo(f"[dry-run] Would copy {source} to {destination}")
return
# Actual copy logic would go here
click.echo(f"Copied {source} to {destination}")
if __name__ == "__main__":
copy_file()
Output:
$ python file_tool.py data.csv output/data.csv --verbose
Copying data.csv -> output/data.csv
Copied data.csv to output/data.csv
$ python file_tool.py data.csv output/data.csv --dry-run
[dry-run] Would copy data.csv to output/data.csv
$ python file_tool.py --help
Usage: file_tool.py [OPTIONS] SOURCE DESTINATION
Copy SOURCE to DESTINATION.
Options:
-v, --verbose Enable verbose output.
--dry-run Show what would happen without doing it.
--help Show this message and exit.
The is_flag=True parameter makes an option a boolean toggle — present means True, absent means False. The short form alias -v is declared alongside --verbose in the same decorator. Click also uppercases argument names in the help text automatically, which is how SOURCE and DESTINATION appear in all caps in the usage line.
Types and Built-In Validation
Click’s type system validates inputs before your function even runs, generating clear error messages automatically. Built-in types include INT, FLOAT, BOOL, STRING, UUID, PATH, and File. There is also click.Choice for enum-style options and click.IntRange for bounded numbers.
# types_demo.py
import click
@click.command()
@click.option(
"--format",
type=click.Choice(["json", "csv", "tsv"], case_sensitive=False),
default="json",
help="Output format."
)
@click.option(
"--workers",
type=click.IntRange(1, 32),
default=4,
show_default=True,
help="Number of worker threads (1-32)."
)
@click.option(
"--output",
type=click.Path(dir_okay=False, writable=True),
required=True,
help="Output file path."
)
def process(format, workers, output):
"""Process data and write results."""
click.echo(f"Processing with {workers} workers -> {output} ({format})")
if __name__ == "__main__":
process()
Output:
$ python types_demo.py --output results.json
Processing with 4 workers -> results.json (json)
$ python types_demo.py --format xml --output results.json
Error: Invalid value for '--format': 'xml' is not one of 'json', 'csv', 'tsv'.
$ python types_demo.py --workers 100 --output results.json
Error: Invalid value for '--workers': 100 is not in the range 1<=x<=32.
$ python types_demo.py --help
Usage: types_demo.py [OPTIONS]
Options:
--format [json|csv|tsv] Output format.
--workers INTEGER RANGE Number of worker threads (1-32). [default: 4; 1<=x<=32]
--output PATH Output file path. [required]
--help Show this message and exit.
Notice show_default=True on the workers option — it adds [default: 4] to the help text automatically, saving you from writing it manually. The click.Path type validates that the path is writable and not a directory without requiring any custom validation code in your function.
Prompts and Confirmation
Some commands need to ask the user for input at runtime — a password, a confirmation before a destructive operation, or a value when an option was not provided. Click has built-in support for all of these patterns.
# prompts_demo.py
import click
@click.command()
@click.option("--username", prompt="Username", help="Your username.")
@click.option(
"--password",
prompt="Password",
hide_input=True,
confirmation_prompt=True,
help="Your password."
)
def login(username, password):
"""Log in to the system."""
click.echo(f"Logging in as: {username}")
click.echo(f"Password length: {len(password)} chars")
@click.command()
@click.argument("path")
def delete(path):
"""Delete a file at PATH."""
confirmed = click.confirm(f"Are you sure you want to delete {path!r}?")
if confirmed:
click.echo(f"Deleted {path}")
else:
click.echo("Cancelled.")
if __name__ == "__main__":
login()
Interactive session:
$ python prompts_demo.py login
Username: alice
Password:
Repeat for confirmation:
Logging in as: alice
Password length: 8 chars
When prompt=True (or prompt="some text") is set on an option and the option is not provided on the command line, Click prompts the user interactively. The hide_input=True flag suppresses echo for passwords. confirmation_prompt=True asks the user to type the value twice, a standard UX pattern for password entry. For destructive operations, click.confirm() shows a yes/no prompt and returns a boolean.
Groups and Subcommands
Real CLI tools like git, docker, and pip have subcommands: git commit, docker run, pip install. Click builds these with @click.group(), which turns a function into a container for sub-commands.
# cli_group.py
import click
@click.group()
def db():
"""Database management commands."""
pass
@db.command()
@click.option("--host", default="localhost", help="Database host.")
@click.option("--port", default=5432, type=int, help="Database port.")
def connect(host, port):
"""Connect to the database."""
click.echo(f"Connecting to {host}:{port}")
@db.command()
@click.argument("table")
@click.option("--limit", default=10, type=int, help="Max rows to show.")
def query(table, limit):
"""Query TABLE and print results."""
click.echo(f"SELECT * FROM {table} LIMIT {limit}")
@db.command()
@click.confirmation_option(prompt="Are you sure you want to drop all tables?")
def reset():
"""Drop all tables and reset the database."""
click.echo("Database reset.")
if __name__ == "__main__":
db()
Output:
$ python cli_group.py --help
Usage: cli_group.py [OPTIONS] COMMAND [ARGS]...
Database management commands.
Commands:
connect Connect to the database.
query Query TABLE and print results.
reset Drop all tables and reset the database.
$ python cli_group.py connect --host db.prod.example.com
Connecting to db.prod.example.com:5432
$ python cli_group.py query users --limit 5
SELECT * FROM users LIMIT 5
Sub-commands are defined on the group by using @db.command() instead of @click.command(). Each sub-command has its own options and arguments, independently declared. The @click.confirmation_option() decorator is a shorthand for adding a --yes flag that prompts for confirmation before running — ideal for destructive commands where you want a safety check without writing custom prompt logic.
Real-Life Example: CSV Processor CLI
Here is a practical multi-command CLI for processing CSV files, combining everything covered above: typed options, file arguments, a group structure, and error handling with click.echo and click.style:
# csv_tool.py
"""
A CSV processing CLI built with Click.
Install: pip install click
Usage: python csv_tool.py --help
"""
import csv
import sys
import click
@click.group()
@click.version_option("1.0.0")
def cli():
"""CSV file processing toolkit."""
pass
@cli.command()
@click.argument("csv_file", type=click.Path(exists=True, readable=True))
@click.option("--delimiter", default=",", help="Column delimiter.")
@click.option("--max-rows", default=10, type=int, help="Max rows to preview.")
def preview(csv_file, delimiter, max_rows):
"""Preview the first rows of a CSV file."""
try:
with open(csv_file, newline="", encoding="utf-8") as f:
reader = csv.reader(f, delimiter=delimiter)
headers = next(reader)
click.echo(click.style("Headers: ", fg="green") + ", ".join(headers))
click.echo(f"{'---' * 20}")
for i, row in enumerate(reader):
if i >= max_rows:
break
click.echo(" | ".join(row))
except StopIteration:
click.echo(click.style("Error: Empty CSV file.", fg="red"), err=True)
sys.exit(1)
@cli.command()
@click.argument("csv_file", type=click.Path(exists=True, readable=True))
@click.option(
"--output",
type=click.Path(dir_okay=False, writable=True),
required=True,
help="Output file path."
)
@click.option(
"--columns",
multiple=True,
help="Columns to keep. Repeat for multiple: --columns id --columns name"
)
def extract(csv_file, output, columns):
"""Extract specific columns from a CSV file."""
with open(csv_file, newline="", encoding="utf-8") as f_in:
reader = csv.DictReader(f_in)
fieldnames = columns if columns else reader.fieldnames
with open(output, "w", newline="", encoding="utf-8") as f_out:
writer = csv.DictWriter(f_out, fieldnames=fieldnames, extrasaction="ignore")
writer.writeheader()
row_count = 0
for row in reader:
writer.writerow(row)
row_count += 1
click.echo(click.style(f"Extracted {row_count} rows", fg="green") + f" -> {output}")
if __name__ == "__main__":
cli()
Create a sample CSV and run it:
# First, create a sample file
$ echo "id,name,email,age
1,Alice,alice@example.com,30
2,Bob,bob@example.com,25
3,Charlie,charlie@example.com,35" > people.csv
$ python csv_tool.py preview people.csv
Headers: id, name, email, age
------------------------------------------------------------
1 | Alice | alice@example.com | 30
2 | Bob | bob@example.com | 25
3 | Charlie | charlie@example.com | 35
$ python csv_tool.py extract people.csv --output names.csv --columns id --columns name
Extracted 3 rows -> names.csv
This example shows several real-world Click patterns: click.style() for colored terminal output, multiple=True for options that can be repeated, click.Path(exists=True) for automatic file validation, and err=True in click.echo() to write to stderr instead of stdout. The @click.version_option() decorator adds a free --version flag to the group.
Frequently Asked Questions
When should I use Click instead of argparse?
Use Click for any CLI you are writing from scratch or actively maintaining. The decorator syntax is more readable and generates better help text with less code. Use argparse only if you are adding to an existing codebase that already uses it extensively, or if you are building tooling that ships in environments where adding third-party dependencies is restricted (Click is not in the standard library). For scripts you write for yourself, Click is almost always worth the pip install.
How do I test Click commands?
Click provides a CliRunner class in click.testing that lets you invoke commands programmatically without spawning a subprocess. Call runner.invoke(your_command, args=["--option", "value"]) and inspect result.output and result.exit_code. This makes CLI testing as straightforward as testing any other function, without needing to mock sys.argv or capture stdout manually.
Can Click read values from environment variables?
Yes, via the envvar parameter on @click.option(): @click.option("--api-key", envvar="API_KEY"). When the option is not provided on the command line, Click checks the environment variable. You can also set auto_envvar_prefix="MYAPP" on the group to make all options automatically check for environment variables prefixed with MYAPP_. This is a clean pattern for twelve-factor applications that configure via the environment.
How do I package a Click CLI as a pip-installable command?
In your pyproject.toml, set the entry point under [project.scripts]: my-tool = "mypackage.cli:cli". After pip install -e ., running my-tool invokes your Click group or command directly. This is how Flask exposes its flask CLI, how Black exposes the black command, and how most pip-installed tools expose their entry points.
How do I accept a variable number of values for one option?
Two ways: use multiple=True on an option to allow it to be repeated (--tag foo --tag bar), or use nargs=-1 on an argument to accept any number of positional values (tool.py file1.txt file2.txt file3.txt). With multiple=True, the value in your function is a tuple of all the provided values. With nargs=-1, the argument value is also a tuple. Both patterns are shown in the CSV tool example above (--columns uses multiple=True).
Conclusion
Click covers the full range of CLI development tasks: simple single-command scripts, complex multi-subcommand tools, interactive prompts, file handling, and colored output. The decorator pattern means your code describes the interface rather than constructing a parser object, which makes it easier to read and easier to modify later.
The key functions and decorators to remember are @click.command(), @click.group(), @click.option(), @click.argument(), click.echo(), and click.style() for colored output. From there, the official Click documentation covers advanced topics like context objects, plugin systems, and integrating Click with other frameworks.