Python: Better CLIs with 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!