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