You can use the Click library to quickly provide your Python automation and tooling scripts with an extensible, composable, and user-friendly command-line interface (CLI). Whether youβre a developer, data scientist, DevOps engineer, or someone who often uses Python to automate repetitive tasks, youβll very much appreciate Click and its unique features.
In the Python ecosystem, youβll find multiple libraries for creating CLIs, including argparse from the standard library, Typer, and a few others. However, Click offers a robust, mature, intuitive, and feature-rich solution.
In this tutorial, youβll learn how to:
- Create command-line interfaces with Click and Python
- Add arguments, options, and subcommands to your CLI apps
- Enhance the usage and help pages of your CLI apps with Click
- Prepare a Click CLI app for installation, use, and distribution
To get the most out of this tutorial, you should have a good understanding of Python programming, including topics such as using decorators. Itβll also be helpful if youβre familiar with using your current operating systemβs command line or terminal.
Get Your Code: Click here to download the sample code that youβll use to build your CLI app with Click and Python.
Creating Command-Line Interfaces With Click and Python
The Click library enables you to quickly create robust, feature-rich, and extensible command-line interfaces (CLIs) for your scripts and tools. This library can significantly speed up your development process because it allows you to focus on the applicationβs logic and leave CLI creation and management to the library itself.
Click is a great alternative to the argparse module, which is the default CLI framework in the Python standard library. Next up, youβll learn what sets it apart.
Why Use Click for CLI Development
Compared with argparse, Click provides a more flexible and intuitive framework for creating CLI apps that are highly extensible. It allows you to gradually compose your apps without restrictions and with a minimal amount of code. This code will be readable even when your CLI grows and becomes more complex.
Clickβs application programming interface (API) is highly intuitive and consistent. The API takes advantage of Python decorators, allowing you to add arguments, options, and subcommands to your CLIs quickly.
Functions are fundamental in Click-based CLIs. You have to write functions that you can then wrap with the appropriate decorators to create arguments, commands, and so on.
Click has several desirable features that you can take advantage of. For example, Click apps:
- Can be lazily composable without restrictions
- Follow the Unix command-line conventions
- Support loading values from environment variables
- Support custom prompts for input values
- Handle paths and files out of the box
- Allow arbitrary nesting of commands, also known as subcommands
Youβll find that Click has many other cool features. For example, Click keeps information about all of your arguments, options, and commands. This way, it can generate usage and help pages for the CLI, which improves the user experience.
When it comes to processing user input, Click has a strong understanding of data types. Because of this feature, the library generates consistent error messages when the user provides the wrong type of input.
Now that you have a general understanding of Clickβs most relevant features, itβs time to get your hands dirty and write your first Click app.
How to Install and Set Up Click: Your First CLI App
Unlike argparse, Click doesnβt come in the Python standard library. This means that you need to install Click as a dependency of your CLI project to use the library. You can install Click from PyPI using pip. First, you should create a Python virtual environment to work on. You can do all of that with the following platform-specific commands:
With the first two commands, you create and activate a Python virtual environment called venv in your working directory. Once the environment is active, you install Click using pip.
Great! Youβve installed Click in a fresh virtual environment. Now go ahead and fire up your favorite code editor. Create a new hello.py file and add the following content to it:
# hello.py
import click
@click.command("hello")
@click.version_option("0.1.0", prog_name="hello")
def hello():
click.echo("Hello, World!")
if __name__ == "__main__":
hello()
In this file, you first import the click package. Then you create a function called hello(). In this function, you print a message to the screen. To do this, you use the Click echo() function instead of your old friend print(). Why would you do that?
The echo() function applies some error corrections in case the terminal program has configuration issues. It also supports colors and other styles in the output. It automatically removes any styling if the output stream is a file rather than the standard output. So, when working with Click, you should use echo() to handle the appβs output.
You use two decorators on top of this function. The @click.command decorator declares hello() as a Click command with the name "hello". The @click.version_option decorator sets the CLI appβs version and name. This information will show up when you run the app with the --version command-line option.
Finally, you add the name-main idiom to call the hello() function when you run the file as an executable program.
Go ahead and run the app from your command line:
(venv) $ python hello.py
Hello, World!
(venv) $ python hello.py --version
hello, version 0.1.0
(venv) $ python hello.py --help
Usage: hello.py [OPTIONS]
Options:
--version Show the version and exit.
--help Show this message and exit.
When you run the script without arguments, you get Hello, World! displayed on your screen. If you use the --version option, then you get information about the appβs name and version. Note that Click automatically provides the --help option, which you can use to access the appβs main help page.
That was cool! With a few lines of code and the power of Click, youβve created your first CLI app. Itβs a minimal app, but itβs enough to get a grasp of what youβll be capable of creating with Click. To continue your CLI journey, youβll learn how to make your apps take input arguments from the user.
Adding Arguments to a Click App
In CLI development, an argument is a required or optional piece of information that a command uses to perform its intended action. Commands typically accept arguments, which you can provide as a whitespace-separated or comma-separated list on your command line.
In this section, youβll learn how to take command-line arguments in your Click applications. Youβll start with the most basic form of arguments and walk through different types of arguments, including paths, files, environment variables, and more.
Adding Basic Arguments
You can use the @click.argument decorator to make your Click app accept and parse arguments that you provide at the command line directly. Click parses the most basic arguments as strings that you can then pass to the underlying function.
To illustrate, say that you want to create a small CLI app to mimic the Unix ls command. In its most minimal variation, this command takes a directory as an argument and lists its content. If youβre on Linux or macOS, then you can try out the command like in the following example:
$ ls sample/
hello.txt lorem.md realpython.md
This example assumes that you have a folder called sample/ in your current working directory. Your folder contains the files hello.txt, lorem.md, and realpython.md, which are listed on the same output line.
Note: If youβre on Windows, then youβll have an ls command that works similarly to the Unix ls command. However, in its plain form, the command displays a different output:
PS> ls .\sample\
Directory: C:\sample
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 07/11/2023 10:06 AM 88 hello.txt
-a--- 07/11/2023 10:06 AM 2629 lorem.md
-a--- 07/11/2023 10:06 AM 429 realpython.md
The PowerShell ls command issues a table containing detailed information on every file and subdirectory in your target directory. So, in the upcoming versions of ls.py, youβll be mimicking the Unix version of this command instead of the PowerShell version.
To follow along with this tutorial and get the same outputs, you can download the companion sample code and resources, including the sample/ directory, by clicking the link below:
Get Your Code: Click here to download the sample code that youβll use to build your CLI app with Click and Python.
How can you emulate this command behavior using Click and Python? You can do something like the following:
# ls.py v1
from pathlib import Path
import click
@click.command()
@click.argument("path")
def cli(path):
target_dir = Path(path)
if not target_dir.exists():
click.echo("The target directory doesn't exist")
raise SystemExit(1)
for entry in target_dir.iterdir():
click.echo(f"{entry.name:{len(entry.name) + 5}}", nl=False)
click.echo()
if __name__ == "__main__":
cli()
This is your first version of the ls command emulator app. You start by importing the Path class from pathlib. Youβll use this class to efficiently manage paths in your application. Next, you import click as usual.
Your ls emulator needs a single function to perform its intended task. You call this function cli(), which is a common practice. Click apps typically name the entry-point command cli(), as youβll see throughout this tutorial.
In this example, you use the @click.command decorator to define a command. Then you use the @click.argument decorator with the string "path" as an argument. This call to the decorator adds a new command-line argument called "path" to your custom ls command.
Note that the name of the command-line argument must be the same as the argument to cli(). This way, youβre passing the user input directly to your processing code.
Inside cli(), you create a new Path instance using the user input. Then you check the input path. If the path doesnβt exist, then you inform the user and exit the app with an appropriate exit status. If the path exists, then the for loop lists the directory content, simulating what the Unix ls command does.
The call to click.echo() at the end of cli() allows you to add a new line at the end of the output to match the ls behavior.
Note: To dive deeper into listing the content of a directory, check out How to Get a List of All Files in a Directory With Python.
If you run the commands below, then youβll get the following results:
(venv) $ python ls.py sample/
lorem.md realpython.md hello.txt
(venv) $ python ls.py non_existing/
The target directory doesn't exist
(venv) $ python ls.py
Usage: ls.py [OPTIONS] PATH
Try 'ls.py --help' for help.
Error: Missing argument 'PATH'.
If you run the app with a valid directory path, then you get the directory content listed. If the target directory doesnβt exist, then you get an informative message. Finally, running the app without an argument causes the app to fail, displaying the help page.
Note: You may find that the order in which ls.py lists the files doesnβt match the order in your output or in the original ls commandβs output. Thatβs because the .iterdir() method yields directory entries in arbitrary order.
How does that look for not even twenty lines of Python code? Great! However, Click offers you a better way to do this. You can take advantage of Clickβs power to automatically handle file paths in your applications.
Using Path Arguments
The @click.argument decorator accepts an argument called type that you can use to define the target data type of the argument at hand. In addition to this, Click provides a rich set of custom classes that allow you to consistently handle different data types, including paths.
Note: Youβll learn more about Clickβs custom parameter types throughout this tutorial, especially in the Providing Parameter Types for Arguments and Options section.
In the example below, you rewrite the ls app using Clickβs capabilities:
# ls.py v2
from pathlib import Path
import click
@click.command()
@click.argument(
"path",
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(path):
for entry in path.iterdir():
click.echo(f"{entry.name:{len(entry.name) + 5}}", nl=False)
click.echo()
if __name__ == "__main__":
cli()
In this new version of ls.py, you pass a click.Path object to the type argument of @click.argument. With this addition, Click will treat any input as a path object.
To instantiate the click.Path() class in this example, you use several arguments:
exists: If you set it toTrue, then Click will make sure that the path exists.file_okay: If you set it toFalse, then Click will make sure that the input path doesnβt point to a file.readable: If you set it toTrue, then Click will make sure that you can read the content of the target directory.path_type: If you set it topathlib.Path, then Click will turn the input into aPathobject.
With these settings in place, your cli() function is more concise. It only needs the for loop to list the directory content. Go ahead and run the following commands to test the new version of your ls.py script:
(venv) $ python ls.py sample/
lorem.md realpython.md hello.txt
(venv) $ python ls.py non_existing/
Usage: ls.py [OPTIONS] PATH
Try 'ls.py --help' for help.
Error: Invalid value for 'PATH': Directory 'non_existing/' does not exist.
Again, when you run the app with a valid directory path, you get the directory content listed. If the target directory doesnβt exist, then Click handles the issue for you. You get a nice usage message and an error message describing the current issue. Thatβs an even better behavior if you compare it with your first ls.py version.
Accepting Variadic Arguments
In Clickβs terminology, a variadic argument is one that accepts an undetermined number of input values at the command line. This type of argument is pretty common in CLI development. For example, the Unix ls command takes advantage of this feature, allowing you to process multiple directories at a time.
To give it a try, make a copy of your sample/ folder and run the following command:
$ ls sample/ sample_copy/
sample/:
hello.txt lorem.md realpython.md
sample_copy/:
hello.txt lorem.md realpython.md
The ls command can take multiple target directories at the command line. The output will list the content of each directory, as you can conclude from the example above. How can you emulate this behavior using Click and Python?
The @click.argument decorator takes an argument called nargs that allows you to predefine the number of values that an argument can accept at the command line. If you set nargs to -1, then the underlying argument will collect an undetermined number of input values in a tuple.
Hereβs how you can take advantage of nargs to accept multiple directories in your ls emulator:
# ls.py v3
from pathlib import Path
import click
@click.command()
@click.argument(
"paths",
nargs=-1,
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(paths):
for i, path in enumerate(paths):
if len(paths) > 1:
click.echo(f"{path}/:")
for entry in path.iterdir():
click.echo(f"{entry.name:{len(entry.name) + 5}}", nl=False)
if i < len(paths) - 1:
click.echo("\n")
else:
click.echo()
if __name__ == "__main__":
cli()
In the first highlighted line, you change the argumentβs name from "path" to "paths" because now the argument will accept multiple directory paths. Then you set nargs to -1 to indicate that this argument will accept multiple values at the command line.
In the cli() function, you change the argumentβs name to match the command-line argumentβs name. Then you start a loop over the input paths. The conditional statement prints the name of the current directory, simulating what the original ls command does.
Then you run the usual loop to list the directory content, and finally, you call echo() to add a new blank line after the content of each directory. Note that you use the enumerate() function to get an index for every path. This index allows you to figure out when the output should end so that you can skip the extra blank line and mimic the ls behavior.
With these updates in place, you can run the app again:
(venv) $ python ls.py sample/ sample_copy/
sample/:
lorem.md realpython.md hello.txt
sample_copy/:
lorem.md realpython.md hello.txt
Now your custom ls command behaves similarly to the original Unix ls command when you pass multiple target directories at the command line. Thatβs great! Youβve learned how to implement variadic arguments with Click.
Taking File Arguments
Click provides a parameter type called File that you can use when the input for a given command-line argument must be a file. With File, you can declare that a given parameter is a file. You can also declare whether your app should open the file for reading or writing.
To illustrate how you can use the File parameter type, say that you want to emulate the basic functionality of the Unix cat command. This command reads files sequentially and writes their content to the standard output, which is your screen:
$ cat sample/hello.txt sample/realpython.md
Hello, Pythonista!
Welcome to Real Python!
At Real Python you'll learn all things Python from the ground up.
Their tutorials, books, and video courses are created, curated,
and vetted by a community of expert Pythonistas. With new content
published weekly, custom Python learning paths, and interactive
code challenges, you'll always find something to boost your skills.
Join 3,000,000+ monthly readers and take your Python skills to the
next level at realpython.com.
In this example, you use cat to concatenate the content of two files from your sample directory. The following app mimics this behavior using Clickβs File parameter type:
# cat.py
import click
@click.command()
@click.argument(
"files",
nargs=-1,
type=click.File(mode="r"),
)
def cli(files):
for file in files:
click.echo(file.read().rstrip())
if __name__ == "__main__":
cli()
In this example, you set the type argument to click.File. The "r" mode means that youβre opening the file for reading.
Inside cli(), you start a for loop to iterate over the input files and print their content to the screen. Itβs important to note that you donβt need to worry about closing each file once youβve read its content. The File type automatically closes it for you once the command finishes running.
Hereβs how this app works in practice:
(venv) $ python cat.py sample/hello.txt sample/realpython.md
Hello, Pythonista!
Welcome to Real Python!
At Real Python you'll learn all things Python from the ground up.
Their tutorials, books, and video courses are created, curated,
and vetted by a community of expert Pythonistas. With new content
published weekly, custom Python learning paths, and interactive
code challenges, you'll always find something to boost your skills.
Join 3,000,000+ monthly readers and take your Python skills to the
next level at realpython.com.
Your cat.py script works pretty similarly to the Unix cat command. It accepts multiple files at the command line, opens them for reading, reads their content, and prints it to the screen sequentially. Great job!
Providing Options in Your Click Apps
Command options are another powerful feature of Click applications. Options are named, non-required arguments that modify a commandβs behavior. You pass an option to a command using a specific name, which typically has a prefix of one dash (-) or two dashes (--) on Unix systems. On Windows, you may also find options with other prefixes, such as a slash (/).
Because options have names, they enhance the usability of a CLI app. In Click, options can do the same as arguments. Additionally, options have a few extra features. For example, options can:
- Prompt for input values
- Act as flags or feature switches
- Pull their value from environment variables
Unlike arguments, options can only accept a fixed number of input values, and this number defaults to 1. Additionally, you can specify an option multiple times using multiple options, but you canβt do this with arguments.
In the following sections, youβll learn how to add options to your Click command and how options can help you improve your usersβ experience while they work with your CLI apps.
Adding Single-Value Options
To add an option to a Click command, youβll use the @click.option decorator. The first argument to this decorator will hold the optionβs name.
CLI options often have a long and a short name. The long name typically describes what the option does, while the short name is commonly a single-letter shortcut. To Click, names with a single leading dash are short names, while names with two leading dashes are long ones.
In Click, the most basic type of option is a single-value option, which accepts one argument at the command line. If you donβt provide a parameter type for the option value, then Click assumes the click.STRING type.
To illustrate how you can create options with Click, say that you want to write a CLI app that emulates the Unix tail command. This command displays the tail end of a text file:
$ tail sample/lorem.md
ac. Nulla sapien nulla, egestas at pretium ac, feugiat nec arcu. Donec
ullamcorper laoreet odio, id posuere nisl ullamcorper at.
### Nam Aliquam Ultricies Pharetra
Nam aliquam ultricies pharetra. Pellentesque accumsan finibus ex porta
aliquet. Morbi placerat sagittis tortor, ut maximus sem iaculis sit amet.
Aliquam sit amet libero dapibus, vehicula arcu non, pulvinar felis.
Suspendisse a risus magna. Nulla facilisi. Donec eu consequat ligula, iaculis
aliquet augue.
$ tail --lines 3 sample/lorem.md
Aliquam sit amet libero dapibus, vehicula arcu non, pulvinar felis.
Suspendisse a risus magna. Nulla facilisi. Donec eu consequat ligula, iaculis
aliquet augue.
By default, tail displays the last ten lines of the input file. However, the command has an -n or --lines option that allows you to tweak that number, as you can see in the second execution above, where you only printed the last three lines of lorem.md.
You can use Click to emulate the behavior of tail. In this case, you need to add an option using the @click.option decorator as in the code below:
# tail.py
from collections import deque
import click
@click.command()
@click.option("-n", "--lines", type=click.INT, default=10)
@click.argument(
"file",
type=click.File(mode="r"),
)
def cli(file, lines):
for line in deque(file, maxlen=lines):
click.echo(line, nl=False)
if __name__ == "__main__":
cli()
In this example, you first import the deque data type from the collections module. Youβll use this type to quickly get the final lines of your input file. Then you import click as usual.
Note: Your use of deque for this example comes from the Python documentation on deque. Check out the section on deque recipes for further examples.
In the highlighted line, you call the @click.option decorator to add a new option to your Click command. The first two arguments in this call provide short and long names for the option, respectively.
Because the user input must be an integer number, you use click.INT to define the parameterβs type. The default behavior of tail is to display the final ten lines, so you set default to 10 and discover another cool feature of Clickβs options. They can have default values.
Next, you add an argument called "file", which is of type click.File. You already know how the File type works.
In cli(), you take the file and the number of lines as arguments. Then you loop over the last lines using a deque object. This specific deque object can only store up to lines items. This guarantees that you get the desired number of lines from the end of the input file.
Go ahead and give tail.py a try by running the following commands:
(venv) $ python tail.py sample/lorem.md
ac. Nulla sapien nulla, egestas at pretium ac, feugiat nec arcu. Donec
ullamcorper laoreet odio, id posuere nisl ullamcorper at.
### Nam Aliquam Ultricies Pharetra
Nam aliquam ultricies pharetra. Pellentesque accumsan finibus ex porta
aliquet. Morbi placerat sagittis tortor, ut maximus sem iaculis sit amet.
Aliquam sit amet libero dapibus, vehicula arcu non, pulvinar felis.
Suspendisse a risus magna. Nulla facilisi. Donec eu consequat ligula, iaculis
aliquet augue.
(venv) $ python tail.py --lines 3 sample/lorem.md
Aliquam sit amet libero dapibus, vehicula arcu non, pulvinar felis.
Suspendisse a risus magna. Nulla facilisi. Donec eu consequat ligula, iaculis
aliquet augue.
(venv) $ python tail.py --help
Usage: tail.py [OPTIONS] FILE
Options:
-n, --lines INTEGER
--help Show this message and exit.
Your custom tail command works similarly to the original Unix tail command. It takes a file and displays the last ten lines by default. If you provide a different number of lines with the --lines option, then the command displays only your desired lines from the end of the input file.
When you check the help page of your tail command, you see that the -n or --lines option now shows up under the Options heading. By default, you also get information about the optionβs parameter type, which is an integer number in this example.
Creating Multi-Value Options
Sometimes, you need to implement an option that takes more than one input value at the command line. Unlike arguments, Click options only support a fixed number of input values. You can configure this number using the nargs argument of @click.option.
The example below accepts a --size option that needs two input values, width and height:
# rectangle.py v1
import click
@click.command()
@click.option("--size", nargs=2, type=click.INT)
def cli(size):
width, height = size
click.echo(f"size: {size}")
click.echo(f"{width} Γ {height}")
if __name__ == "__main__":
cli()
In this example, you set nargs to 2 in the call to the @click.option decorator that defines the --size option. This setting tells Click that the option will accept two values at the command line.
Hereβs how this toy app works in practice:
(venv) $ python rectangle.py --size 400 200
size: (400, 200)
400 Γ 200
(venv) $ python rectangle.py --size 400
Error: Option '--size' requires 2 arguments.
(venv) $ python rectangle.py --size 400 200 100
Usage: rectangle.py [OPTIONS]
Try 'rectangle.py --help' for help.
Error: Got unexpected extra argument (100)
The --size option accepts two input values at the command line. Click stores these values in a tuple that you can process inside the cli() function. Note how the --size option doesnβt accept fewer or more than two input values.
Click provides an alternative way to create multi-value options. Instead of using the nargs argument of @click.option, you can set the type argument to a tuple. Consider the following alternative implementation of your rectangle.py script:
# rectangle.py v2
import click
@click.command()
@click.option("--size", type=(click.INT, click.INT))
def cli(size):
width, height = size
click.echo(f"size: {size}")
click.echo(f"{width} Γ {height}")
if __name__ == "__main__":
cli()
In this alternative implementation of rectangle.py, you set the type argument to a tuple of integer values. Note that you can also use the click.Tuple parameter type to get the same result. Using this type will be more explicit, and you only have to do type=click.Tuple([int, int]).
Go ahead and try out this new variation of your app:
(venv) $ python rectangle.py --size 400 200
size: (400, 200)
400 Γ 200
(venv) $ python rectangle.py --size 400
Error: Option '--size' requires 2 arguments.
(venv) $ python rectangle.py --size 400 200 100
Usage: rectangle.py [OPTIONS]
Try 'rectangle.py --help' for help.
Error: Got unexpected extra argument (100)
This implementation works the same as the one that uses nargs=2. However, by using a tuple for the type argument, you can customize the parameter type of each item in the tuple, which can be a pretty handy feature in some situations.
To illustrate how click.Tuple can help you, consider the following example:
# person.py
import click
@click.command()
@click.option("--profile", type=click.Tuple([str, int]))
def cli(profile):
click.echo(f"Hello, {profile[0]}! You're {profile[1]} years old!")
if __name__ == "__main__":
cli()
In this example, the --profile option takes a two-item tuple. The first item should be a string representing a personβs name. The second item should be an integer representing their age.
Hereβs how this toy app works in practice:
(venv) $ python person.py --profile John 35
Hello, John! You're 35 years old!
(venv) $ python person.py --profile Jane 28.5
Usage: person.py [OPTIONS]
Try 'person.py --help' for help.
Error: Invalid value for '--profile': '28.5' is not a valid integer.
The --profile option accepts a string and an integer value. If you use a different data type, then youβll get an error. Click does the type validation for you.
Specifying an Option Multiple Times
Repeating an option multiple times at the command line is another cool feature that you can implement in your CLI apps with Click. As an example, consider the following toy app, which takes a --name option and displays a greeting. The app allows you to specify --name multiple times:
# greet.py
import click
@click.command()
@click.option("--name", multiple=True)
def cli(name):
for n in name:
click.echo(f"Hello, {n}!")
if __name__ == "__main__":
cli()
The multiple argument to @click.option is a Boolean flag. If you set it to True, then you can specify the underlying option multiple times.
Hereβs how this app works in practice:
(venv) $ python greet.py --name Pythonista --name World
Hello, Pythonista!
Hello, World!
In this command, you specify the --name option two times. Each time, you use a different input value. As a result, the application prints two greetings to your screen, one greeting per option repetition. Next up, youβll learn more about Boolean flags.
Defining Options as Boolean Flags
Boolean flags are options that you can enable or disable. Click accepts two types of Boolean flags. The first type allows you to define on and off switches. The second type only provides an on switch. To define a flag with on and off switches, you can provide the two flags separated by a slash (/).
As an example of an on and off flag, consider the following app:
# upper_greet.py
import click
@click.command()
@click.argument("name", default="World")
@click.option("--upper/--no-upper", default=False)
def cli(name, upper):
message = f"Hello, {name}!"
if upper:
message = message.upper()
click.echo(message)
if __name__ == "__main__":
cli()
In the highlighted line, you define an option that works as an on and off flag. In this example, --upper is the on (or True) switch, while --no-upper is the off (or False) switch. Note that the off flag doesnβt have to use the no- prefix. You can name it what you want, depending on your specific use case.
Then you pass upper as an argument to your cli() function. If upper is true, then you uppercase the greeting message. Otherwise, the message keeps its original casing. Note that the default value for this flag is False, which means that the app will display the message without changing its original casing.
Hereβs how this app works in practice:
(venv) $ python upper_greet.py Pythonista --upper
HELLO, PYTHONISTA!
(venv) $ python upper_greet.py Pythonista --no-upper
Hello, Pythonista!
(venv) $ python upper_greet.py Pythonista
Hello, Pythonista!
When you run your app with the --upper flag, you get the greeting in uppercase. When you run the app with the --no-upper flag, you get the message in its original casing. Finally, running the app without a flag displays the message without modification because the default value for the flag is False.
The second type of Boolean flag only provides an on, or True, switch. In this case, if you provide the flag at the command line, then its value will be True. Otherwise, its value will be False. You can set the is_flag argument of @click.option() to True when you need to create this type of flag.
To illustrate how you can use these flags, get back to your ls simulator. This time, youβll add an -l or --long flag that mimics the behavior of the equivalent flag in the original Unix ls command. Hereβs the updated code:
# ls.py v4
from datetime import datetime
from pathlib import Path
import click
@click.command()
@click.option("-l", "--long", is_flag=True)
@click.argument(
"paths",
nargs=-1,
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(paths, long):
for i, path in enumerate(paths):
if len(paths) > 1:
click.echo(f"{path}/:")
for entry in path.iterdir():
entry_output = build_output(entry, long)
click.echo(f"{entry_output:{len(entry_output) + 5}}", nl=long)
if i < len(paths) - 1:
click.echo("" if long else "\n")
elif not long:
click.echo()
def build_output(entry, long=False):
if long:
size = entry.stat().st_size
date = datetime.fromtimestamp(entry.stat().st_mtime)
return f"{size:>6d} {date:%b %d %H:%M:%S} {entry.name}"
return entry.name
if __name__ == "__main__":
cli()
Wow! There are several new things happening in this code. First, you define the -l or --long option as a Boolean flag by setting its is_flag argument to True.
Inside cli(), you update the loop to produce a normal or long output depending on the userβs choice. In the loop, you call the build_output() helper function to generate the appropriate output for each case.
The build_output() function returns a detailed output when long is True and a minimal output otherwise. The detailed output will contain the size, modification date, and name of an entry. To generate the detailed output, you use tools like Path.stat() and a datetime object with a custom string format.
With all this new code in place, you can give your custom ls app a try. Go ahead and run the following commands:
(venv) $ python ls.py -l sample/
2609 Jul 13 15:27:59 lorem.md
428 Jul 12 15:28:38 realpython.md
44 Jul 12 15:26:49 hello.txt
(venv) $ python ls.py -l sample/ sample_copy/
sample/:
2609 Jul 12 15:27:59 lorem.md
428 Jul 12 15:28:38 realpython.md
44 Jul 12 15:26:49 hello.txt
sample_copy/:
2609 Jul 12 15:27:18 lorem.md
428 Jul 12 15:28:48 realpython.md
44 Jul 12 15:27:18 hello.txt
(venv) $ python ls.py sample/ sample_copy/
sample/:
lorem.md realpython.md hello.txt
sample_copy/:
lorem.md realpython.md hello.txt
When you run the ls.py script with the -l flag, you get a detailed output of all the entries in the target directory. If you run it without the flag, then you get a short output.
Creating Feature Switches
In addition to Boolean flags, Click also supports what it calls feature switches. As the name suggests, this type of option allows you to enable or disable a given feature in your CLI apps. To define a feature switch, youβll have to create at least two options for the same parameter.
For example, consider the following update to your upper_greet.py app:
# upper_greet.py v2
import click
@click.command()
@click.argument("name", default="World")
@click.option("--upper", "casing", flag_value="upper")
@click.option("--lower", "casing", flag_value="lower")
def cli(name, casing):
message = f"Hello, {name}!"
if casing == "upper":
message = message.upper()
elif casing == "lower":
message = message.lower()
click.echo(message)
if __name__ == "__main__":
cli()
The new version of upper_greet.py has two options: --upper and --lower. Both of these options operate on the same parameter, "casing". You pass this parameter as an argument to the cli() function.
Inside cli(), you check the current value of casing and make the appropriate message transformation. If the user doesnβt provide one of these options at the command line, then the app will display the message using its original casing:
(venv) $ python upper_greet.py --upper
HELLO, WORLD!
(venv) $ python upper_greet.py --lower
hello, world!
(venv) $ python upper_greet.py
Hello, World!
The --upper switch allows you to enable the uppercasing feature. Similarly, the --lower switch lets you use the lowercasing feature of your app. If you run the app with no switch, then you get the message with its original casing.
Itβs important to note that you can make one of the switches the default behavior of your app by setting its default argument to True. For example, if you want the --upper option to be the default behavior, then you can add default=True to the optionβs definition. In the above example, you didnβt do this because printing the message using its original casing seems to be the appropriate and less surprising behavior.
Getting an Optionβs Value From Multiple Choices
Click has a parameter type called Choice that allows you to define an option with a list of allowed values to select from. You can instantiate Choice with a list of valid values for the option at hand. Click will take care of checking whether the input value that you provide at the command line is in the list of allowed values.
Hereβs a CLI app that defines a choice option called --weekday. This option will accept a string with the target weekday:
# days.py
import click
@click.command()
@click.option(
"--weekday",
type=click.Choice(
[
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
]
),
)
def cli(weekday):
click.echo(f"Weekday: {weekday}")
if __name__ == "__main__":
cli()
In this example, you use the Choice class to provide the list of weekdays as strings. When your user runs this app, theyβll have to provide a weekday that matches one of the values in the list. Otherwise, theyβll get an error:
(venv) $ python days.py --weekday Monday
Weekday: Monday
(venv) $ python days.py --weekday Wednesday
Weekday: Wednesday
(venv) $ python days.py --weekday FRIDAY
Usage: days.py [OPTIONS]
Try 'days.py --help' for help.
Error: Invalid value for '--weekday': 'FRIDAY' is not one of 'Monday',
'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'.
The first two examples work as expected because the input values are in the list of allowed values. However, when you use FRIDAY in uppercase, you get an error because this value with that specific casing isnβt in the list.
You have the possibility of working around this casing issue by setting the case_sensitive argument to False when you instantiate the Choice parameter type.
Getting Options From Environment Variables
Another exciting feature of Click options is that they can retrieve their values from environment variables. This feature can be pretty useful and may have several use cases.
For example, say that youβre creating a CLI tool to consume a REST API. In this situation, you may need a secret key to access the API. One way to handle this key is by exporting it as an environment variable and making your app read it from there.
In the example below, you write a CLI app to retrieve cool space pictures and videos from NASAβs main API page. To access this API, your application needs an API key that you can store in an environment variable and retrieve with Click automatically:
# nasa.py
import webbrowser
import click
import requests
BASE_URL = "https://api.nasa.gov/planetary"
TIMEOUT = 3
@click.command()
@click.option("--date", default="2021-10-01")
@click.option("--api-key", envvar="NASA_API_KEY")
def cli(date, api_key):
endpoint = f"{BASE_URL}/apod"
try:
response = requests.get(
endpoint,
params={
"api_key": api_key,
"date": date,
},
timeout=TIMEOUT,
)
except requests.exceptions.RequestException as e:
print(f"Error connecting to API: {e}")
return
try:
url = response.json()["url"]
except KeyError:
print(f"No image available on {date}")
return
webbrowser.open(url)
if __name__ == "__main__":
cli()
In this example, you import webbrowser from the Python standard library. This module allows you to quickly open URLs in your default browser. Then you import the requests library to make HTTP requests to the target REST API.
Note: To learn more about accessing REST APIs in your code, check out Python & APIs: A Winning Combo for Reading Public Data.
In the highlighted line, you create the --api_key option and set its envvar argument to "NASA_API_KEY". This string represents the name that youβll use for the environment variable where youβll store the API key.
In the cli() function, you make an HTTP request to the /apod endpoint, get the target URL, and finally open that URL in your default browser.
Note: The /apod endpointβs name is an acronym that comes from astronomy picture of the day.
To give the above CLI app a try, go ahead and run the commands below. Note that in these commands, youβll use "DEMO_KEY" to access the API. This key has rate limits. So, if you want to create your own key, then you can do it on the API page:
(venv) $ export NASA_API_KEY="DEMO_KEY"
(venv) $ python nasa.py --date 2023-06-05
With the first command, you export the target environment variable. The second command runs the app. Youβll see how your browser executes and shows an incredible image from space. Go ahead and play with different dates to retrieve some other amazing universe views.
Itβs important to note that you can also provide the key at the command line by explicitly using the --api-key option as usual. This comes in handy in situations where the environment variable is undefined.
Prompting the User for Input Values
Prompting the user for input is a pretty common requirement in CLI applications. Prompts can considerably improve your userβs experience when they work with your app. Fortunately, Click has you covered with prompts as well.
With Click, you can create at least the following types of prompts:
- Input prompts
- Password prompts
- Confirmation prompts
You can create user prompts by using either the prompt argument to @click.option or the click.prompt() function. Youβll also have dedicated decorators, such as @click.password_option and @click.confirmation_option, to create password and confirmation prompts.
For example, say that you need your application to get the user name and password at the command line to perform some restricted actions. In this case, you can take advantage of input and password prompts:
# user.py
import click
@click.command()
@click.option("--name", prompt="Username")
@click.option("--password", prompt="Password", hide_input=True)
def cli(name, password):
if name != read_username() or password != read_password():
click.echo("Invalid user credentials")
else:
click.echo(f"User {name} successfully logged in!")
def read_password():
return "secret"
def read_username():
return "admin"
if __name__ == "__main__":
cli()
The --name option has a regular input prompt that you define with the prompt argument. The --password option has a prompt with the additional feature of hiding the input. This behavior is perfect for passwords. To set this new feature, you use the hide_input flag.
If you run this application from your command line, then youβll get the following behavior:
(venv) $ python user.py
Username: admin
Password:
User admin successfully logged in!
As youβll notice, the Username prompt shows the input value on the screen. In contrast, the Password prompt hides the input as you type, which is an appropriate behavior for password input.
When youβre working with passwords, allowing the user to change their password may be a common requirement. In this scenario, you can use the @click.password_option decorator. This decorator allows you to create a password option that hides the input and asks for confirmation. If the two passwords donβt match, then you get an error, and the password prompt shows again.
Hereβs a toy example of how to change the password of a given user:
# set_password.py
import click
@click.command()
@click.option("--name", prompt="Username")
@click.password_option("--set-password", prompt="Password")
def cli(name, set_password):
# Change the password here...
click.echo("Password successfully changed!")
click.echo(f"Username: {name}")
click.echo(f"Password: {set_password}")
if __name__ == "__main__":
cli()
Using @click.password_option, you can create a password prompt that automatically hides the input and asks for confirmation. In this example, you create a --set-password, which does exactly that. Hereβs how it works in practice:
(venv) $ python set_password.py
Username: admin
Password:
Repeat for confirmation:
Error: The two entered values do not match.
Password:
Repeat for confirmation:
Password successfully changed!
Username: admin
Password: secret
In the first attempt to change the password, the initial input and the confirmation didnβt match, so you got an error. The prompt shows again to allow you to enter the password again. Note that the prompt will appear until the two provided passwords match.
You can manually ask users for input. To do this, you can use the prompt() function. This function takes several arguments that allow you to create custom prompts and use them in other parts of your code, separate from where you defined the options.
For example, say that you want to create a command that adds two numbers together. In this case, you can have two custom prompts, one for each input number:
# add.py
import click
@click.command()
def cli():
a = click.prompt("Enter an integer", type=click.INT, default=0)
b = click.prompt("Enter another integer", type=click.INT, default=0)
click.echo(f"{a} + {b} = {a + b}")
if __name__ == "__main__":
cli()
In this example, you create two input prompts inside cli() using the prompt() function. The first prompt asks for the a value, while the second prompt asks for the b value. Both prompts will check that the input is a valid integer number and will show an error if not. If the user doesnβt provide any input, then they can accept the default value, 0, by pressing Enter.
Hereβs how this app works:
(venv) $ python add.py
Enter an integer [0]: 42.0
Error: '42.0' is not a valid integer.
Enter an integer [0]: 42
Enter another integer [0]: 7
42 + 7 = 49
In the first input attempt, you enter a floating-point number. Click checks the input for you and displays an error message. Then you enter two valid integer values and get a successful result.
Click also provides a function called confirm(). This function comes in handy when you need to ask the user for confirmation to proceed with a sensitive action, such as deleting a file or removing a user.
The confirm() function prompts for confirmation with a yes or no question:
# remove.py
import click
@click.command()
@click.option("--remove-user")
def cli(remove_user):
if click.confirm(f"Remove user '{remove_user}'?"):
click.echo(f"User {remove_user} successfully removed!")
else:
click.echo("Aborted!")
if __name__ == "__main__":
cli()
The confirm() function returns a Boolean value depending on the userβs response to the yes or no confirmation question. If the userβs answer is yes, then you run the intended action. Otherwise, you abort it.
Hereβs an example of using this app:
(venv) $ python remove.py --remove-user admin
Remove user 'admin'? [y/N]:
Aborted!
(venv) $ python remove.py --remove-user john
Remove user 'john'? [y/N]: y
User john successfully removed!
In the first example, you accept the default answer, N for no, by pressing Enter as a reply to the prompt. Note that in CLI apps, youβll often find that the default option is capitalized as a way to indicate that itβs the default. Click follows this common pattern in its prompts too.
In the second example, you explicitly respond yes by entering y and pressing Enter. The app acts according to your response, either aborting or running the action.
Providing Parameter Types for Arguments and Options
In CLI development, arguments and options can take concrete input values at the command line. Youβve already learned that Click has some custom parameter types that you can use to define the type of input values. Using these parameter types, you can have type validation out of the box without writing a single line of code.
Here are some of the most relevant parameter types available in Click:
| Parameter Type | Description |
|---|---|
click.STRING |
Represents Unicode strings and is the default parameter type for arguments and options |
click.INT |
Represents integer numbers |
click.FLOAT |
Represents floating-point numbers |
click.BOOL |
Represents Boolean values |
Apart from these constants that represent primitive types, Click also has some handy classes that you can use to define other types of input values. Youβve already learned about the click.Path, click.File, click.Choice, and click.Tuple classes in previous sections. In addition to these classes, Click also includes the following:
| Parameter Type Class | Description |
|---|---|
click.IntRange |
Restricts the input value to a range of integer numbers |
click.FloatRange |
Restricts the input value to a range of floating-point numbers |
click.DateTime |
Converts date strings into datetime objects |
With all these custom types and classes, you can make your Click apps more robust and reliable. Theyβll also make you more productive because you wonβt have to implement type validation logic for your appβs input values. Click does the hard work for you.
However, if you have specific validation needs, then you can create a custom parameter type with your own validation strategies.
Creating Subcommands in Click
Nested commands, or subcommands, are one of the most powerful and distinctive features of Click. Whatβs a subcommand anyway? Many command-line applications, such as pip, pyenv, Poetry, and git, make extensive use of subcommands.
For example, pip has the install subcommand that allows you to install Python packages and libraries in a given environment. You used pip install click at the beginning of this tutorial to install the Click library, for example.
Similarly, the git application has many subcommands, such as pull, push, and clone. Youβll likely find several examples of CLI applications with subcommands in your daily workflow because subcommands are pretty useful in real-world apps.
In the following section, youβll learn how to add subcommands to your Click applications using the @click.group decorator. Youβll learn about two common approaches for creating subcommands:
- Registering subcommands right away, which is appropriate when you have a minimal app in a single file
- Deferring subcommand registration, which comes in handy when you have a complex app whose commands are distributed among multiple modules
Before you dive into the meat of this section, itβs important to note that, in this tutorial, youβll only scratch the surface of Clickβs subcommands. However, youβll learn enough to get up and running with them in your CLI apps.
Registering Subcommand Right Away
To illustrate the first approach to creating subcommands in Click applications, say that you want to create a CLI app with four subcommands representing arithmetic operations:
addfor adding two numbers togethersubfor subtracting two numbersmulfor multiplying two numbersdivfor dividing two numbers
To build this app, you start by creating a file called calc.py in your working directory. Then you create a command group using the @click.group decorator as in the code below:
# calc.py v1
import click
@click.group()
def cli():
pass
In this piece of code, you create a command group called cli by decorating the cli() function with the @click.group decorator.
In this specific example, the cli() function provides the entry point for the appβs CLI. It wonβt run any concrete operations. Thatβs why it only contains a pass statement. However, other applications may need to take arguments and options in cli(), which you can implement as usual.
With the command group in place, you can start to add new subcommands right away. To do this, you use a decorator built using the groupβs name plus the command() function. For example, in the code below, you create the add command:
# calc.py v1
# ...
@cli.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def add(a, b):
click.echo(a + b)
if __name__ == "__main__":
cli()
In this code snippet, the decorator to create the add command is @cli.command rather than @click.command. This way, youβre telling Click to attach the add command to the cli group.
At the end of the file, you place the usual name-main idiom to call the cli() function and start the CLI. Thatβs it! Your add subcommand is ready for use. Go ahead and run the following command:
(venv) $ python calc.py add 3 8
11.0
Cool! Your add subcommand works as expected. It takes two numbers and adds them together, printing the result to your screen as a floating-point number.
As an exercise, you can implement the subcommands for the subtraction, multiplication, and division operations. Expand the collapsible section below to see the complete solution:
Hereβs the complete implementation for your calc.py application:
# calc.py v1
import click
@click.group()
def cli():
pass
@cli.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def add(a, b):
click.echo(a + b)
@cli.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def sub(a, b):
click.echo(a - b)
@cli.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def mul(a, b):
click.echo(a * b)
@cli.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def div(a, b):
click.echo(a / b)
if __name__ == "__main__":
cli()
In all the new subcommands, you follow the same pattern as add. You attach each one to the cli group and then define its arguments and specific arithmetic operation. That was a nice coding experience, wasnβt it?
Once youβve written the rest of the operations as subcommands, then go ahead and give them a try from your command line. Great, they work! But what if you donβt have all your subcommands ready right off the bat? In that case, you can defer your subcommand registration, as youβll learn next.
Deferring Subcommand Registration
Instead of using the @group_name.command() decorator to add subcommands on top of a command group right away, you can use group_name.add_command() to add or register the subcommands later.
This approach is suitable for those situations where you have your commands spread into several modules in a complex application. It can also be useful when you need to build the CLI dynamically based on some configuration loaded from a file, for example.
Say that you refactor your calc.py application from the previous section, and now it has the following structure:
calc/
βββ calc.py
βββ commands.py
In this directory tree diagram, you have the calc.py and commands.py files. In the latter file, youβve put all your commands, and it looks something like this:
# commands.py
import click
@click.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def add(a, b):
click.echo(a + b)
@click.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def sub(a, b):
click.echo(a - b)
@click.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def mul(a, b):
click.echo(a * b)
@click.command()
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def div(a, b):
click.echo(a / b)
Note that in this file, youβve defined the commands using the @click.command decorator. The rest of the code is the same code that you used in the previous section. Once you have this file with all your subcommands, then you can import them from calc.py and use the .add_command() method to register them:
# calc.py v2
import click
import commands
@click.group()
def cli():
pass
cli.add_command(commands.add)
cli.add_command(commands.sub)
cli.add_command(commands.mul)
cli.add_command(commands.div)
if __name__ == "__main__":
cli()
In the calc.py file, you first update the imports to include the commands module, which provides the implementations for your appβs subcommands. Then you use the .add_command() method to register those subcommands in the cli group.
If you give this new version of your application a try, then youβll note that it works the same as its first version:
(venv) $ python calc/calc.py add 3 8
11.0
To run the app, you need to provide the new path because the appβs entry-point script now lives in the calc/ folder. As you can see, the functionality of calc.py remains the same. Youβve only changed the internal organization of your code.
In general, youβll use .add_command() to register subcommands when your CLI application is made of multiple modules and your commands are spread throughout those modules. For basic apps with limited functionalities and features, you can register the commands right away using the @group_name.command decorator, as you did in the previous section.
Tweaking Usage and Help Messages in a Click App
For CLI applications, itβs crucial that you provide detailed documentation on how to use them. CLI apps donβt have a graphical user interface for the user to interact with the app. They only have commands, arguments, and options, which are generally hard to memorize and learn. So, you have to carefully document all these commands, arguments, and options so that your users can use them.
Click has you covered in this aspect too. It provides convenient tools that allow you to fully document your apps, providing detailed and user-friendly help pages for them.
In the following sections, youβll learn how to fully document your CLI app using Click and some of its core features. To kick things off, youβll start by learning how to document commands and options.
Documenting Commands and Options
Clickβs commands and options accept a help argument that you can use to provide specific help messages for them. Those messages will show when you run the app with the --help option. To illustrate, get back to the most recent version of your ls.py script and check its current help page:
(venv) $ python ls.py --help
Usage: ls.py [OPTIONS] [PATHS]...
Options:
-l, --long
--help Show this message and exit.
This help page is nice as a starting point. Its most valuable characteristic is that you didnβt have to write a single line of code to build it. The Click library automatically generates it for you. However, you can tweak it further and make it more user-friendly and complete.
To start off, go ahead and update the code by adding a help argument to the @click.command decorator:
# ls.py v5
import datetime
from pathlib import Path
import click
@click.command(help="List the content of one or more directories.")
@click.option("-l", "--long", is_flag=True)
@click.argument(
"paths",
nargs=-1,
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(paths, long):
# ...
In the highlighted line, you pass a help argument containing a string that provides a general description of what the underlying command does. Now go ahead and run the app with the --help option again:
(venv) $ python ls.py --help
Usage: ls.py [OPTIONS] [PATHS]...
List the content of one or more directories.
Options:
-l, --long
--help Show this message and exit.
The appβs help page looks different now. It includes a general description of what the application does.
Itβs important to note that when it comes to help pages for commands, the docstring of the underlying function will produce the same effect as the help argument. So, you can remove the help argument and provide a docstring for the cli() function to get an equivalent result. Go ahead and give it a try!
Note: If your app has multiple subcommands, then Click will add a Commands section to the help page and list each command. If you add help messages to all your subcommands, theyβll appear beside the commandβs name.
As an exercise, you can add help messages to the subcommands of your calc.py app. Hereβs how the appβs help page could look:
(venv) $ python calc.py --help
Usage: calc.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
add Add two numbers.
div Divide two numbers.
mul Multiply two numbers.
sub Subtract two numbers.
On this help page, each subcommand shows its own help message, which is pretty helpful from the userβs perspective.
Now update the --long option as in the code below to provide a descriptive help message:
# ls.py v6
import datetime
from pathlib import Path
import click
@click.command(help="List the content of one or more directories.")
@click.option(
"-l",
"--long",
is_flag=True,
help="Display the directory content in long format.",
)
@click.argument(
"paths",
nargs=-1,
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(paths, long):
# ...
In the definition of the --long option, you include the help argument with a description of what this specific option does. Hereβs how this change affects the appβs help page:
(venv) $ python ls.py --help
Usage: ls.py [OPTIONS] [PATHS]...
List the content of one or more directories.
Options:
-l, --long Display the directory content in long format.
--help Show this message and exit.
The --long option now includes a nice description that tells the user what its purpose is. Thatβs great!
Documenting Arguments
Unlike @click.command and @click.option, the click.argument() decorator doesnβt take a help argument. As the Click documentation says:
This [the absence of a
helpargument] is to follow the general convention of Unix tools of using arguments for only the most necessary things, and to document them in the command help text by referring to them by name. (Source)
So, how can you document a command-line argument in a Click application? Youβll use the docstring of the underlying function. Yes, it sounds weird. Commands also use that docstring. So, youβll have to refer to arguments by their names. For example, hereβs how youβd document the PATHS argument in your ls.py app:
# ls.py v6
import datetime
from pathlib import Path
import click
@click.command()
@click.option(
"-l",
"--long",
is_flag=True,
help="Display the directory content in long format.",
)
@click.argument(
"paths",
nargs=-1,
type=click.Path(
exists=True,
file_okay=False,
readable=True,
path_type=Path,
),
)
def cli(paths, long):
"""List the content of one or more directories.
PATHS is one or more directory paths whose content will be listed.
"""
# ...
In this updated version of ls.py, you first remove the help argument from the commandβs definition. If you donβt do this, then the docstring wonβt work as expected because the help argument will prevail. The docstring of cli() includes the original help message for the command. It also has an additional line that describes what the PATHS argument represents. Note how youβve referred to the argument by its name.
Hereβs how the help page looks after these updates:
(venv) $ python ls.py --help
Usage: ls.py [OPTIONS] [PATHS]...
List the content of one or more directories.
PATHS is one or more directory paths whose content will be listed.
Options:
-l, --long Display the directory content in long format.
--help Show this message and exit.
This help page is looking good! Youβve documented the appβs main command and the PATHS arguments using the docstring of the underlying cli() function. Now the appβs help page provides enough guidance for the user to use it effectively.
Preparing a Click App for Installation and Use
When you dive into building CLI applications with Click, you quickly note that the official documentation recommends switching from the name-main idiom to setuptools. Using setuptools is the preferred way to install, develop, work with, and even distribute Click apps.
In this tutorial, youβve used the idiom approach in all the examples to have a quick solution. Now itβs time to do the switch into setuptools. In this section, youβll use the calc.py app as the sample project, and youβll start by creating a proper project layout for the app.
Create a Project Layout for Your CLI App
Youβll use the calc.py script as the sample app to get into the setuptools switching. As a first step, you need to organize your code and lay out your CLI project. In the process, you should observe the following points:
- Create modules and packages to organize your code.
- Name the core package of a Python app after the app itself.
- Name each Python module according to its specific content or functionality.
- Add a
__main__.pymodule to any Python package thatβs directly executable.
With these ideas in mind, you can use the following directory structure for laying out your calc project:
calc/
β
βββ calc/
β βββ __init__.py
β βββ __main__.py
β βββ commands.py
β
βββ pyproject.toml
βββ README.md
The calc/ folder is the projectβs root directory. In this directory, you have the following files:
pyproject.tomlis a TOML file that specifies the projectβs build system and many other configurations. In modern Python, this file is a sort of replacement for thesetup.pyscript. So, youβll usepyproject.tomlinstead ofsetup.pyin this example.README.mdprovides the project description and instructions for installing and running the application. Adding a descriptive and detailedREADME.mdfile to your projects is a best practice in programming, especially if youβre planning to publish the project to PyPI as an open-source solution.
Then you have the calc/ subdirectory, which holds the appβs core package. Hereβs a description of its content:
__init__.pyenablescalc/as a Python package. In this example, this file will be empty.__main__.pyprovides the applicationβs entry-point script or executable file.commands.pyprovides the applicationβs subcommands.
In the following collapsible sections, youβll find the content of the __main__.py and commands.py files:
Hereβs the source code for the __main__.py file:
# __main__.py
import click
from . import commands
@click.group()
def cli():
pass
cli.add_command(commands.add)
cli.add_command(commands.sub)
cli.add_command(commands.mul)
cli.add_command(commands.div)
Compared to the previous version of calc.py, the __main__.py file uses a relative import to grab the commands module from the containing package, calc. Youβve also removed the name-main idiom from the end of the file.
Hereβs the source code for the commands.py file:
# commands.py
import click
@click.command(help="Add two numbers.")
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def add(a, b):
click.echo(a + b)
@click.command(help="Subtract two numbers.")
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def sub(a, b):
click.echo(a - b)
@click.command(help="Multiply two numbers.")
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def mul(a, b):
click.echo(a * b)
@click.command(help="Divide two numbers.")
@click.argument("a", type=click.FLOAT)
@click.argument("b", type=click.FLOAT)
def div(a, b):
click.echo(a / b)
This file has the same content as your original commands.py file.
With the project layout in place, youβre now ready to write a suitable pyproject.toml file and get your project ready for development, use, and even distribution.
Write a pyproject.toml File for Your Click Project
The pyproject.toml file allows you to define the appβs build system as well as many other general configurations. Hereβs a minimal example of how to fill in this file for your sample calc project:
# pyproject.toml
[build-system]
requires = ["setuptools>=65.5.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "calc"
version = "0.1.0"
description = "A CLI application that performs arithmetic operations."
readme = "README.md"
authors = [{ name = "Real Python", email = "info@realpython.com" }]
dependencies = [
"click >= 8.1.3",
]
[project.scripts]
calc = "calc.__main__:cli"
The [build-system] table header defines setuptools as your appβs build system and specifies the dependencies for building the application.
The [project] header allows you to provide general metadata for the application. This metadata may include many key-value pairs, including the appβs name, version, general description, and so on.
The dependencies key is quite important and convenient. Through this key, you can list all the projectβs dependencies and their target versions. In this example, the only dependency is Click, and youβre using a version greater than or equal to 8.1.3. The projectβs build system will take that list and automatically install all of its items.
Finally, in the [project.scripts] heading, you define the applicationβs entry-point script, which is the cli() function in the __main__.py module in this example. With this final setup in place, youβre ready to give the app a try. To do this, you should first create a dedicated virtual environment for your calc project.
Create a Virtual Environment and Install Your Click App
You already learned how to create a Python virtual environment. So, go ahead and open a terminal window. Then navigate to your calc projectβs root folder. Once youβre in there, run the following commands to create a fresh environment:
Great! You have a fresh virtual environment within your projectβs folder. To install the application in there, youβll use the -e option of pip install. This option allows you to install packages, libraries, and tools in editable mode.
Editable mode lets you work on the source code while being able to try the latest modifications as you implement them. This mode is quite useful in the development stage.
Hereβs the command that you need to run to install calc:
(venv) $ python -m pip install -e .
Once youβve run this command, then your calc app will be installed in your current Python environment. To check this out, go ahead and run the following command:
(venv) $ pip list
Package Version
----------------- -------
calc 0.1.0
click 8.1.6
...
The pip list command lists all the currently installed packages in a given environment. As you can see in the above output, calc is installed. Another interesting point is that the project dependency, click, is also installed. Yes, the dependencies key in your projectβs pyproject.toml file did the magic.
From within the projectβs dedicated virtual environment, youβll be able to run the app directly as a regular command:
(venv) $ calc add 3 4
7.0
(venv) $ calc sub 3 4
-1.0
(venv) $ calc mul 3 4
12.0
(venv) $ calc div 3 4
0.75
(venv) $ calc --help
Usage: calc [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
add Add two numbers.
div Divide two numbers.
mul Multiply two numbers.
sub Subtract two numbers.
Your calc application works as a regular command now. Thereβs a detail that you should note on the appβs help page at the end of the examples above. The Usage line now shows the appβs name, calc, instead of the Python filename, calc.py. Thatβs great!
You can try to extend the appβs functionalities and maybe add more complex math operations as an exercise. Go ahead and give it a try!
Conclusion
Now you have a broad understanding of how the Click library works in Python. You know how to use it to create powerful command-line interfaces for small or large applications and automation tools. With Click, you can quickly create apps that provide arguments, options, and subcommands.
Youβve also learned how to tweak your appβs help page, which can fundamentally improve your user experience.
In this tutorial, youβve learned how to:
- Build command-line interfaces for your apps using Click and Python
- Support arguments, options, and subcommands in your CLI apps
- Enhance the usage and help pages of your CLI apps
- Prepare a Click app for installation, development, use, and distribution
The Click library provides a robust and mature solution for creating extensible and powerful CLIs. Knowing how to use this library will allow you to write effective and intuitive command-line applications, which is a great skill to have as a Python developer.
Get Your Code: Click here to download the sample code that youβll use to build your CLI app with Click and Python.



