Skip to content

Instantly share code, notes, and snippets.

@harkabeeparolus
Last active April 19, 2024 04:53
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harkabeeparolus/a6e18b1f4f4f938f450090c5e7523f68 to your computer and use it in GitHub Desktop.
Save harkabeeparolus/a6e18b1f4f4f938f450090c5e7523f68 to your computer and use it in GitHub Desktop.
Typer — my attempt at reference documentation

Typer Cheat Sheet

You should read the Typer Tutorial - User Guide before referring to this summary. Information icons ℹ️ link to the relevant sections of the Typer Tutorial.

Installing ℹ️

$ python3 -m pip install typer  # or typer-slim, to omit rich and shellingham

See also Building a Package for a complete tutorial on making a standalone CLI app with Typer.

Running A Typer App

Single app ℹ️

import typer

def main():
    """Help text for the app."""

typer.run(main)

Exiting ℹ️

raise typer.Exit(code=1)  # default is code=0
raise typer.Abort  # prints "Aborted." and exits with code 1

If there is an EOFError or KeyboardInterrupt inside your app, it is reraised as Abort automatically.

Subcommands ℹ️

import typer

app = typer.Typer()
state = {"verbose": False}

@app.command()
def enable():
    """Help text for subcommand."""

@app.command()
def disable():
    ...

@app.callback()
def main(verbose: bool = False):
    """Help text for main app."""
    state["verbose"] = verbose

if __name__ = "__main__":
    app()

Note:

  • A Typer app callback() could define CLI parameters and help text for the main CLI application itself.
    • It could contain code, or only help text, or both.
  • If there is only a single command(), Typer will automatically use it as the main CLI application.
    • If you do want an app with only a single command, add an app callback(). ℹ️

Typer() arguments:

  • help -- provide help text for the app
  • rich_markup_mode -- If set to "rich", enable Rich console markup in all help. Can also be set to "markdown", which includes 💥 emoji codes. (default: None) ℹ️
  • callback -- provide a callback function here, instead of using @app.callback() decorator.
  • add_completion=False -- disable the Typer completion system, and remove the options from the help text.
  • pretty_exceptions_enable=False -- turn off improved or Rich exceptions. ℹ️
    • You could also achieve the same with the environment variable _TYPER_STANDARD_TRACEBACK=1.
  • pretty_exceptions_show_locals=False -- disable Rich showing local variables, maybe for security reasons.
  • pretty_exceptions_short=False -- include Click and Typer in tracebacks.
  • no_args_is_help=True -- add --help as argument if no arguments are passed.
  • context_settings -- extra Click Context settings.
    • E.g. Typer(context_settings={"help_option_names": ["-h", "--help"]}).

command() arguments:

  • "custom-name" -- as optional first argument, choose a different name. ℹ️
    • By default the command name is the same as the function name, except any underscores in the function name will be replaced with dashes.
  • help -- provide help text
  • rich_help_panel -- display subcommand in a separate Rich panel ℹ️
  • epilog -- provide an epilog section to the help of your command ℹ️
  • no_args_is_help=True -- add --help as argument if no arguments are passed.
    • Unfortunately, this is not inherited from the Typer app, so you have to specify it for each command.
  • context_settings -- extra Click Context settings.

app() arguments:

  • prog_name -- manually set the program name to be displayed in help texts, when Click messes it up, or when you run your app through python -m. ℹ️

Callback function parameters:

  • ctx: typer.Context -- ctx.invoked_subcommand contains the name of the app subcommand.
  • invoke_without_command -- if True, run the app callback when no command is provided. Default is False, which just prints CLI help and exits.
@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
    if ctx.invoked_subcommand is None:
        # This code only runs when no subcommand is provided.
        print("Initializing database")

Command Groups ℹ️

Subcommands can have their own subcommands, which could be defined in another file:

import items
import users

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

add_typer() arguments:

  • name -- set the subcommand name
  • callback -- function for callbacks and help docstring
  • help -- help text if not defined in a callback docstring

Inferring Name and Help Text ℹ️

The precedence to generate a command's name and help, from lowest priority to highest, is:

  1. Implicitly inferred from sub_app = typer.Typer(callback=some_function)
  2. Implicitly inferred from the callback function under @sub_app.callback()
  3. Implicitly inferred from app.add_typer(sub_app, callback=some_function)
  4. Explicitly set on sub_app = typer.Typer(name="some-name", help="Some help.")
  5. Explicitly set on @sub_app.callback("some-name", help="Some help.")
  6. Explicitly set on app.add_typer(sub_app, name="some-name", help="Some help.")

CLI Arguments ℹ️

from typing_extensions import Annotated
from typer import Argument

# without a help text, only type hints are needed
def main(name: str): ...
def main(name: Optional[str] = "Bob"): ...

# to specify help, or other options, you need to use Annotated[]
def main(name: Annotated[str, Argument(help="Name is required")]): ...
def main(name: Annotated[Optional[str], Argument()] = None): ...
def main(name: Annotated[str, Argument(help="Name is optional")] = "Bob"): ...
def main(names: Annotated[List[str], Argument(default_factory=list)]):

# old syntax, without type hints
def main(name: str = Argument(default=...)): ...

Argument() arguments:

  • help -- provide help text
  • metavar -- argument value's name in help text
  • hidden -- hide argument from main help text (will still show up in the first line with Usage )
  • rich_help_panel -- place in separate box in summary
  • show_default -- show default value (bool, or custom str) (default: True) ℹ️
  • envvar -- default to use value from environment variable ℹ️
    • show_envvar -- hide envvar information from help summary

CLI Options ℹ️

from typing_extensions import Annotated
from typer import Option

def main(name: Annotated[str, Option(help="Required name")]): ...
def main(name: Annotated[str, Option(help="Optional name")] = "Bob"): ...
def main(name: Annotated[str, Option("--custom-name", "-n")]): ...

# old syntax
def main(name: str = Option()): ...
def main(name: str = Option("Bob")): ...

Notes:

  • To make a CLI option required, rather than optional, you can put typer.Option() inside of Annotated and leave the parameter without a default value. ℹ️
  • Bool options get automatic "--verbose/--no-verbose" names. You could override this with a custom name. ℹ️

Option() arguments:

  • "--custom-name", "-n" -- provide custom long and short option names ℹ️
  • help -- help text
  • rich_help_panel -- place in separate box in summary
  • show_default -- show default value (bool, or custom str) (default: True) ℹ️
  • prompt -- ask user interactively for missing value, instead of showing an error ℹ️
    • confirmation_prompt -- repeat a prompt for confirmation ℹ️
    • hide_input -- for typing passwords; could be combined with confirmation
  • autocompletion -- provide a callable that takes a str and returns a list of alternatives (str only); see below.
  • callback -- custom logic, e.g. validation; see below
  • is_eager -- process this callback before any other callbacks; good for --version, see below ℹ️

CLI Option Callbacks ℹ️

Does custom logic, e.g. validation, and can print version number:

__version__ = "0.1.0"

def version_callback(value: bool):
    if value:
        print(f"Awesome CLI Version: {__version__}")
        raise typer.Exit

def name_callback(value: str):
    if value != "Camila":
        raise typer.BadParameter("Only Camila is allowed")
    return value

def main(
    name: Annotated[str, typer.Option(callback=name_callback)],
    version: Annotated[
        Optional[bool],
        typer.Option("--version", callback=version_callback, is_eager=True),
    ] = None,
):
    print(f"Hello {name}")

Callback function parameters:

  • ctx: typer.Context -- access the current context
  • param: typer.CallbackParam -- for access to param.name

CLI Option Autocompletion ℹ️

The autocompletion function can return an iterable of str items, or of tuples ("item", "help text"). It can have function parameters of these types:

  • str -- for the incomplete value.
  • ctx: typer.Context -- for the current context.
    • ctx.resilient_parsing is True when handling completion ℹ️, so do not try to validate anything or print to stdout.
  • List[str] -- for the raw CLI parameters.
valid_completion_items = [
    ("Camila", "The reader of books."),
    ("Carlos", "The writer of scripts."),
    ("Sebastian", "The type hints guy."),
]

def complete_name(ctx: typer.Context, incomplete: str):
    names = ctx.params.get("name") or []
    for name, help_text in valid_completion_items:
        if name.startswith(incomplete) and name not in names:
            yield (name, help_text)

@app.command()
def main(
    name: Annotated[
        List[str],
        typer.Option(help="The name to say hi to.", autocompletion=complete_name),
    ] = ["World"],
):
    for n in name:
        print(f"Hello {n}")

Data Validation ℹ️

When you declare a CLI parameter (Argument or Option) with some type Typer will convert the data received in the command line to that data type.

  • Numbers (int, float) ℹ️
    • Can specify max and min for validation.
    • With clamp=True, auto restrict to max/min instead of erroring.
    • count=true will act as a counter, e.g.:
      def main(verbose: Annotated[int, typer.Option("-v", count=True)] = 0): ...
  • Boolean CLI options ℹ️
    • By default Typer creates --something and --no-something automatically.
      • To avoid this, specify the name(s) for typer.Option()
        def main(accept: Annotated[bool, typer.Option("--accept/--reject", "-a/-r")] = False): ...
  • UUID ℹ️
  • DateTime ℹ️
    • formats -- how to parse. (default: ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"])
  • Enum - Choices ℹ️
    • You can make an Enum (choice) CLI parameter be case-insensitive with case_sensitive=False.
    • For Python 3.11 or newer, you should use StrEnum, preferably with auto() values.
  • Path ℹ️
    • exists -- if set to true, the file or directory needs to exist for this value to be valid. If this is not required and a file does indeed not exist, then all further checks are silently skipped.
    • file_okay -- controls if a file is a possible value.
    • dir_okay -- controls if a directory is a possible value.
    • writable -- if True, a writable check is performed.
    • readable -- if True, a readable check is performed.
    • resolve_path -- if this is True, then the path is fully resolved before the value is passed onwards. This means that its absolute and symlinks are resolved.
    • allow_dash -- if True, a single dash to indicate standard streams is permitted.
  • File ℹ️
    • You can use several configuration parameters for these types (classes) in typer.Option() and typer.Argument():
      • mode: controls the "mode" to open the file with.
      • encoding: to force a specific encoding, e.g. "utf-8".
      • lazy: delay I/O operations. By default, it's lazy=True for writing and lazy=False for reading.
      • atomic: if true, all writes will go to a temporary file and then moved to the final destination after completing.
    • By default, Typer will configure the mode for you:
      • typer.FileText: mode="r", to read text.
      • typer.FileTextWrite: mode="w", to write text.
      • typer.FileBinaryRead: mode="rb", to read binary data.
      • typer.FileBinaryWrite: mode="wb", to write binary data.

Custom Types ℹ️

You can have custom parameter types with a parser callable (function or class). If your conversion fails, raise a ValueError.

class CustomClass:
    def __init__(self, value: str):
        self.value = value

    def __str__(self):
        return f"<CustomClass: value={self.value}>"


def parse_custom_class(value: str):
    return CustomClass(value * 2)


def main(
    custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)],
    custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo",
):
    print(f"custom_arg is {custom_arg}")
    print(f"--custom-opt is {custom_opt}")

Multiple Values

List -- any number of values

Use a typing.List to declare a parameter as a list of any number of values, for both Options: ℹ️

def main(number: Annotated[List[float], typer.Option()] = []):
    print(f"The sum is {sum(number)}")

...And Arguments: ℹ️

def main(files: List[Path], celebration: str):
    for path in files:
        if path.is_file():
            print(f"This file exists: {path.name}")
            print(celebration)

Tuple -- fixed number of values

Use a typing.Tuple to get a fixed number of values, possibly of different types, for Options: ℹ️

def main(user: Annotated[Tuple[str, int, bool], typer.Option()] = (None, None, None)):
    username, coins, is_wizard = user

...Or Arguments(): ℹ️

def main(
    names: Annotated[
        Tuple[str, str, str], typer.Argument(help="Select 3 characters to play with")
    ] = ("Harry", "Hermione", "Ron")
):
    for name in names:
        print(f"Hello {name}")

Miscellaneous Functions

Ask with Prompt ℹ️

Prompt and confirm (it is suggested that you prefer to use the CLI Options):

person_name = typer.prompt("What's your name?")
# What's your name?:
delete = typer.confirm("Are you sure you want to delete it?", abort=True)
# Are you sure you want to delete it? [y/N]: n
# Aborted!

User App Dir ℹ️

app_dir = typer.get_app_dir(APP_NAME)
config_path = Path(app_dir) / "config.json"
  • roaming -- controls if the folder should be roaming or not on Windows (default: True)
  • force_posix -- store dotfile directly in $HOME, instead of XDG or macOS default locations (default: False)

Launch File or URL ℹ️

typer.launch("https://typer.tiangolo.com")
typer.launch("/my/downloaded/file", locate=True)
  • locate -- open the file browser indicating where a file is located
  • wait -- wait for the program to exit before returning

Automatically Generated Documentation ℹ️

Use the typer CLI to generate Markdown documentation:

$ typer my_package.main utils docs --name awesome-cli --output README.md
Docs saved to: README.md

Testing ℹ️

Use a CliRunner:

from typer.testing import CliRunner

from .main import app

runner = CliRunner()

def test_app():
    result = runner.invoke(app, ["Camila", "--city", "Berlin"])
    assert result.exit_code == 0
    assert "Hello Camila" in result.stdout
    assert "Let's have a coffee in Berlin" in result.stdout
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment