Python: Better CLIs with Typer

Build a Python command line tool

Pravash
5 min readApr 28, 2023
python_typer

Hi Everyone, In this article, I will explore Typer, a powerful and intuitive library for building CLI applications in Python. And also will explain this topic with example which will help you to get started with it and use it in your day to day coding.

What is Typer

This python library makes it easy to create command-line interfaces for the Python applications. With this we can easily create simplified, user-friendly and robust CLI applications.

Rather than specifying valid CLI arguments using argparse, Typer infers the arguments from the underlying functions. It is based on python type hints.

Install Typer module:

pip install typer

After this, you only need to do is write your own functions and typer will can call it from the command line. Lets see an example -

import typer

app = typer.Typer()

@app.command()
def hello_world():
print("hello world")

@app.command()
def hello(name):
print(f"hello {name}")

if __name__ == "__main__":
app()

In the above code snippet, I am defining 2 functions to print the a message. Now you can call your script from the command-line to perform various tasks:

python ./typer_ex/hello_ex.py hello-world  # hello world
python ./typer_ex/hello_ex.py hello <your_name> # hello <your_name>

NOTE:
Before running, you can check the help message of the script, by running -
python ./typer_ex/hello_ex.py help

Cool Features of Typer

Adding arguments

Typer provides an easy way to define arguments in your CLI application. Let’s take a look at an example:

import typer

app = typer.Typer()

@app.command()
def greet(name: str):
typer.echo(f"Hello, {name}!")

if __name__ == "__main__":
app()

As you can see in this example, a CLI command greet with an argument name of type str is defined. The name argument is defined within the function signature using type hints. Typer automatically generates the command-line interface for this command, that will make you to provide a value for the name argument when the command from the command line is executed.

Here’s how you can use this CLI command from the command line:

$ python my_app.py greet John
Hello, John!

Default value and Help Message

Another cool feature is, the behaviour of the arguments can be customized, like providing a default value, specifying the help message, setting a list of allowed choices and more. Typer provides decorators and utility functions for these customizations. For example:

import typer

app = typer.Typer()

@app.command()
def greet2(name: str = typer.Argument("world", help="The name to greet", show_default=True)):
typer.echo(f"Hello, {name}!")

if __name__ == "__main__":
app()

Here, I have modified the previous example that is, I have provided the default value of "world" for the argument name using the typer.Argument() utility. I have also specified a help message and set show_default=True to display the default value in the help message. So now when you run the below command:

python my_app.py greet2 --help

In the terminal it will print -

Usage: app.py greet2 [OPTIONS] [NAME]

Arguments:
[NAME] The name to greet [default: world]

Options:
- help Show this message and exit.

Adding CLI switch

In Typer, we can easily add CLI switches to the commands by defining options with a bool type hint. A CLI switch is a boolean flag that is either set (True) or unset (False) by us, and it does not requires a value to be provided. Switches are useful for providing binary options or toggles in the CLI applications.

Here’s an example of how we can define a CLI switch using Typer:

import typer

app = typer.Typer()

@app.command()
def backup(database: str, output_dir: str, force: bool = False):
if force:
typer.echo("Forced backup requested!")
else:
typer.echo("Regular backup requested.")
typer.echo(f"Database: {database}")
typer.echo(f"Output directory: {output_dir}")

if __name__ == "__main__":
app()

In this example, a force option as a boolean switch is used. The bool type hint indicates that this option is a boolean flag, and it does not require a value to be provided.

Now we can invoke the backup command with or without the — force flag, like this -

# Without --force flag
python my_cli.py backup mydb /backup

# With --force flag
python my_cli.py backup mydb /backup --force

And if you run the “--help” command it will print -

Usage: app.py backup [OPTIONS] DATABASE OUTPUT_DIR

Arguments:
DATABASE [required]
OUTPUT_DIR [required]

Options:
--force / --no-force [default: no-force]
--help Show this message and exit.

Using callback

This feature is really very helpful if some validations to be done before running the main function. We can use @app.callback(), decorator to perform some logic before executing any commands, Here’s an example of how it works:

import typer

app = typer.Typer()


# Define a callback function to be executed before a command is invoked
@app.callback()
def before_command_callback():
typer.echo("Before command")

@app.command()
def greet(name: str):
typer.echo(f"Hello, {name}!")

if __name__ == "__main__":
app()

In the above example, the before_command_callback() function will print "Before command" before the greet command is invoked. This shows, how we can use @app.callback() to define custom actions that are executed before specific commands within the Typer app.

Creating sub-apps

Lets use a scenario where we want to create a separate sub-app that is dedicated to perform operations on files, while we already have the main-app to perform some other tasks.
Here’s an example of how we can create a main app with a sub-app using Typer:

import typer
import shutil

app = typer.Typer()

# Typer app for 'files'
files_app = typer.Typer(name="files", help="Manage files")

# 'files' as sub-app to the main app
app.add_typer(files_app, name="files", help="File management commands")


def move_files(src_path: str, dest_path: str):
shutil.move(src_path, dest_path)
typer.echo(f"Moved files from '{src_path}' to '{dest_path}'")

files_app.command(name="move", help="Move files")(move_files)

def delete_files(file_path: str):
shutil.rmtree(file_path)
typer.echo(f"Deleted files at '{file_path}'")

files_app.command(name="delete", help="Delete files")(delete_files)


@app.command()
def main_app():
print("Inside Main App")

if __name__ == "__main__":
app()

So now we can separetly run the sub-commands “move” and “delete” under the “files” command trigger their respective functions and perform file management operations. For example:

# Run the main app
python file_manager.py main-app

# Move files from source path to destination path
python file_manager.py files move /path/to/src /path/to/dest

# Delete files at the specified path
python file_manager.py files delete /path/to/files

Throughout this article, I covered some of the key features of Typer, including adding arguments, adding CLI switches, and creating sub-commands. I believe this will help you to get started. Although there are manu use cases and other cool features which can be done with Typer (link).

By leveraging Typer’s functionality, It’s easy to create command-line tools for tasks such as automation, data processing, DevOps, and more.

Typer can help in creating professional and efficient command-line applications with ease. So why wait? Give Typer a try and level up your command-line tool development in Python!

--

--

Pravash

I am a passionate Data Engineer and Technology Enthusiast. Here I am using this platform to share my knowledge and experience on tech stacks.