Skip to main content

typer-cli

Add or modify CLI commands using Typer. Use when: adding new CLI subcommands, wrapping async functions for CLI use, understanding the CLI entrypoint structure, or following the @syncify pattern for async commands.

Source facts

Repository
tedivm/robs_awesome_python_template
Last source activity
May 16, 2026 at 17:54
Detected SKILL.md language
English
Stars
312
Forks
23

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
typer-cli
description
Add or modify CLI commands using Typer. Use when: adding new CLI subcommands, wrapping async functions for CLI use, understanding the CLI entrypoint structure, or following the @syncify pattern for async commands.
# Typer CLI > **context7**: If documentation tools are available, resolve and load the full `typer` documentation before making any changes to the CLI system: > ``` > context7_resolve-library-id: "typer" > context7_query-docs: <resolved-id> "your query here" > ``` Any command or script that must be accessible to users must be exposed through the Typer library. For full developer documentation see [docs/dev/cli.md](../../docs/dev/cli.md). --- ## CLI Structure The CLI is registered in `pyproject.toml` as a project script pointing to `{{cookiecutter.__package_slug}}.cli:app`. The main app is defined in `{{cookiecutter.__package_slug}}/cli.py`. Domain-specific subcommand groups live in separate `{{cookiecutter.__package_slug}}/cli_<domain>.py` files and are mounted onto the main app. --- ## Async Commands — `@syncify` Typer runs commands synchronously, but this project uses async throughout (database access, HTTP calls, etc.). Use the `@syncify` decorator from `{{cookiecutter.__package_slug}}/cli.py` to bridge them: ```python from {{cookiecutter.__package_slug}}.cli import syncify @app.command() @syncify async def my_command(name: str) -> None: """This async function will run correctly from the CLI.""" result = await some_async_operation(name) typer.echo(result) ``` **Critical**: `@app.command()` must appear **before** `@syncify` in decorator order. Do **not** use `asyncio.run()` directly — `syncify` handles the event loop correctly. --- {%- if cookiecutter.include_sqlalchemy == "y" %} ### Database Access in CLI Commands Use `get_session` from `{{cookiecutter.__package_slug}}.services.db` for database-backed commands. Always use it as an async context manager: ```python from {{cookiecutter.__package_slug}}.services.db import get_session @app.command() @syncify async def my_db_command() -> None: """Example command using database access.""" async with get_session() as session: result = await session.execute(select(MyModel)) items = result.scalars().all() for item in items: typer.echo(item.name) ``` --- {%- endif %} ## Parameter Pattern Use `Annotated[]` for all arguments and options — it keeps the signature clean and is the Typer-recommended style: ```python import typer from typing import Annotated app = typer.Typer() @app.command() def process( input_file: Annotated[str, typer.Argument(help="Path to the input file")], output_file: Annotated[str | None, typer.Option(help="Path to the output file")] = None, verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")] = False, ) -> None: """Process the input file and generate output.""" if verbose: typer.echo(f"Processing {input_file}...") typer.echo("Done!") ``` ### Arguments vs Options | Type | Typer class | CLI usage | | ------------------------ | ------------------ | -------------------------- | | Positional (required) | `typer.Argument()` | `cmd my-cmd value` | | Named flag/option | `typer.Option()` | `cmd my-cmd --flag x` | | Boolean flag pair | `typer.Option(None, "--yes/--no")` | `cmd my-cmd --yes` | --- ## Adding a New Command Group Create `{{cookiecutter.__package_slug}}/cli_<domain>.py`: ```python # {{cookiecutter.__package_slug}}/cli_reports.py import typer from typing import Annotated reports_app = typer.Typer( help="Report generation commands.", no_args_is_help=True, ) @reports_app.command("generate") def generate_report( output: Annotated[str, typer.Argument(help="Output file path")], ) -> None: """Generate a report and write it to the output path.""" ... ``` Then mount it in `{{cookiecutter.__package_slug}}/cli.py`: ```python from .cli_reports import reports_app app.add_typer(reports_app, name="reports") ``` Use `no_args_is_help=True` on subapp `Typer()` instances so that `cmd reports` without arguments shows help rather than an error. --- ## Error Handling Pattern For error output, write to stderr: `typer.echo(..., err=True)`. Exit with a non-zero code on failure: `raise typer.Exit(code=1)`. For clean exits: `raise typer.Exit()`. --- ## Style Checklist - [ ] New commands in `{{cookiecutter.__package_slug}}/cli.py` or `{{cookiecutter.__package_slug}}/cli_<domain>.py` - [ ] Async commands use `@syncify` (not `asyncio.run()`) - [ ] `@app.command()` decorator appears **before** `@syncify` - [ ] All parameters use `Annotated[type, typer.Argument/Option(...)]` - [ ] Every command has a docstring (used as `--help` text) - [ ] Subapp `Typer()` instances use `no_args_is_help=True` - [ ] New subapp mounted in `{{cookiecutter.__package_slug}}/cli.py` with `app.add_typer(...)` - [ ] Error output goes to `err=True` - [ ] Failures raise `typer.Exit(code=1)` (not `sys.exit(1)` or bare exceptions)
View on GitHub