From ee27e0d6cf33c4887c23c978354b0e43d07376dc Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 3 Mar 2026 17:54:59 +0100 Subject: [PATCH 1/5] first draft of the official Typer skill --- typer/.agents/skills/typer/SKILL.md | 129 ++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 typer/.agents/skills/typer/SKILL.md diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md new file mode 100644 index 0000000000..bf57ab1f3d --- /dev/null +++ b/typer/.agents/skills/typer/SKILL.md @@ -0,0 +1,129 @@ +--- +name: typer +description: Typer best practices and conventions. Use when working with Typer CLIs. Keeps Typer code clean and up to date with the latest features and patterns, updated with new versions. Write new code or refactor and update old code. +--- + +# Typer + +Official Typer skill to write code with best practices, keeping up to date with new versions and features. + +## Use an explicit `Typer` app + +For maximum generalizability, create an explicit Typer app and register subcommand(s), instead of using `typer.run`: + +```python +import typer + +app = typer.Typer() + + +@app.command() +def hello(): + print(f"Hello World") + + +if __name__ == "__main__": + app() +``` + +instead of: + +```python +# DO NOT DO THIS: Not extensible. Use Typer() instead. +import typer + + +def main(): + print(f"Hello World") + + +if __name__ == "__main__": + typer.run(main) +``` + +## Execute the app + +To execute the app in the terminal, run + +```bash +python main.py +``` + +Or, when multiple commands are registered to the Typer app, add the command name: +```bash +python main.py hello +``` + +## Use `Annotated` + +Always prefer the `Annotated` style for declarations of CLI arguments and options. + +It allows us to pass additional metadata that can be used by Typer. + +```python +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def hello(name: Annotated[str, typer.Argument()] = "World"): + # Note that name is an optional Argument, as a default is provided + print(f"Hello {name}") + + +if __name__ == "__main__": + app() +``` + +An older way of setting a default value is this: +```python +# DO NOT DO THIS: old style. Use Annotated instead. + +@app.command() +def main(name: str = typer.Argument(default="World")): + # Note that name is an optional Argument, as a default is provided + print(f"Hello {name}") +``` + +Similarly, the old style could use ellipsis (...) to explicitely mark an argument as required. +```python +# DO NOT DO THIS: old style. Use Annotated without a default value instead. + +@app.command() +def main(name: str = typer.Argument(default=...)): + # Note that name is now a required Argument + print(f"Hello {name}") +``` + +## CLI Options + +CLI options are declared in a similar fashion, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name): + +```python +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(user_name: Annotated[str, typer.Option("--name", "-n")]): + # On the CLI, the required user name can be specified with -n or --name + print(f"Hello {user_name}") + + +if __name__ == "__main__": + app() +``` + +## Click + +Originally, Typer was built on Click. In new versions however, Click has been vendored and Click extensions should therefor not be used anymore. + +Other settings of Option and Argument that came from Click but shouldn't be used in Typer include `expose_value`, `shell_complete`, `show_choices`, `errors`, `prompt_required`, `is_flag`, `flag_value`, `allow_from_autoenv`. + +Code bases using these should be refactored to use pure Typer functionality. From 4ad9074e048dfd14c3d2c313693ad9e710aaf9e2 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 3 Mar 2026 20:36:02 +0100 Subject: [PATCH 2/5] few more additions --- typer/.agents/skills/typer/SKILL.md | 78 ++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md index bf57ab1f3d..ab03d1e862 100644 --- a/typer/.agents/skills/typer/SKILL.md +++ b/typer/.agents/skills/typer/SKILL.md @@ -7,6 +7,12 @@ description: Typer best practices and conventions. Use when working with Typer C Official Typer skill to write code with best practices, keeping up to date with new versions and features. +## Installing typer + +In a virtual environment, `pip install typer` (with pip) or `uv pip install typer` (with uv). For your library/project, add `typer` to the dependencies in `pyproject.toml`. + +Do not install `typer-slim` or `typer-cli`, they are both deprecated and will now simply install `typer`. + ## Use an explicit `Typer` app For maximum generalizability, create an explicit Typer app and register subcommand(s), instead of using `typer.run`: @@ -49,7 +55,7 @@ To execute the app in the terminal, run python main.py ``` -Or, when multiple commands are registered to the Typer app, add the command name: +When multiple commands are registered to the Typer app, you have to add the command name: ```bash python main.py hello ``` @@ -120,6 +126,76 @@ if __name__ == "__main__": app() ``` +### CLI options with multiple values + +By declaring a CLI option as a list, it can receive multiple values: + +```python +from typing import Annotated + +import typer + +app = typer.Typer() + + +@app.command() +def main(user: Annotated[list[str] | None, typer.Option()] = None): + if not user: + print(f"No users provided!") + raise typer.Abort() + for u in user: + print(f"Processing user: {u}") + + +if __name__ == "__main__": + app() +``` + +This can be executed like so: + +```bash +python main.py --user Rick --user Morty --user Summer +``` + +## Rich + +By default, Rich can be used with its custom markup syntax to set colors and styles, e.g. + +```python +from rich import print + +print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:") +``` + +Typer also supports using Rich formatting in the docstrings and the help messages of CLI arguments and CLI options. + +To disable this, set `rich_markup_mode` to `None` when creating a `Typer()` app. By default it is enabled (i.e. set to `"rich"`). + +### Rich markdown + +You can also set `rich_markup_mode` to `"markdown"` to use Markdown in the docstring: + +```python +from typing import Annotated + +import typer + +app = typer.Typer(rich_markup_mode="markdown") + +@app.command(help="**Delete** a user with *USERNAME*.") +def delete( + username: Annotated[str, typer.Argument(help="The username to be **deleted**")], + force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False, +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username} (force={force})") + +if __name__ == "__main__": + app() +``` + ## Click Originally, Typer was built on Click. In new versions however, Click has been vendored and Click extensions should therefor not be used anymore. From 79d105e68cfec62f2642b3438fc90d79c29b3e3e Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 3 Mar 2026 20:45:11 +0100 Subject: [PATCH 3/5] small fixes --- typer/.agents/skills/typer/SKILL.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md index ab03d1e862..e5476acad9 100644 --- a/typer/.agents/skills/typer/SKILL.md +++ b/typer/.agents/skills/typer/SKILL.md @@ -13,6 +13,8 @@ In a virtual environment, `pip install typer` (with pip) or `uv pip install type Do not install `typer-slim` or `typer-cli`, they are both deprecated and will now simply install `typer`. +Typer supports Python 3.10 and above. + ## Use an explicit `Typer` app For maximum generalizability, create an explicit Typer app and register subcommand(s), instead of using `typer.run`: @@ -56,6 +58,7 @@ python main.py ``` When multiple commands are registered to the Typer app, you have to add the command name: + ```bash python main.py hello ``` @@ -95,6 +98,7 @@ def main(name: str = typer.Argument(default="World")): ``` Similarly, the old style could use ellipsis (...) to explicitely mark an argument as required. + ```python # DO NOT DO THIS: old style. Use Annotated without a default value instead. @@ -106,7 +110,7 @@ def main(name: str = typer.Argument(default=...)): ## CLI Options -CLI options are declared in a similar fashion, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name): +CLI options are declared in a similar fashion as arguments, but will be called on the CLI with a single dash (single letter) or 2 dashes (full name): ```python from typing import Annotated @@ -126,6 +130,18 @@ if __name__ == "__main__": app() ``` +You can run this program with + +```bash +python main.py -n "Rick" +``` + +or + +```bash +python main.py --name "Morty" +``` + ### CLI options with multiple values By declaring a CLI option as a list, it can receive multiple values: @@ -167,7 +183,7 @@ from rich import print print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:") ``` -Typer also supports using Rich formatting in the docstrings and the help messages of CLI arguments and CLI options. +Typer supports using Rich formatting in the docstrings and the help messages of CLI arguments and CLI options. To disable this, set `rich_markup_mode` to `None` when creating a `Typer()` app. By default it is enabled (i.e. set to `"rich"`). @@ -198,8 +214,8 @@ if __name__ == "__main__": ## Click -Originally, Typer was built on Click. In new versions however, Click has been vendored and Click extensions should therefor not be used anymore. +Originally, Typer was built on Click. However, going forward Typer will vendor Click. As such, Click extensions should not be used anymore. -Other settings of Option and Argument that came from Click but shouldn't be used in Typer include `expose_value`, `shell_complete`, `show_choices`, `errors`, `prompt_required`, `is_flag`, `flag_value`, `allow_from_autoenv`. +Other settings of `Option` and `Argument` that came from Click but shouldn't be used in Typer anymore, include: `expose_value`, `shell_complete`, `show_choices`, `errors`, `prompt_required`, `is_flag`, `flag_value` and `allow_from_autoenv`. Code bases using these should be refactored to use pure Typer functionality. From e939c979688fe0993781eccba153a0b7249b1cbd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:45:57 +0000 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/.agents/skills/typer/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md index e5476acad9..00b2912910 100644 --- a/typer/.agents/skills/typer/SKILL.md +++ b/typer/.agents/skills/typer/SKILL.md @@ -130,7 +130,7 @@ if __name__ == "__main__": app() ``` -You can run this program with +You can run this program with ```bash python main.py -n "Rick" @@ -170,7 +170,7 @@ if __name__ == "__main__": This can be executed like so: ```bash -python main.py --user Rick --user Morty --user Summer +python main.py --user Rick --user Morty --user Summer ``` ## Rich From 12220390115703e5b5cc9fa8ec1afbb71bf2d6e6 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Tue, 3 Mar 2026 21:02:22 +0100 Subject: [PATCH 5/5] further refinements --- typer/.agents/skills/typer/SKILL.md | 71 +++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/typer/.agents/skills/typer/SKILL.md b/typer/.agents/skills/typer/SKILL.md index 00b2912910..19b63c491f 100644 --- a/typer/.agents/skills/typer/SKILL.md +++ b/typer/.agents/skills/typer/SKILL.md @@ -63,6 +63,14 @@ When multiple commands are registered to the Typer app, you have to add the comm python main.py hello ``` +To see the automatically generated help documentation, run + +```bash +python main.py --help +``` + +or set `no_args_is_help` to `True` when creating the `Typer()` add to automatically show the help when running a command without any arguments. + ## Use `Annotated` Always prefer the `Annotated` style for declarations of CLI arguments and options. @@ -87,6 +95,13 @@ if __name__ == "__main__": app() ``` +This program can be run as-is, or can provide a specific name: + +```bash +python main.py +python main.py Rick +``` + An older way of setting a default value is this: ```python # DO NOT DO THIS: old style. Use Annotated instead. @@ -130,15 +145,10 @@ if __name__ == "__main__": app() ``` -You can run this program with +You can run this program as such: ```bash python main.py -n "Rick" -``` - -or - -```bash python main.py --name "Morty" ``` @@ -185,7 +195,46 @@ print("[bold red]Alert![/bold red] [green]Portal gun[/green] shooting! :boom:") Typer supports using Rich formatting in the docstrings and the help messages of CLI arguments and CLI options. -To disable this, set `rich_markup_mode` to `None` when creating a `Typer()` app. By default it is enabled (i.e. set to `"rich"`). + +```python +from typing import Annotated + +import typer + +app = typer.Typer(rich_markup_mode="rich") + + +@app.command() +def create( + username: Annotated[ + str, typer.Argument(help="The username to be [green]created[/green]") + ], +): + """ + [bold green]Create[/bold green] a new [italic]shiny[/italic] user. :sparkles: + + This requires a [underline]username[/underline]. + """ + print(f"Creating user: {username}") + + +@app.command(help="[bold red]Delete[/bold red] a user with [italic]USERNAME[/italic].") +def delete( + username: Annotated[ + str, typer.Argument(help="The username to be [red]deleted[/red]") + ], +): + """ + Some internal utility function to delete. + """ + print(f"Deleting user: {username}") + + +if __name__ == "__main__": + app() +``` + +To disable Rich formatting, set `rich_markup_mode` to `None` when creating a `Typer()` app. By default (when no value is given), Rich formatting is enabled. ### Rich markdown @@ -200,13 +249,9 @@ app = typer.Typer(rich_markup_mode="markdown") @app.command(help="**Delete** a user with *USERNAME*.") def delete( - username: Annotated[str, typer.Argument(help="The username to be **deleted**")], - force: Annotated[bool, typer.Option(help="Force the **deletion** :boom:")] = False, + username: Annotated[str, typer.Argument(help="The username to be **deleted** :boom:")] ): - """ - Some internal utility function to delete. - """ - print(f"Deleting user: {username} (force={force})") + print(f"Deleting user: {username}") if __name__ == "__main__": app()