{"id":11959,"date":"2024-11-28T16:05:11","date_gmt":"2024-11-28T16:05:11","guid":{"rendered":"https:\/\/www.blopig.com\/blog\/?p=11959"},"modified":"2024-11-28T16:12:09","modified_gmt":"2024-11-28T16:12:09","slug":"building-cli-applications-with-typer","status":"publish","type":"post","link":"https:\/\/www.blopig.com\/blog\/2024\/11\/building-cli-applications-with-typer\/","title":{"rendered":"Building  CLI Applications with Typer"},"content":{"rendered":"\n<p>Remember the last time you had to build a command-line tool? If you&#8217;re like me, you probably started with <code>argparse<\/code> or <code>click<\/code>, wrote boilerplate code, and still ended up with something that felt clunky. That&#8217;s where <a href=\"https:\/\/typer.tiangolo.com\/\" data-type=\"link\" data-id=\"https:\/\/typer.tiangolo.com\/\">typer<\/a> comes in \u2013 it&#8217;s a game-changer that lets you build CLI apps with minimal code. Although there are several other options, typer stands out because it leverages Python&#8217;s type hints to do the heavy lifting. No more manual argument parsing! The following snippet shows how to use typer in its simplest form:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import typer\n\napp = typer.Typer()\n\n@app.command()\ndef hello(name: str):\n    typer.echo(f\"Hello {name}!\")\n\nif __name__ == \"__main__\":\n    app()<\/pre>\n\n\n\n<p>And you will be able to execute it with just:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ python hello.py Pedro\nHello Pedro!\n\n<\/pre>\n\n\n\n<p>In this simple example, we were only defining positional arguments, but having optional arguments is as easy as setting default values in the function signature.<\/p>\n\n\n\n<!--more-->\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import typer\n\napp = typer.Typer()\n\n@app.command()\ndef hello2(\n    name: str,\n    count: int = 1,\n    favorite_color: str = None\n):\n    for _ in range(count):\n        message = f\"Hello {name}!\"\n        if favorite_color:\n            message += f\" I like {favorite_color} too!\"\n        typer.echo(message)\n\nif __name__ == \"__main__\":\n    app()<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ python hello2.py --help\n\n Usage: hello2.py [OPTIONS] NAME                                                                                                               \n                                                                                                                                                   \n\u256d\u2500 Arguments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2502 *    name      TEXT  [default: None] [required]                                                                  \n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\u2502 --count                     INTEGER  [default: 1]                                                                     \n\u2502 --favorite-color            TEXT     [default: None]                                                                  \n\u2502 --install-completion                 Install completion for the current shell.                                       \n\u2502 --show-completion                    Show completion for the current shell, to copy it or customize the installation.  \n\u2502 --help                               Show this message and exit.                                                       \n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\n\n<\/pre>\n\n\n\n<p>And you can also provide additional metadata, like preferred parameter names or  help information by using the <code>Annotated<\/code> type hint in combination with <code data-enlighter-language=\"generic\" class=\"EnlighterJSRAW\">typer.Argument<\/code> for positional arguments and <code data-enlighter-language=\"generic\" class=\"EnlighterJSRAW\">typer.Option<\/code> for optional arguments. And it also allows for some very useful options such as file path validation.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">from pathlib import Path\nimport typer\n\nfrom typing import Annotated\n\napp = typer.Typer()\n\n@app.command()\ndef process(\n    input_file: Annotated[\n        Path,\n        typer.Argument(\n            exists=True,\n            file_okay=True,\n            dir_okay=False,\n            help=\"Input file to process\"\n        )\n    ],\n    output_dir: Annotated[\n        Path,\n        typer.Option(\n            \"--output\",\n            \"-o\",\n            help=\"Output directory\",\n        )\n    ] = Path(\"output\"),\n    backup: Annotated[\n        bool,\n        typer.Option(\n            help=\"Create backup before processing\"\n        )\n    ] = False,\n):\n    \"\"\"Process a file with progress tracking.\"\"\"\n    \n    # Ensure output directory exists\n    output_dir.mkdir(parents=True, exist_ok=True)\n    \n    # Create backup if requested\n    if backup:\n        backup_file = output_dir \/ f\"{input_file.name}.bak\"\n        typer.echo(f\"Creating backup at {backup_file}\")\n        backup_file.write_bytes(input_file.read_bytes())\n    n = 0\n    with open(input_file) as f:\n        for line in f:\n            n+= len(line)\n    with open(output_dir \/ \"count.txt\", \"w\") as f:\n        f.write(f\"{n}\\n\")\n    \n    typer.echo(f\"Processing complete! N lines= {n}\")\n    \n\nif __name__ == \"__main__\":\n    app()<\/pre>\n\n\n\n<p>Whether you&#8217;re building a simple utility or a complex CLI application, typer&#8217;s type-driven approach will save you time and result in better tools. Next time you need to build a CLI tool, give typer a try. Your users (and future self) will thank you!<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Remember the last time you had to build a command-line tool? If you&#8217;re like me, you probably started with argparse or click, wrote boilerplate code, and still ended up with something that felt clunky. That&#8217;s where typer comes in \u2013 it&#8217;s a game-changer that lets you build CLI apps with minimal code. Although there are [&hellip;]<\/p>\n","protected":false},"author":85,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"nf_dc_page":"","wikipediapreview_detectlinks":true,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"ngg_post_thumbnail":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[29,227],"tags":[809,152],"ppma_author":[557],"class_list":["post-11959","post","type-post","status-publish","format-standard","hentry","category-code","category-python-code","tag-cli","tag-python"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"authors":[{"term_id":557,"user_id":85,"is_guest":0,"slug":"ruben","display_name":"Ruben Sanchez-Garcia","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/7301ee01be22b8e41f73a40f0f9a3c1a8fcc9bd35f6ed27763b3827f56e8735c?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/11959","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/users\/85"}],"replies":[{"embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/comments?post=11959"}],"version-history":[{"count":5,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/11959\/revisions"}],"predecessor-version":[{"id":12005,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/posts\/11959\/revisions\/12005"}],"wp:attachment":[{"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/media?parent=11959"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/categories?post=11959"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/tags?post=11959"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.blopig.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=11959"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}