Typer Python
Build a CLI as a stable user-facing protocol: command path, parameter spelling,
types, defaults, streams, exit codes, help, side effects, and tests are its
contract.
Boundary
Use this skill when the project imports Typer or the user explicitly requests a
Typer CLI. Keep domain operations in ordinary Python functions and let command
functions adapt terminal input/output to them. Do not introduce Typer for a
library API, a Click-only project, a one-off shell invocation, or Rich-only
rendering. Use Rich guidance separately when terminal presentation itself is
the job.
Know the runtime objects
| Object |
Meaning |
Responsibility |
typer.Typer |
Command registry and root/group configuration |
Own commands, callbacks, sub-apps, and help behavior. |
| Command function |
Type-annotated adapter called after parsing |
Validate command-level invariants, call domain code, emit output, map failures. |
Annotated[T, typer.Argument(...)] |
Positional CLI field |
Required ordered input whose position is natural and stable. |
Annotated[T, typer.Option(...)] |
Named CLI field |
Optional/configuration input, flags, secrets, repeatable values, or a required named field. |
typer.Context |
One invocation's Click/Typer state |
Shared object state, invoked subcommand, raw extra arguments only when explicitly allowed. |
CliRunner result |
Captured invocation outcome |
Assert exit code, stdout/stderr-visible output, exception, and side effects. |
Read the command object model before changing a
single-command app into groups, adding callbacks, or passing shared state.
Ordered workflow
- Recover the public command contract from documentation, entry points,
existing invocations, completion, and tests. Preserve names and exit behavior
unless the task explicitly changes them.
- Separate pure domain work from the command adapter. Do not unit-test domain
behavior only through terminal text.
- Choose command topology first: one command, several peer commands, or nested
sub-apps. Do not add a callback merely to make help look different.
- Classify every field as argument or option, then specify its Python type,
required/default state, aliases, validation, environment source, and
sensitivity.
- Map expected user errors to concise CLI errors and nonzero exits. Let
unexpected defects retain useful diagnostics; never print secrets or local
values in production tracebacks.
- Test help, success, invalid input, and one side-effect boundary with
CliRunner. Test the domain function directly as well.
- Run the real installed entry point, not only
python module.py, before
declaring completion.
Parameter decisions
| Condition |
Use |
A value is the primary ordered subject (FILE, NAME) |
Argument |
| A value configures behavior or should be self-describing |
Option |
| Omission means a meaningful default |
Option with an explicit default |
| The user must supply a named value |
Required option; do not encode a fake sentinel default |
| Exactly two states exist |
Boolean flag, with positive/negative spelling when both matter |
| Values come from a closed set |
Enum/Literal supported by the installed Typer version |
| A filesystem precondition is structural |
Path plus exists, file/dir, readable/writable constraints |
| Validation depends on another field or external state |
Validate in the command adapter/domain boundary, not an isolated parameter callback |
| Input is secret |
Hidden prompt or environment/config source; never echo it |
Prefer typing.Annotated metadata on Python 3.11+. Keep Python defaults as the
source of default/required semantics. Do not blindly copy legacy
name: str = typer.Option(...) syntax from a prompt; inspect the installed
version when editing older code.
Canonical anchor
from pathlib import Path
from typing import Annotated
import typer
app = typer.Typer(no_args_is_help=True)
@app.callback()
def main() -> None:
"""File export commands."""
@app.command()
def export(
source: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],
output: Annotated[Path, typer.Option("--output", "-o")],
force: Annotated[bool, typer.Option("--force/--no-force")] = False,
) -> None:
if output.exists() and not force:
raise typer.BadParameter("output exists; pass --force", param_hint="--output")
export_file(source, output)
typer.echo(str(output))
if __name__ == "__main__":
app()
The callback keeps export as a named subcommand; without it, Typer can promote
a sole command function to the root invocation. typer.Exit(code=...)
represents an intentional termination, not a returned
process code. BadParameter is for actionable user input errors. Do not catch
Exception and convert programmer defects into a friendly success-looking
message.
Groups, callbacks, and state
- Use one root
Typer and add_typer for a coherent noun namespace such as
users create; do not nest a single command without a stable grouping need.
- A group callback runs before its subcommand. Keep it cheap and free of
irreversible side effects. If
invoke_without_command=True, branch on
ctx.invoked_subcommand is None when root-only behavior must not also run for
subcommands.
- Put shared invocation state in a typed object assigned to
ctx.obj; do not
use module globals for per-invocation configuration.
- Parameter callbacks also run during completion. They must not print, mutate
durable state, or perform expensive network work.
- Enable unknown/extra arguments only for an intentional pass-through command;
otherwise preserve parser rejection.
Read design and testing rules for composition,
error handling, entry points, and CliRunner assertions.
Verification
Inspect the installed version with importlib.metadata.version("typer") and
the installed signatures when
a parameter or help behavior may drift; read version grounding.
Completion requires: the packaged entry point resolves; --help and no-args
behavior are intentional; aliases, defaults, env sources, and secret handling
match the contract; invalid input exits nonzero without a traceback; success
uses the correct output stream and exit code; side effects happen exactly once;
and tests invoke the Typer app with representative arguments rather than
calling only command functions directly.
References
- Command object model
- CLI design and testing
- Version grounding
1---2name: typer-python3description: Use for writing, reviewing, debugging, or testing Python command-line interfaces built with Typer, including typed arguments and options, command groups, callbacks, contexts, exit behavior, help, and CliRunner tests. Do not use merely for terminal styling, arbitrary business logic, a Click-only CLI, shell scripts, or invoking an existing command.4---56# Typer Python78Build a CLI as a stable user-facing protocol: command path, parameter spelling,9types, defaults, streams, exit codes, help, side effects, and tests are its10contract.1112## Boundary1314Use this skill when the project imports Typer or the user explicitly requests a15Typer CLI. Keep domain operations in ordinary Python functions and let command16functions adapt terminal input/output to them. Do not introduce Typer for a17library API, a Click-only project, a one-off shell invocation, or Rich-only18rendering. Use Rich guidance separately when terminal presentation itself is19the job.2021## Know the runtime objects2223| Object | Meaning | Responsibility |24|---|---|---|25| `typer.Typer` | Command registry and root/group configuration | Own commands, callbacks, sub-apps, and help behavior. |26| Command function | Type-annotated adapter called after parsing | Validate command-level invariants, call domain code, emit output, map failures. |27| `Annotated[T, typer.Argument(...)]` | Positional CLI field | Required ordered input whose position is natural and stable. |28| `Annotated[T, typer.Option(...)]` | Named CLI field | Optional/configuration input, flags, secrets, repeatable values, or a required named field. |29| `typer.Context` | One invocation's Click/Typer state | Shared object state, invoked subcommand, raw extra arguments only when explicitly allowed. |30| `CliRunner` result | Captured invocation outcome | Assert exit code, stdout/stderr-visible output, exception, and side effects. |3132Read [the command object model](references/object-model.md) before changing a33single-command app into groups, adding callbacks, or passing shared state.3435## Ordered workflow36371. Recover the public command contract from documentation, entry points,38 existing invocations, completion, and tests. Preserve names and exit behavior39 unless the task explicitly changes them.402. Separate pure domain work from the command adapter. Do not unit-test domain41 behavior only through terminal text.423. Choose command topology first: one command, several peer commands, or nested43 sub-apps. Do not add a callback merely to make help look different.444. Classify every field as argument or option, then specify its Python type,45 required/default state, aliases, validation, environment source, and46 sensitivity.475. Map expected user errors to concise CLI errors and nonzero exits. Let48 unexpected defects retain useful diagnostics; never print secrets or local49 values in production tracebacks.506. Test help, success, invalid input, and one side-effect boundary with51 `CliRunner`. Test the domain function directly as well.527. Run the real installed entry point, not only `python module.py`, before53 declaring completion.5455## Parameter decisions5657| Condition | Use |58|---|---|59| A value is the primary ordered subject (`FILE`, `NAME`) | `Argument` |60| A value configures behavior or should be self-describing | `Option` |61| Omission means a meaningful default | Option with an explicit default |62| The user must supply a named value | Required option; do not encode a fake sentinel default |63| Exactly two states exist | Boolean flag, with positive/negative spelling when both matter |64| Values come from a closed set | `Enum`/`Literal` supported by the installed Typer version |65| A filesystem precondition is structural | `Path` plus `exists`, file/dir, readable/writable constraints |66| Validation depends on another field or external state | Validate in the command adapter/domain boundary, not an isolated parameter callback |67| Input is secret | Hidden prompt or environment/config source; never echo it |6869Prefer `typing.Annotated` metadata on Python 3.11+. Keep Python defaults as the70source of default/required semantics. Do not blindly copy legacy71`name: str = typer.Option(...)` syntax from a prompt; inspect the installed72version when editing older code.7374## Canonical anchor7576```python77from pathlib import Path78from typing import Annotated7980import typer8182app = typer.Typer(no_args_is_help=True)838485@app.callback()86def main() -> None:87 """File export commands."""888990@app.command()91def export(92 source: Annotated[Path, typer.Argument(exists=True, dir_okay=False)],93 output: Annotated[Path, typer.Option("--output", "-o")],94 force: Annotated[bool, typer.Option("--force/--no-force")] = False,95) -> None:96 if output.exists() and not force:97 raise typer.BadParameter("output exists; pass --force", param_hint="--output")98 export_file(source, output)99 typer.echo(str(output))100101102if __name__ == "__main__":103 app()104```105106The callback keeps `export` as a named subcommand; without it, Typer can promote107a sole command function to the root invocation. `typer.Exit(code=...)`108represents an intentional termination, not a returned109process code. `BadParameter` is for actionable user input errors. Do not catch110`Exception` and convert programmer defects into a friendly success-looking111message.112113## Groups, callbacks, and state114115- Use one root `Typer` and `add_typer` for a coherent noun namespace such as116 `users create`; do not nest a single command without a stable grouping need.117- A group callback runs before its subcommand. Keep it cheap and free of118 irreversible side effects. If `invoke_without_command=True`, branch on119 `ctx.invoked_subcommand is None` when root-only behavior must not also run for120 subcommands.121- Put shared invocation state in a typed object assigned to `ctx.obj`; do not122 use module globals for per-invocation configuration.123- Parameter callbacks also run during completion. They must not print, mutate124 durable state, or perform expensive network work.125- Enable unknown/extra arguments only for an intentional pass-through command;126 otherwise preserve parser rejection.127128Read [design and testing rules](references/design-testing.md) for composition,129error handling, entry points, and `CliRunner` assertions.130131## Verification132133Inspect the installed version with `importlib.metadata.version("typer")` and134the installed signatures when135a parameter or help behavior may drift; read [version grounding](references/version-grounding.md).136Completion requires: the packaged entry point resolves; `--help` and no-args137behavior are intentional; aliases, defaults, env sources, and secret handling138match the contract; invalid input exits nonzero without a traceback; success139uses the correct output stream and exit code; side effects happen exactly once;140and tests invoke the `Typer` app with representative arguments rather than141calling only command functions directly.142143## References144145- [Command object model](references/object-model.md)146- [CLI design and testing](references/design-testing.md)147- [Version grounding](references/version-grounding.md)