Typer turns annotated Python functions into terminal commands. Required parameters become arguments, defaults become options, and type hints drive conversion, validation, and generated help. It fits internal tools and automation projects that already accept third-party dependencies.

For a small tool that must stay in the standard library, use the Python argparse guide instead.

Create a command

python -m pip install typer
from pathlib import Path
from typing import Annotated

import typer

app = typer.Typer(no_args_is_help=True)


@app.command()
def count(
    file: Path,
    skip_blank: Annotated[
        bool, typer.Option("--skip-blank/--keep-blank")
    ] = True,
) -> None:
    """Count lines in a text file."""
    if not file.is_file():
        raise typer.BadParameter("file does not exist")

    lines = file.read_text(encoding="utf-8").splitlines()
    total = sum(bool(line.strip()) for line in lines) if skip_blank else len(lines)
    typer.echo(total)


if __name__ == "__main__":
    app()

Run python app.py --help. Help text is part of the public interface, so keep docstrings direct and command names stable.

Design subcommands and errors

Larger tools can group actions such as users create and users list in separate Typer objects. Command functions should parse input and render output. Put file access, HTTP requests, and domain rules in regular modules so they remain testable without the CLI.

Do not pass API keys as visible command options because shell history may retain them. Prefer environment variables or an appropriately hidden prompt.

Test the interface

from typer.testing import CliRunner

from app import app

runner = CliRunner()


def test_help() -> None:
    result = runner.invoke(app, ["--help"])
    assert result.exit_code == 0
    assert "Count lines" in result.stdout

Cover missing files, invalid input, and option combinations too. The pytest guide explains how to organize these cases.

The official Typer tutorial, accessed July 22, 2026, documents arguments, options, subcommands, and testing. Choose Typer for maintainability, not just colorful help: reliable CLIs also need meaningful exit codes, actionable errors, and safe handling of secrets.

Arguments, options, and explicit metadata

Arguments identify the main resource a command acts on; options tune behavior. Avoid a long sequence of positional arguments because users cannot easily remember their order. Annotated keeps the Python type and Typer metadata together:

from typing import Annotated

import typer


def export(
    source: Annotated[Path, typer.Argument(help="Input JSON file")],
    output: Annotated[
        Path, typer.Option("--output", "-o", help="Destination file")
    ] = Path("report.csv"),
    limit: Annotated[
        int, typer.Option(min=1, max=10_000, help="Maximum rows")
    ] = 100,
) -> None:
    ...

The type conversion occurs before the command body. Typer can reject an invalid integer and generate consistent help without manual parsing. Domain validation still belongs in application code: a syntactically valid path may contain the wrong schema, and a valid identifier may refer to a missing record.

Boolean pairs such as --color/--no-color make both states discoverable. Do not use a boolean option when there are three meaningful modes; an enum communicates the allowed values better. Keep option names stable because shell scripts depend on them even when humans do not.

Produce useful failures and exit codes

Use typer.BadParameter for invalid user input related to an argument or option. For an operational failure, print a concise message to standard error and exit with a nonzero code:

try:
    rows = load_rows(file)
except PermissionError:
    typer.echo(f"Cannot read {file}", err=True)
    raise typer.Exit(code=2)

Do not expose an internal traceback for a routine input error. Conversely, do not catch every Exception, print a vague message, and discard debugging context. Unexpected defects should remain observable in logs or error reporting. Establish an exit-code contract if other programs consume the CLI: zero for success, a documented nonzero code for expected failures, and distinct codes only when callers can act on the distinction.

Output also forms an interface. Send machine-readable data to standard output and diagnostics to standard error. If automation is a real use case, provide a stable --json mode rather than asking scripts to scrape decorated tables. Avoid colors when output is redirected, or rely on Typer and its terminal stack to detect that condition.

Split a larger application

Create one Typer object per command group and register it with add_typer:

app = typer.Typer(no_args_is_help=True)
users_app = typer.Typer(no_args_is_help=True)


@users_app.command("list")
def list_users(active: bool = True) -> None:
    for user in find_users(active=active):
        typer.echo(user.name)


app.add_typer(users_app, name="users")

Place groups in separate modules once the file becomes difficult to navigate. Avoid circular imports by creating the application object in a small entry module and importing command groups there. Command functions should translate CLI values into calls to ordinary services; those services should not import Typer.

A callback is appropriate for global options such as --verbose or a configuration path. Store shared state in typer.Context.obj, but do not turn that dictionary into a hidden dependency throughout the business layer.

Configuration, prompts, and secrets

Define predictable precedence when a setting may come from a command option, environment variable, configuration file, and default. A common order is explicit option, environment, file, then default. Document it in --help.

Typer supports environment-backed options and hidden prompts. A prompt can protect a password from shell history, but interactive input breaks unattended jobs. For automation, read credentials from a secret manager or environment variable supplied by the execution platform. Never echo the value or include it in an exception message.

Confirmation prompts are useful for destructive operations, but also provide an explicit noninteractive mechanism such as --yes for controlled automation. The default should remain safe.

Test behavior, not only help

CliRunner gives each invocation isolated arguments and captured streams:

def test_missing_file(tmp_path: Path) -> None:
    missing = tmp_path / "missing.txt"
    result = runner.invoke(app, ["count", str(missing)])
    assert result.exit_code != 0
    assert "does not exist" in result.output


def test_count(tmp_path: Path) -> None:
    source = tmp_path / "lines.txt"
    source.write_text("one\n\nthree\n", encoding="utf-8")
    result = runner.invoke(app, ["count", str(source)])
    assert result.exit_code == 0
    assert result.stdout.strip() == "2"

Test the public invocation just as a user runs it, including command names. Also unit-test the extracted service functions directly. This combination catches interface regressions without forcing every business-rule test through terminal emulation.

Package the command

For an installable project, declare a console entry point in pyproject.toml:

[project.scripts]
text-tools = "text_tools.cli:app"

After installation, users run text-tools instead of python app.py. Keep the import target lightweight so --help starts quickly. Pin supported Python and Typer versions in project metadata, and test the actual installed entry point in CI at least once.

Typer is most valuable when annotations reduce parsing code while the architecture stays ordinary Python. Clear help, stable names, deterministic output, safe secret handling, and tests are what turn a convenient command into a dependable interface.

Release checklist

Run every command with --help and verify argument names, defaults, examples, and terminology. Exercise invalid types, absent files, permission errors, empty input, and conflicting options. Confirm expected diagnostics go to stderr and successful machine-readable output stays clean on stdout.

Install the built package in a fresh virtual environment instead of testing only the source checkout. Invoke the console script from another directory to uncover accidental assumptions about the current working directory. Test paths containing spaces and non-ASCII characters when the supported platforms allow them.

For backwards compatibility, treat command names, options, exit codes, and structured output fields as a public API. Deprecate a spelling before removing it when scripts may rely on it. Record the behavior in release notes, and keep a small end-to-end test for the most important automation path.

Operationally, verify Ctrl+C stops promptly, temporary files are cleaned up, writes are atomic where partial output would be harmful, and a repeated safe command has a predictable result. These details matter more than elaborate terminal styling.