Developing Python CLIs with Typer

Developing Python CLIs with Typer

CLIs (Command Line Interfaces) are one of the most common categories of software. As opposed to GUIs (Graphical User Interfaces), they do not expose visual means of interacting with the program, they can only be interacted with through a terminal. Due to this, CLIs are generally considered harder to use, but they have the advantage of being easily usable for automation purposes. You can easily call CLIs from within scripts or programs.

CLIs can be written in many programming languages. In this article, we will focus on writing CLIs in Python. In particular, we will focus on using the Typer library.

Typer Introduction

Typer is a Python library dedicated to building intuitive, powerful and fun to code CLIs. It relies on Python's type hinting system (a.k.a. annotations) to avoid a lot of boilerplate code generally required to develop CLIs.

To install typer, use the following command in your Python virtual environment (If you are unfamiliar with virtual environments, I recommend checking out conda ):

pip install typer[all]

We install typer[all] instead of just typer to get all of Typer's features such as rich (colorful) text.

Now, let's write a basic CLI that generates a random number:

# file: typer_example.py

import typer
import random


def main(start: int, end: int):
    """Generate a random integer between start and end"""
    print(random.randint(start, end))


if __name__ == '__main__':
    typer.run(main)        

Let's try calling this script without any arguments:

typer_example_incorrect_call

Note: For now, we will call the CLI using the command python typer_example.py. We will see below how we can define our own command name.

As you can see, Typer handles the argument checks and raises an error describing the problem. We didn't have to write any additional code to handle this.

As suggested by the CLI, let's try the --help option:

typer_example_help

Once again, Typer automatically generates the help menu. The CLI's description was deduced from the main function's docstring. And the arguments, as well as their type, were deduced from the main function's parameters.

One last call. This time, we will give all the required arguments:

typer_example_correct_call

CLI Project: Random Number Generator

In this section, we will build a simple Python project: A CLI that exposes some randomness functionality. This CLI will be exposed through the command rng.

Our CLI will have two sub-commands:

  • number: Will be used to generate one or many random number(s) given an interval (start, end). The number of generated numbers will be controlled through an option --count. For example:rng number 1 100 --count 5 generates five numbers between 1 and 100.rng number 4 20 generates one number between 4 and 20
  • choice: Will choose a random option from a list of options. For example:rng choice cat dog hedgehog will choose randomly one of animals given as options

Project Structure:

This will be the structure of our project:

random-number-generator/
├── src/
│   └── rng/
│      ├── __init__.py
│      └── main.py
├── pyproject.toml
└── README.md        

In the main.py file, we will move the content of our introductory example while making a few small adjustments:

# file: src/rng/main.py

import typer
import random

app = typer.Typer()


@app.command()
def main(start: int, end: int):
    """Generate a random integer between start and end"""
    print(random.randint(start, end))        

In contrast to the introductory example, we have now built a Typer app object. This will allow us to do two things:

  • Introduce sub-commands to the CLI
  • Add an entry-point to the project which allows us to call it as a specific command instead of a python script.

In the pyproject.toml file, we will find the description of our project:

# file: pyproject.toml

[project]
name = "rng"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = [
    "typer[all]"
]

[project.scripts]
rng = "rng.main:app"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/rng"]        

  • In the project section, we find project meta-data such as the name, version and dependencies.
  • In the project.scripts section, we define an entry-point to our project. This will be the name of our CLI's main command: rng. We link it to the Typer app we defined in the main.py module.
  • In the build-system section, we specify the tool that should be used to build our project into a distributable format such as a wheel. We use hatchling as our build system.
  • In the tool.hatch.build.targets.wheel, we tell hatchling where to find the source files for our project.

Thanks to the pyproject.toml project format, we can easily install our project in development mode:

pip install -e .

This installs the project in "editable" mode, which means changes made to the project will be reflected automatically in the development environment.

Once the project is installed, we can call our CLI using the rng command directly:

rng_help

Compared to our first example, two new options have been added:

  • --install-completion
  • --show-completion

These options relate to autocompletion but they do not work on all types of shells, so we will not cover them. You can read more about autocompletion here .

We can deactivate these options in our Typer app:

# file: src/rng/main.py

# ...
app = typer.Typer(add_completion=False)
# ...        

Typer Subcommands

Now that we have the project and the CLI setup, we can start extending it!

We already have a command that generates a random number. Let's add a command that makes a random choice:

# file src/rng/main.py

import typer
import random

app = typer.Typer(add_completion=False)


@app.command()
def number(start: int, end: int):
    """Generate a random integer between start and end"""
    print(random.randint(start, end))


@app.command()
def choice(values: list[str]):
    """Make a random choice from an arbitrary list of value"""
    print(random.choice(values))        

We added a new command called choice which takes a list of options and prints a random one.

We also renamed the main command to number. Renaming the function is not necessary, but it informs Typer of the name we want to give our sub-command.

Let's see what the help menu shows us now:

rng_help_sub_commands

As you can see, Typer automatically detected the second command and is now showing both number and choice as sub-command of our rng command.

Let's see what the new choice command shows:

rng_choice_help

Typer indicates that our command accepts an arbitrary number of arguments: VALUES...

Now let's try the command!

rng_choice

One last thing to do: add the --count option to our number command!

# file: src/rng/main.py

import typer
import random

app = typer.Typer(add_completion=False)


@app.command()
def number(start: int, end: int, count: int = 1):
    """Generate a random integer between start and end"""
    for _ in range(count):
        print(random.randint(start, end))


@app.command()
def choice(values: list[str]):
    """Make a random choice from an arbitrary list of value"""
    print(random.choice(values))        

We've added an optional argument count and used it to loop for the number generator.

Let's see what the help menu shows now:

rng_number_help

Seeing that the count argument is optional, Typer has automatically generated an equivalent option --count in our CLI.

Let's try using this option:

rng_number

Works as intended!

Conclusion

As we saw, Typer allows us to create CLIs quickly and intuitively. It is built with developer experience in mind.

This was a simple tutorial to emphasize the ease of working with Typer, but it can do much more than what was shown here. You can find out more about Typer in the official Typer documentation .

If you've enjoyed how the type hinting system of Python was cleverly used to ease the developer experience, you may also be interested in:

  • FastAPI : a modern, fast and intuitive web framework that makes use of Python's type hinting.
  • Pydantic : a data validation library that enables defining models with minimalistic code. One of its core features is the automatic and performant serialization of models.

要查看或添加评论,请登录

社区洞察

其他会员也浏览了