Skip to content

Typer

Typer is a library for building CLI applications that users will love using and developers will love creating. Based on Python 3.6+ type hints.

The key features are:

  • Intuitive to write: Great editor support. Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
  • Easy to use: It's easy to use for the final users. Automatic help, and automatic completion for all shells.
  • Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
  • Start simple: The simplest example adds only 2 lines of code to your app: 1 import, 1 function call.
  • Grow large: Grow in complexity as much as you want, create arbitrarily complex trees of commands and groups of subcommands, with options and arguments.

Installation

pip install 'typer[all]'

Minimal usage

import typer


def cli(name: str):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(cli)

Usage

Create a typer.Typer() app, and create two subcommands with their parameters.

import typer

app = typer.Typer()


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


@app.command()
def goodbye(name: str, formal: bool = False):
    if formal:
        print(f"Goodbye Ms. {name}. Have a good day.")
    else:
        print(f"Bye {name}!")


if __name__ == "__main__":
    app()

Using subcommands

In some cases, it's possible that your application code needs to live on a single file.

import typer

app = typer.Typer()
items_app = typer.Typer()
app.add_typer(items_app, name="items")
users_app = typer.Typer()
app.add_typer(users_app, name="users")


@items_app.command("create")
def items_create(item: str):
    print(f"Creating item: {item}")


@items_app.command("delete")
def items_delete(item: str):
    print(f"Deleting item: {item}")


@items_app.command("sell")
def items_sell(item: str):
    print(f"Selling item: {item}")


@users_app.command("create")
def users_create(user_name: str):
    print(f"Creating user: {user_name}")


@users_app.command("delete")
def users_delete(user_name: str):
    print(f"Deleting user: {user_name}")


if __name__ == "__main__":
    app()

Then you'll be able to call each subcommand with:

python main.py items create

For more complex code use nested subcommands

Nested Subcommands

You can split the commands in different files for clarity once the code starts to grow:

File: reigns.py:

import typer

app = typer.Typer()


@app.command()
def conquer(name: str):
    print(f"Conquering reign: {name}")


@app.command()
def destroy(name: str):
    print(f"Destroying reign: {name}")


if __name__ == "__main__":
    app()

File: towns.py:

import typer

app = typer.Typer()


@app.command()
def found(name: str):
    print(f"Founding town: {name}")


@app.command()
def burn(name: str):
    print(f"Burning town: {name}")


if __name__ == "__main__":
    app()

File: lands.py:

import typer

import reigns
import towns

app = typer.Typer()
app.add_typer(reigns.app, name="reigns")
app.add_typer(towns.app, name="towns")

if __name__ == "__main__":
    app()

File: users.py:

import typer

app = typer.Typer()


@app.command()
def create(user_name: str):
    print(f"Creating user: {user_name}")


@app.command()
def delete(user_name: str):
    print(f"Deleting user: {user_name}")


if __name__ == "__main__":
    app()

File: items.py:

import typer

app = typer.Typer()


@app.command()
def create(item: str):
    print(f"Creating item: {item}")


@app.command()
def delete(item: str):
    print(f"Deleting item: {item}")


@app.command()
def sell(item: str):
    print(f"Selling item: {item}")


if __name__ == "__main__":
    app()

File: main.py:

import typer

import items
import lands
import users

app = typer.Typer()
app.add_typer(users.app, name="users")
app.add_typer(items.app, name="items")
app.add_typer(lands.app, name="lands")

if __name__ == "__main__":
    app()

Arguments with help

import typer
from typing_extensions import Annotated


def main(name: Annotated[str, typer.Argument(help="The name of the user to greet")]):
    print(f"Hello {name}")

Using the context

When you create a Typer application it uses Click underneath. And every Click application has a special object called a "Context" that is normally hidden.

But you can access the context by declaring a function parameter of type typer.Context.

The context is also used to store objects that you may need for all the commands, for example a repository.

developer)suggests to use global variables or a function with lru_cache. Tiangolo (typers main

Using short option names

import typer


def main(user_name: str = typer.Option(..., "--name", "-n")):
    print(f"Hello {user_name}")


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

The ... as the first argument is to make the option required

Create a verbose argument

A simple --verbose and -v flag can be defined as:

import typer


def main(
    verbose: Annotated[bool, typer.Option("--verbose", "-v")] = False,
):
    print(f"Verbose level is {verbose}")


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

You can make a CLI option work as a counter with the counter parameter:

import typer


def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0):
    print(f"Verbose level is {verbose}")


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

Get the command line application directory

You can get the application directory where you can, for example, save configuration files with typer.get_app_dir():

from pathlib import Path

import typer

APP_NAME = "my-super-cli-app"


def main() -> None:
    """Define the main command line interface."""
    app_dir = typer.get_app_dir(APP_NAME)
    config_path: Path = Path(app_dir) / "config.json"
    if not config_path.is_file():
        print("Config file doesn't exist yet")


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

It will give you a directory for storing configurations appropriate for your CLI program for the current user in each operating system.

Exiting with an error code

typer.Exit() takes an optional code parameter. By default, code is 0, meaning there was no error.

You can pass a code with a number other than 0 to tell the terminal that there was an error in the execution of the program:

import typer


def main(username: str):
    if username == "root":
        print("The root user is reserved")
        raise typer.Exit(code=1)
    print(f"New user created: {username}")


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

Create a --version command

You could use a callback to implement a --version CLI option.

It would show the version of your CLI program and then it would terminate it. Even before any other CLI parameter is processed.

from typing import Optional

import typer

__version__ = "0.1.0"


def version_callback(value: bool) -> None:
    """Print the version of the program."""
    if value:
        print(f"Awesome CLI Version: {__version__}")
        raise typer.Exit()


def main(
    version: Optional[bool] = typer.Option(
        None, "--version", callback=version_callback, is_eager=True
    ),
) -> None:
    ...


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

Add a --help and -h command

typer by default gives the --help command. If you want -h to work too add:

import typer

app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})


@app.command()
def main(name: str):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

You can print to "standard error" with a Rich Console(stderr=True)

from rich.console import Console

err_console = Console(stderr=True)
err_console.print("error message")

Testing

Testing is similar to click testing, but you import the CliRunner directly from typer:

from typer.testing import CliRunner

References