Click: a beautiful python library to write CLI applications

An image of a terminal running top command
An image of my terminal running top command
  • cleo: a CLI toolkit used by poetry, a python dependency manager (oh, I have a nice introduction to poetry here).
  • cliff: The CLI toolkit created by the Openstack team and from which all their command line utilities are created. It has a nice oriented object approach for writing CLI.
  • docopt: An old competitor to click which parses docstring to dynamically create CLI.
  • fire: An unofficial google project using objects to dynamically create CLI.

Basic concepts


Click supports two types of parameters: options and arguments. Parameters in click have different types to handle numbers, file, datetime, etc.. The complete list can be find on this page. You can also define custom parameter types, I will show an example of how to do it afterwards.


Arguments are less powerful than options. There are often used to specify file paths and urls. They cannot be documented like options in the click parameter constructor. The only way to document it is in the function docstring. Example usage:

import click

@click.argument('input_file', type=click.Path(dir_okay=False))
def cli(input_file):
"""INPUT_FILE represents the path to a file on your file tree"""


Options on the other side are full of features including:

  • Automatic prompting for missing input
  • Act as flags
  • Pulling values from environment
  • Documented via the click parameter constructor.


To install click, you can use pip.

pip install click

Project setup

To learn how to use click, we will create a project click_tutorial. So create a folder of the same name somewhere on your home folder. Inside it, run “poetry init”, this will help you to setup the pyproject.toml file interactively. When you get to the main dependencies definition, just install click. And for the dev dependencies section, install pytest.

name = "click_tutorial"
version = "0.1.0"
description = "A tutorial about using click"
authors = ["le_woudar <XXX@XX>"]
license = "MIT"

python = "^3.7"
click = "^7.1.2"

pytest = "^6.1.2"

requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
name = "click_tutorial"
version = "0.1.0"
description = "A tutorial about using click"
authors = ["le_woudar <>"]
license = "MIT"

packages = [
{ include = "scripts" }

python = "^3.7"


Our first CLI: hello

Yeah let’s start with something traditional in the programming world, I named “hello world”. In our case we will greet the user entering his name. Copy the following content in a file inside the scripts package.

import click

@click.option('-n', '--name', prompt='Your name', help='Name to greet')
def cli(name):
"""Greets a user who gives his name as input"""
click.echo(f'Hello {name}!')
hello = "scripts.hello:cli"
$ hello --help
Usage: hello [OPTIONS]
Greets a user who gives his name as inputOptions:
-n, --name TEXT Name to greet
  • You have an automatic “ — help” option which prints the docstring of the function as the CLI documentation.
  • Options are documented automatically when printing CLI help. The “help” argument passed in the option constructor is used like you can see. Also when defining the option, we didn’t specify a type, but you can see TEXT printed. So, note that if you don’t specify the type of an option or argument, it will be a string (aka TEXT) by default.
$ hello -n kevin
Hello kevin!
$ hello --name=kevin
Hello kevin!
$ hello
Your name: kevin
Hello kevin!
  • You can pass option using the short syntax “-n” or the long syntax “ — name” as we defined it in the parameter constructor.
  • If you don’t pass the name option, you are automatically prompted to do it. Remember the prompt argument used in the option constructor with the value “Your name”. This is one of the features of the options.
  • For details about option naming, I recommend you to read this short paragraph of click documentation.

Second example: clone of cat command

For our second example. We will create a simple version of the well known unix cat command. Create a new file “” in the scripts package with the following content (tip: click on “view raw” at the end of the github code snippet if you want to copy paste the content).

$ pycat
Usage: pycat [OPTIONS] FILE
Try 'pycat --help' for help.
Error: Missing argument 'FILE'.$ pycat hello.txt
Hello world!
  • We have an example usage of a click argument. The type used is a click.File which opens the given filename in read mode by default. The file is automatically closed when the function finishes its execution. This is why you see I don’t use the close method in the definition of the function.
  • If you don’t specify a filename when calling the CLI you have an error message, the same if you pass a file that doesn’t exist. Click handles these common mistakes automatically. And if you pass a correct file (replace “hello.txt” in my example by a file on your computer), you will have its content displayed.
  • Now we add an option “-n” which acts as a boolean flag, you can guess it with the argument is_flag. This means that if you don’t pass this option when using pycat, the behaviour will be the same as before. If you use it like this “pycat -n <file>” then you will have the number of line printed just before the line itself.
  • As an exercise, you can try to implement the remaining options of the cat command.

Test click CLI

Like any software we are writing, we need to write tests when developing our CLI. For this click provides a test runner to check output, return code or exception. Create a tests package near scripts and inside it create a file with the following content:

Snippet code of pycat command
  • Line 1, we import the CliRunner useful for testing CLI applications
  • Line 6, we use the default pytest fixture tmp_path which is a Path object representing a temporary directory. If you don’t know about pytest or fixtures, I recommend to read its documentation or at least this section.
  • Line 10, we invoke our CLI by passing arguments as a list as a second parameter. If you wonder about the resolve method, it is just to have the full path of the temporary file created.
  • Line 12, as you can see, the default exit code when all goes well is 0. It will be different if there is an error.
  • Line 13, note the “\n” at the end of the text. This is because click.echo by default adds a new line when printing text, just like the builtin print function does 😉
  • As an exercise, you can also test when adding the flag “-n” to the command. Tip: you can use the parametrize feature to don’t have to create a second test, you will just need to adapt what is passed to the invoke method and the output tested at the end.
  • For more information about testing, you can read this section of click documentation.

Third example: less command

At this point you should know how to install a script in your project, so I won’t repeat it anymore. I’ll just display the code and make a few comments. This time, we will implement the less command. This command is useful when you have a long text to print. Do not name the CLI less to avoid clashing with the existant command, call it for example pyless.

Snippet code of pyless command
$ pyless lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In est velit, lobortis ut magna vel, venenatis ultrices magna. Maecenas accumsan mi ut metus vehicula, sed facilisis enim sollicitudin. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse quis tellus scelerisque, semper risus sed, efficitur erat. Proin semper quis leo iaculis sodales. Mauris in diam tortor. Aenean condimentum felis odio, non consequat nisi lacinia eu.
... # use the down arrow or Enter key to continue reading the text
  • If you want to test the CLI and see the effect, you will need to read a long file or drastically reduce the size of your terminal window. I use the lorem ipsum generator to help me .😄
  • All the logic of the pager is done by click.echo_via_pager, you can read its api documentation if you are curious.

Fourth example: wc command

This time, we will implement the wc command.

Snippet code of pywc command
$ pywc scripts/ 
17 55 512 scripts/
$ pywc -wl scripts/
17 55 scripts/
  • I use the click.Path type for the argument. I could always have used the click.File but I wanted to show you an example usage of click.Path and open_file helper function. The “open_file” helper is a tiny wrapper around the open builtin function with additional features to handle stdin and stdout, atomic writes, etc.. The api documentation can be found here.
  • I’ll let you understand how the script works, it shouldn’t be too difficult. I just put here the links to functions you might not know, I’m thinking of any and re.split.

Fifth example: A reverse pointer address helper

If you recall at the beginning of the article, I said I would show you how to implement a custom parameter type, well the time has come! And you’ll see that it’s pretty easy.

Snippet code of ptr command
$ ptr
$ ptr 2001:db8::1
$ ptr 4
Try 'ptr --help' for help.
Error: Invalid value for 'IP_ADDRESS': 4 is not a valid ip address
  • Lines 5 to 12, we define the custom parameter type. We just need to inherit from click.ParamType and redefine the convert method. It takes the value, the corresponding parameter object and a context which holds the state of the CLI. The last two arguments are generally only useful for error messages.
  • Line 10, we try to convert the given value to an ip address object and if it things go wrong, we return an error message line 12 passing the parameter and context.
  • As a little advertising, I have a project implementing a lot of custom parameter types. The documentation is available here.

Sixth example: A simple implementation of an HTTP client

In this example, we will see how to implement a simple HTTP client like curl or more specifically in our case like httpie. The latter is a well-known CLI in python, if you don’t know it and deals with HTTP in your terminal daily, I recommend you to check it, it is really well done.

$ poetry add httpx pygments
$ httpx get -q foo:bar -q hello:world
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 325
Content-Type: application/json
Date: Tue, 01 Dec 2020 19:18:52 GMT
Server: gunicorn/19.9.0
"args": {
"foo": "bar",
"hello": "world"
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "",
"User-Agent": "HTTPie/2.0.0",
"X-Amzn-Trace-Id": "Root=1-5fc6979b-38e769ff191d5526046ef5f4"
"origin": "",
"url": ""
$ httpx --no-verify get -q foo:bar -q hello:world
# again we need to have the same answer as before, the difference is that we use https instead of http and we don't want to check the certificate, hence the "--no-verify" option
  • As you can see in the first use case, we must be able to pass an option multiple times, in this case “-q” which represents a query parameter. For that the option constructor has a boolean attribute multiple to allow it.
  • The value of the option is in the form “key:value” so we need to parse it to get the values separately and print errors if there are any.
  • In the last use case, we use a flag option “ — no-verify”. Note that this attribute is placed directly after httpx command, not after the get subcommand. But we will need this information in the subcommand, so how do we do? The answer is the click context object. It is an implicit object created by click when invoking a CLI function which holds some state about the application executed. It has a special obj attribute where we can store arbitrary data and access it in subcommands via the use of the pass_obj decorator. You will see an example below.
  • is a website created by requests author to help developers to test efficiently their http client.
  • Fun fact: the creator of httpx also created a CLI called httpx. 😆
snippet code of httpx command
  • If you test this code, you will have the expected behaviour for the get subcommand. I also implemented the post subcommand. Again this is not a complete implementation, for example the json data only handles strings, not other types support by the json standard library.
  • The file contains a function to pretty print HTTP data in the terminal. If you don’t understand it, I recommend to go through the pygments documentation.
  • In the main file, lines 14 to 18, we define a function to parse an option value and raises click exception if it is not correct.
  • Lines 21 to 44, we define functions to get all http attributes we care about like headers, cookies, query parameters, etc.. I hope you are familiar with type annotations, if you are not, you should. It improves code readibility and if you have the right tools like mypy or IDE like Pycharm that understand annotations, it can prevent you from some common typing bugs.
  • To declare a CLI that will contain subcommand, we decorate the main callback with line 47.
  • Line 49, we pass the click context for this CLI and at line 52 we instantiate the RequestData class holding some state we need in the subcommands. The resulting object is passed to the obj attribute of the context.
  • Line 55, note that the subcommand is defined with cli.command and not click.command. Yeah we use the main callback which is transformed by decorator.
  • Line 60, I use the click.pass_obj decorator to pass the object created at line 52.
  • For the rest, I leave it to your understanding this shouldn’t be too difficult.
  • As an exercise, you can try to implement other subcommands like patch, put, etc... You can also add authentication (basic, digest). Complete the handling of json data, etc… Don’t forget that the best way to learn is to practice. 😉

Bonus: Image downloader

As a final example, we will implement a CLI taken a bunch of images written in a file as input and downloading them in a directory of our choice. I will name it imgdl but you can give the name you want. To run this last example, you will need to install two new libraries.

$ poetry add anyio rich
$ imgdl --version
imgdl version 0.1.0
$ imgdl -h # note that this time I use "-h" and not "--help"
Downloads multiple images given their urls in FILE and store them in
$ imgdl images.txt ./images
Downloading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00
Downloads are finished! 🌟
  • To test it, I take images from this website. It is the lorem ipsum for images.😄
  • About the code, lines 11 to 19, we define a function to get the list of urls given a file as input.
  • Lines 22 to 34, we define an async function to download the image and place it in the destination folder specified by the user.
  • Lines 37 to 46, we define the main async function run by anyio at line 72. This function runs concurrently all the downloads.
  • Line 49, we use a cool option defined by click to be able to request the CLI version. You can look my CLI checks above to see how it works.
  • Line 50, we define the context_settings argument to be able to use “-h” in addition to “ — help” when requesting the help message of the CLI. More information can be found here.
  • The rest should not be difficult to understand.
  • Take care when defining arguments of their position in the code. If you see again the help message, you will notice the order in which click CLI expects the arguments, file before destination. If you look at the code, you see that the definition of the file argument is on top of the definition of the destination argument. This can save you from some debugging time, trust me.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Kevin Tewouda

Kevin Tewouda


Déserteur camerounais résidant désormais en France. Passionné de programmation, sport, de cinéma et mangas. J’écris en français et en anglais dû à mes origines.