mrkeyoor.com_
Sat 19 Sept 15:50 UTC
PyPICLI & Toolingupdated 19 Sept 2026

typer review

Typer 0.27.1 turns annotated Python functions into terminal commands. Function parameters become arguments or options, Python types drive conversion, and `Annotated` metadata supplies help, flag names, prompts, file checks, completion callbacks, and other CLI behavior. A `Typer` app groups commands and nested sub-apps, while Rich renders help and errors and the bundled parser follows Click's command model. The installed distribution now carries its Click-derived implementation under `typer._click` instead of declaring Click as an external runtime dependency. Version 0.27.1 changes how an application's `epilog` is formatted so it matches the other generated help sections, and its documentation now recommends uv-based project setup. The framework calls ordinary synchronous functions; registering `async def` does not create or run an event loop.

Verdict

Typer 0.27.1 installed in 0.3 seconds, used 8 MB across 7 packages, imported in 0.24 seconds, and produced 0 audit findings in our sandbox. It suits an explicit, tested Python CLI; tiny scripts, Click-plugin hosts, and commands that require native async dispatch should choose a narrower fit.

We installed it

Lab card: what happened when we installed typerScreenshot of typer documentation
Install✓ · 0.3s7 packages on disk · 8 MB
Importimport typer in 0.24s · pure Python · py.typed · requires Python >=3.10
Known vulns0(pip-audit)

Answers from our run

Does typer install cleanly?

Yes. In a fresh container with an empty cache, pip install typer finished in 0.3s, leaving 7 packages and 8 MB on disk. pip-audit reported no known vulnerabilities.

What does typer need to run?

Python >=3.10, and nothing compiled: it is pure Python. In our run import typer succeeded in 0.24s, and the package ships py.typed for type checkers.

typer or click: which should you use?

click: Choose it when decorators, contexts, custom parameter types, and third-party Click extensions are the main interface. Typer 0.27.1 installed in 0.3 seconds, used 8 MB across 7 packages, imported in 0.24 seconds, and produced 0 audit findings in our sandbox.

When should you not use typer?

A short script can stay inside the standard library. argparse avoids 7 installed packages and removes Typer's 8 MB from the environment.

API stability3/5`Typer`, `command`, `callback`, `Option`, `Argument`, `Context`, `Exit`, and `CliRunner` remain the recognizable API in 0.27.1. The project is still below 1.0, current docs favor `Annotated` declarations, and the installed package now contains a private Click-derived implementation rather than depending on Click from PyPI. Existing command code often survives releases, but parser details and rendered help are public behavior for scripts that users automate.
Docs5/5The official site follows a command from one function through arguments, options, files, enums, prompts, callbacks, nested applications, shell completion, packaging, testing, and Rich output. Most lessons show both Python code and the exact terminal invocation, which exposes inferred flag behavior. Version 0.27.1 also updates help documentation and recommends uv projects. Older default-value examples remain easy to find elsewhere, so the current `Annotated` sections deserve priority.
Maintenance5/5GitHub reports an unarchived repository pushed on 2026-08-25 with 44 open issues and pull requests combined. Version 0.27.1 shipped on 2026-08-03, making epilog formatting consistent with the rest of help and updating setup and help documentation. The project tests Python 3.10 through 3.14 and carries its parser code in-tree, which gives maintainers control but also makes parser upkeep their responsibility.
Ecosystem4/5Typer records 76,295,630 weekly downloads, and GitHub reports 19,925 stars. It works with ordinary Python console-script packaging, common shells, Rich terminal output, and a Click-shaped context model. The package now vendors its Click-derived parser, so users do not install Click as one of Typer's 4 direct dependencies. That reduces external version coupling while making direct use of Click plugins a reason to select Click itself.

Discussed on

  1. hnThe Little Typer312 points
  2. hnThe Little Typer (2018)297 points
  3. hnHacker Typer272 points
  4. hnShow HN: Term Typer – Learn a language by typing220 points
  5. hnThe Little Typer (2018)179 points

Use it if

  • A user-facing Python CLI needs typed arguments, options, generated help, completion, and several commands.
  • Function signatures should be the source for parsing rules while `Annotated` carries terminal-specific metadata.
  • Tests need to invoke the command tree in process and inspect stdout, exit codes, and exceptions.
  • The project wants Rich help and prompts while retaining access to contexts, callbacks, custom parameter behavior, and nested command groups.
Skip it if

Setup reality

We installed Typer 0.27.1 in a fresh Python 3.12 Bookworm sandbox. Installation succeeded in 0.3 seconds, left 7 packages and 8 MB on disk, and import typer worked in 0.24 seconds. Pip-audit found 0 known vulnerabilities. The package is pure Python, requires Python 3.10 or newer, ships py.typed, and declares 4 direct dependencies. The installed package's license field was unknown in our measurement.

A single function can use typer.run(). Build a Typer() app once there are subcommands, a callback, shared context, or CliRunner tests. Current docs place typer.Option() and typer.Argument() inside Annotated, leaving ordinary Python defaults after the annotation. Search results still show the older style where those objects are defaults. Both may appear to work, but one convention makes reviews and migrations less confusing.

Typer imports the command module to build help and shell completion. Keep network clients, database connections, and large model loads inside commands or guarded callbacks so --help remains cheap. Completion needs installation per shell and works best through an installed console-script entry point. Version 0.27.1 also includes the typer executable, which can expose a plain script function as a command without editing that script.

Command callbacks are synchronous. Wrap async application work with asyncio.run() only when no event loop already owns the thread. Use typer.Exit(code=...) for deliberate automation outcomes and typer.Abort for cancellation. CliRunner changes process-like state during invocation, including environment and the isolated filesystem when requested, so avoid parallel tests that share global state even though the base import took only 0.24 seconds.

Patterns

Turn one function into a command run-one-function

import typer

def greet(name: str, times: int = 1) -> None:
    for _ in range(times):
        typer.echo(f"Hello {name}")

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

With one function, its name is omitted from the invocation. A second command changes the CLI into a command group, so choose an app early for a stable public syntax.

Create a command group register-commands

import typer

app = typer.Typer(help="Manage local projects")

@app.command()
def create(name: str) -> None:
    typer.echo(f"created {name}")

@app.command()
def remove(name: str) -> None:
    typer.echo(f"removed {name}")

if __name__ == "__main__":
    app()

Underscores in inferred command names become hyphens. Pass an explicit name to `@app.command("public-name")` before users depend on the spelling.

Describe an option with Annotated declare-option

from typing import Annotated
import typer

@app.command()
def export(
    output: Annotated[
        str,
        typer.Option("--output", "-o", help="File to write"),
    ] = "report.json",
) -> None:
    typer.echo(output)

Current docs keep `Option` metadata inside `Annotated` and the ordinary default after it. Many older examples put `Option` in the default position.

Require a readable input file validate-file

from pathlib import Path
from typing import Annotated
import typer

@app.command()
def import_data(
    source: Annotated[
        Path,
        typer.Argument(exists=True, file_okay=True, dir_okay=False, readable=True),
    ],
) -> None:
    typer.echo(source.read_text())

The path checks happen before the function body. File existence says nothing about the contents, so parsing errors still need command-level handling.

Expose positive and negative flags pair-boolean-flags

from typing import Annotated
import typer

@app.command()
def build(
    color: Annotated[bool, typer.Option("--color/--no-color")] = True,
) -> None:
    typer.echo(f"color={color}")

The paired spelling makes both states visible in help. Renaming either flag later can break shell scripts even when the Python parameter stays the same.

Prompt without echoing a secret prompt-for-secret

from typing import Annotated
import typer

@app.command()
def login(
    username: Annotated[str, typer.Option(prompt=True)],
    password: Annotated[
        str,
        typer.Option(prompt=True, hide_input=True, confirmation_prompt=True),
    ],
) -> None:
    authenticate(username, password)

Prompted values can block automation. Provide a noninteractive credential path and never print the password through logs or exception details.

Pass a global option through context share-global-option

from typing import Annotated
import typer

app = typer.Typer()

@app.callback()
def main(
    ctx: typer.Context,
    verbose: Annotated[bool, typer.Option()] = False,
) -> None:
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose

@app.command()
def sync(ctx: typer.Context) -> None:
    typer.echo(f"verbose={ctx.obj['verbose']}")

Global options appear before the subcommand, such as `tool --verbose sync`. Context avoids mutable module-level flags shared by tests or repeated invocations.

Mount a nested application nest-command-group

import typer

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

@users.command("list")
def list_users() -> None:
    typer.echo("ada")

The resulting path is `tool users list`. The nested app owns its own callback, options, commands, help, and completion entries.

Return a deliberate nonzero status set-exit-code

import typer

@app.command()
def deploy(target: str) -> None:
    if target not in {"staging", "production"}:
        typer.echo("unknown target", err=True)
        raise typer.Exit(code=2)

`typer.Exit` stops command execution with the chosen code. Use `typer.Abort` for user cancellation, which carries different meaning in logs and tests.

Call async application code explicitly run-async-boundary

import asyncio
import typer

async def synchronize() -> int:
    return await sync_remote_records()

@app.command()
def sync() -> None:
    count = asyncio.run(synchronize())
    typer.echo(f"synced {count}")

Typer does not await `async def` commands. `asyncio.run()` works when the command thread has no active event loop; embedded environments may need a different boundary.

Invoke the app in a test test-command

from typer.testing import CliRunner
from mytool.cli import app

runner = CliRunner()

def test_create() -> None:
    result = runner.invoke(app, ["create", "demo"])
    assert result.exit_code == 0, result.exception
    assert "created demo" in result.stdout

Check `result.exception` when a command fails unexpectedly. Runner invocations can alter environment and filesystem state, so tests sharing those globals should stay serial.

Add a final help paragraph format-help-epilog

import typer

app = typer.Typer(
    help="Manage release artifacts",
    epilog="See the operations handbook before publishing.",
)

@app.command()
def inspect() -> None:
    typer.echo("checked")

Version 0.27.1 formats `epilog` consistently with other help sections. Treat its wording as public CLI documentation and cover the help output in a snapshot when layout matters.

Alternatives

PackageRegistryPick it when
clickPyPIChoose it when decorators, contexts, custom parameter types, and third-party Click extensions are the main interface.
cycloptsPyPIChoose it to compare another annotation-led parser, especially when its coercion and nested configuration rules fit the command contract better.
firePyPIChoose it for disposable internal commands that expose existing Python objects with little declaration code.

More cli & tooling guides

commander · chalk · typescript · esbuild · yargs · click · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.