typer

star 19.6k

Typer best practices and conventions. Use when working with Typer CLIs. Keeps Typer code clean and up to date with the latest features and patterns, updated with new versions. Write new code or refactor and update old code.

fastapi By fastapi schedule Updated 5/30/2026

name: typer description: Typer best practices and conventions. Use when working with Typer CLIs. Keeps Typer code clean and up to date with the latest features and patterns, updated with new versions. Write new code or refactor and update old code.

Typer

Official Typer skill to write code with best practices, keeping up to date with new versions and features.

Installing typer

In a virtual environment, pip install typer (with pip) or uv pip install typer (with uv). For your library/project, add typer to the dependencies in pyproject.toml.

Do not install typer-slim or typer-cli, they are both deprecated and will now simply install typer.

Typer supports Python 3.10 and above.

Use an explicit Typer app

For maximum generalizability, create an explicit Typer app and register subcommand(s), instead of using typer.run:

import typer

app = typer.Typer()


@app.command()
def hello():
    print(f"Hello World")


if __name__ == "__main__":
    app()

instead of:

# DO NOT DO THIS: Not extensible. Use Typer() instead.
import typer


def main():
    print(f"Hello World")


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

Execute the app

To execute the app in the terminal, run

python main.py

When multiple commands are registered to the Typer app, you have to add the command name:

python main.py hello

To see the automatically generated help documentation, run

python main.py --help

or set no_args_is_help to True when creating the Typer() add to automatically show the help when running a command without any arguments.

Use Annotated

Always prefer the Annotated style for declarations of CLI arguments and options.

It allows us to pass additional metadata that can be used by Typer.

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def hello(name: Annotated[str, typer.Argument()] = "World"):
    # Note that name is an optional Argument, as a default is provided
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

This program can be run as-is, or can provide a specific name:

python main.py
python main.py Rick

An older way of setting a default value is this:

# DO NOT DO THIS: old style. Use Annotated instead.

@app.command()
def main(name: str = typer.Argument(default="World")):
    # Note that name is an optional Argument, as a default is provided
    print(f"Hello {name}")

Similarly, the old style could use ellipsis (...) to explicitly mark an argument as required.

# DO NOT DO THIS: old style. Use Annotated without a default value instead.

@app.command()
def main(name: str = typer.Argument(default=...)):
    # Note that name is now a required Argument
    print(f"Hello {name}")

CLI Options

CLI options are declared in a similar fashion as arguments, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name).

The CLI option name is automatically generated from the variable name, so user_name becomes --user-name automatically:

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(user_name: Annotated[str, typer.Option()]):
    # On the CLI, the required user name can be specified with --user-name
    print(f"Hello {user_name}")


if __name__ == "__main__":
    app()

If you want to specify a different name, or want to add a short version, declare them in the typer.Option:

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(user_name: Annotated[str, typer.Option("--name", "-n")]):
    # On the CLI, the required user name can be specified with -n or --name
    print(f"Hello {user_name}")


if __name__ == "__main__":
    app()

You can run this program as such:

python main.py -n "Rick"
python main.py --name "Morty"

CLI options with multiple values

By declaring a CLI option as a list, it can receive multiple values:

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(user: Annotated[list[str] | None, typer.Option()] = None):
    if not user:
        print(f"No users provided!")
        raise typer.Abort()
    for u in user:
        print(f"Processing user: {u}")


if __name__ == "__main__":
    app()

This can be executed like so:

python main.py --user Rick --user Morty --user Summer

Rich

By default, Rich can be used with its custom markup syntax to set colors and styles, e.g.

from rich import print

print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:")

Typer supports using Rich formatting in the docstrings and the help messages of CLI arguments and CLI options.

from typing import Annotated

import typer

app = typer.Typer(rich_markup_mode="rich")


@app.command()
def create(
    username: Annotated[
        str, typer.Argument(help="The username to be [green]created[/green]")
    ],
):
    """
    [bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles:

    This requires a [underline]username[/underline].
    """
    print(f"Creating user: {username}")


@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].")
def delete(
    username: Annotated[
        str, typer.Argument(help="The username to be [red]deleted[/red]")
    ],
):
    """
    Some internal utility function to delete.
    """
    print(f"Deleting user: {username}")


if __name__ == "__main__":
    app()

To disable Rich formatting, set rich_markup_mode to None when creating a Typer() app. By default (when no value is given), Rich formatting is enabled.

Rich markdown

You can also set rich_markup_mode to "markdown" to use Markdown in the docstring:

from typing import Annotated

import typer

app = typer.Typer(rich_markup_mode="markdown")

@app.command(help="**Delete** a user with *USERNAME*.")
def delete(
    username: Annotated[str, typer.Argument(help="The username to be **deleted** :boom:")]
):
    print(f"Deleting user: {username}")

if __name__ == "__main__":
    app()

Click

Originally, Typer was built on Click. However, since version 0.26.0, Typer has vendored Click. As such, Click extensions should not be used anymore.

Other settings of Option and Argument that came from Click but shouldn't be used in Typer anymore, include: expose_value, shell_complete, show_choices, errors, prompt_required, is_flag, flag_value and allow_from_autoenv.

Code bases using these should be refactored to use pure Typer functionality.

Install via CLI
npx skills add https://github.com/fastapi/typer --skill typer
Repository Details
star Stars 19,578
call_split Forks 915
navigation Branch main
article Path SKILL.md
More from Creator