[{"data":1,"prerenderedAt":2252},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis\u002F":3,"content-directory":1705},{"id":4,"title":5,"body":6,"date":1691,"description":1692,"difficulty":1693,"draft":1694,"extension":1695,"meta":1696,"navigation":404,"path":1697,"seo":1698,"stem":1699,"tags":1700,"updated":1691,"__hash__":1704},"content\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis\u002Findex.md","Building a Cookiecutter Template for Typer CLIs",{"type":7,"value":8,"toc":1671},"minimark",[9,36,41,62,66,73,77,87,91,94,294,301,304,308,314,522,525,890,929,937,955,972,979,983,986,1039,1042,1046,1049,1087,1090,1094,1101,1565,1571,1575,1581,1585,1589,1596,1600,1612,1616,1623,1627,1634,1638,1667],[10,11,12,13,17,18,21,22,25,26,29,30,35],"p",{},"Your team starts a new internal CLI every few weeks, and each one begins by copying the last one and deleting things. The copies drift: one has the current CI workflow, another an old one; one has ",[14,15,16],"code",{},"--version"," wired correctly, another prints ",[14,19,20],{},"0.1.0"," forever; the new hire's project has no tests at all. A project template fixes this by encoding \"how we build CLIs here\" once and generating it on demand. This guide builds a cookiecutter template that produces a complete Typer CLI — ",[14,23,24],{},"src\u002F"," layout, ",[14,27,28],{},"pyproject.toml"," with a console script, a first command with tests, a CI workflow — with names derived from a single answer so they cannot disagree, and a test suite for the template itself so it stays working. It belongs to the ",[31,32,34],"a",{"href":33},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002F","CLI project scaffolding with cookiecutter topic",".",[37,38,40],"h2",{"id":39},"prerequisites","Prerequisites",[42,43,44,51,54],"ul",{},[45,46,47,48,35],"li",{},"Python 3.10+ and uv; cookiecutter runs on demand with ",[14,49,50],{},"uvx cookiecutter",[45,52,53],{},"An existing CLI you consider a good example — the template will be extracted from it.",[45,55,56,57,61],{},"If you have not chosen between cookiecutter and Copier yet, ",[31,58,60],{"href":59},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates\u002F","Copier vs cookiecutter for CLI templates"," compares them; the structure below translates to Copier with minor changes.",[37,63,65],{"id":64},"the-shape-of-the-template","The shape of the template",[10,67,68,69,72],{},"A cookiecutter template is a repository with a ",[14,70,71],{},"cookiecutter.json"," describing the questions to ask, and a directory whose name is itself a template expression. Everything inside that directory is rendered through Jinja, file contents and file names alike.",[74,75],"inline-diagram",{"name":76},"ck-tpl-layout",[78,79,85],"pre",{"className":80,"code":82,"language":83,"meta":84},[81],"language-text","cookiecutter-typer-cli\u002F\n├── cookiecutter.json\n├── hooks\u002F\n│   └── post_gen_project.py\n├── tests\u002F\n│   └── test_template.py\n└── {{cookiecutter.project_slug}}\u002F\n    ├── pyproject.toml\n    ├── README.md\n    ├── .github\u002Fworkflows\u002Fci.yml\n    ├── src\u002F{{cookiecutter.package_name}}\u002F\n    │   ├── __init__.py\n    │   ├── __main__.py\n    │   └── cli.py\n    └── tests\u002F\n        └── test_cli.py\n","text","",[14,86,82],{"__ignoreMap":84},[37,88,90],{"id":89},"the-recipe-questions-and-derived-names","The recipe: questions and derived names",[10,92,93],{},"Ask as few questions as possible and derive everything else. A human-friendly project name produces the distribution name, the package name and the default command name through Jinja filters, so the three can never disagree:",[78,95,99],{"className":96,"code":97,"language":98,"meta":84,"style":84},"language-json shiki shiki-themes github-light github-dark","{\n  \"project_name\": \"My Tool\",\n  \"project_slug\": \"{{ cookiecutter.project_name.lower().strip().replace(' ', '-').replace('_', '-') }}\",\n  \"package_name\": \"{{ cookiecutter.project_slug.replace('-', '_') }}\",\n  \"command_name\": \"{{ cookiecutter.project_slug }}\",\n  \"description\": \"A command-line tool.\",\n  \"author_name\": \"Platform Team\",\n  \"python_min\": [\"3.10\", \"3.11\", \"3.12\"],\n  \"include_docker\": [\"no\", \"yes\"],\n  \"__prompts__\": {\n    \"project_name\": \"Human-readable project name\",\n    \"command_name\": \"Command users will type\",\n    \"python_min\": \"Oldest supported Python\"\n  }\n}\n","json",[14,100,101,110,127,140,153,166,179,192,218,236,245,258,271,282,288],{"__ignoreMap":84},[102,103,106],"span",{"class":104,"line":105},"line",1,[102,107,109],{"class":108},"sVt8B","{\n",[102,111,113,117,120,124],{"class":104,"line":112},2,[102,114,116],{"class":115},"sj4cs","  \"project_name\"",[102,118,119],{"class":108},": ",[102,121,123],{"class":122},"sZZnC","\"My Tool\"",[102,125,126],{"class":108},",\n",[102,128,130,133,135,138],{"class":104,"line":129},3,[102,131,132],{"class":115},"  \"project_slug\"",[102,134,119],{"class":108},[102,136,137],{"class":122},"\"{{ cookiecutter.project_name.lower().strip().replace(' ', '-').replace('_', '-') }}\"",[102,139,126],{"class":108},[102,141,143,146,148,151],{"class":104,"line":142},4,[102,144,145],{"class":115},"  \"package_name\"",[102,147,119],{"class":108},[102,149,150],{"class":122},"\"{{ cookiecutter.project_slug.replace('-', '_') }}\"",[102,152,126],{"class":108},[102,154,156,159,161,164],{"class":104,"line":155},5,[102,157,158],{"class":115},"  \"command_name\"",[102,160,119],{"class":108},[102,162,163],{"class":122},"\"{{ cookiecutter.project_slug }}\"",[102,165,126],{"class":108},[102,167,169,172,174,177],{"class":104,"line":168},6,[102,170,171],{"class":115},"  \"description\"",[102,173,119],{"class":108},[102,175,176],{"class":122},"\"A command-line tool.\"",[102,178,126],{"class":108},[102,180,182,185,187,190],{"class":104,"line":181},7,[102,183,184],{"class":115},"  \"author_name\"",[102,186,119],{"class":108},[102,188,189],{"class":122},"\"Platform Team\"",[102,191,126],{"class":108},[102,193,195,198,201,204,207,210,212,215],{"class":104,"line":194},8,[102,196,197],{"class":115},"  \"python_min\"",[102,199,200],{"class":108},": [",[102,202,203],{"class":122},"\"3.10\"",[102,205,206],{"class":108},", ",[102,208,209],{"class":122},"\"3.11\"",[102,211,206],{"class":108},[102,213,214],{"class":122},"\"3.12\"",[102,216,217],{"class":108},"],\n",[102,219,221,224,226,229,231,234],{"class":104,"line":220},9,[102,222,223],{"class":115},"  \"include_docker\"",[102,225,200],{"class":108},[102,227,228],{"class":122},"\"no\"",[102,230,206],{"class":108},[102,232,233],{"class":122},"\"yes\"",[102,235,217],{"class":108},[102,237,239,242],{"class":104,"line":238},10,[102,240,241],{"class":115},"  \"__prompts__\"",[102,243,244],{"class":108},": {\n",[102,246,248,251,253,256],{"class":104,"line":247},11,[102,249,250],{"class":115},"    \"project_name\"",[102,252,119],{"class":108},[102,254,255],{"class":122},"\"Human-readable project name\"",[102,257,126],{"class":108},[102,259,261,264,266,269],{"class":104,"line":260},12,[102,262,263],{"class":115},"    \"command_name\"",[102,265,119],{"class":108},[102,267,268],{"class":122},"\"Command users will type\"",[102,270,126],{"class":108},[102,272,274,277,279],{"class":104,"line":273},13,[102,275,276],{"class":115},"    \"python_min\"",[102,278,119],{"class":108},[102,280,281],{"class":122},"\"Oldest supported Python\"\n",[102,283,285],{"class":104,"line":284},14,[102,286,287],{"class":108},"  }\n",[102,289,291],{"class":104,"line":290},15,[102,292,293],{"class":108},"}\n",[10,295,296,297,300],{},"Variables whose default refers to another variable are computed after the earlier answers, and users can still override them — useful when the command should be shorter than the project name. A list as the value makes a choice prompt with the first element as default. Keys starting with a double underscore, like ",[14,298,299],{},"__prompts__",", customise the prompts without becoming variables.",[74,302],{"name":303},"ck-tpl-variables",[37,305,307],{"id":306},"the-recipe-the-generated-files","The recipe: the generated files",[10,309,310,311,313],{},"The heart of the template is a ",[14,312,28],{}," that produces a working, installable CLI:",[78,315,319],{"className":316,"code":317,"language":318,"meta":84,"style":84},"language-toml shiki shiki-themes github-light github-dark","# {{cookiecutter.project_slug}}\u002Fpyproject.toml\n[project]\nname = \"{{ cookiecutter.project_slug }}\"\nversion = \"0.1.0\"\ndescription = \"{{ cookiecutter.description }}\"\nreadme = \"README.md\"\nrequires-python = \">={{ cookiecutter.python_min }}\"\nauthors = [{ name = \"{{ cookiecutter.author_name }}\" }]\ndependencies = [\"typer>=0.12\"]\n\n[project.scripts]\n{{ cookiecutter.command_name }} = \"{{ cookiecutter.package_name }}.cli:app\"\n\n[dependency-groups]\ndev = [\"pytest>=8\", \"ruff>=0.6\", \"mypy>=1.10\"]\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.ruff]\ntarget-version = \"py{{ cookiecutter.python_min.replace('.', '') }}\"\n","toml",[14,320,321,327,339,347,355,363,371,379,390,400,406,419,425,429,438,458,463,473,484,493,498,513],{"__ignoreMap":84},[102,322,323],{"class":104,"line":105},[102,324,326],{"class":325},"sJ8bj","# {{cookiecutter.project_slug}}\u002Fpyproject.toml\n",[102,328,329,332,336],{"class":104,"line":112},[102,330,331],{"class":108},"[",[102,333,335],{"class":334},"sScJk","project",[102,337,338],{"class":108},"]\n",[102,340,341,344],{"class":104,"line":129},[102,342,343],{"class":108},"name = ",[102,345,346],{"class":122},"\"{{ cookiecutter.project_slug }}\"\n",[102,348,349,352],{"class":104,"line":142},[102,350,351],{"class":108},"version = ",[102,353,354],{"class":122},"\"0.1.0\"\n",[102,356,357,360],{"class":104,"line":155},[102,358,359],{"class":108},"description = ",[102,361,362],{"class":122},"\"{{ cookiecutter.description }}\"\n",[102,364,365,368],{"class":104,"line":168},[102,366,367],{"class":108},"readme = ",[102,369,370],{"class":122},"\"README.md\"\n",[102,372,373,376],{"class":104,"line":181},[102,374,375],{"class":108},"requires-python = ",[102,377,378],{"class":122},"\">={{ cookiecutter.python_min }}\"\n",[102,380,381,384,387],{"class":104,"line":194},[102,382,383],{"class":108},"authors = [{ name = ",[102,385,386],{"class":122},"\"{{ cookiecutter.author_name }}\"",[102,388,389],{"class":108}," }]\n",[102,391,392,395,398],{"class":104,"line":220},[102,393,394],{"class":108},"dependencies = [",[102,396,397],{"class":122},"\"typer>=0.12\"",[102,399,338],{"class":108},[102,401,402],{"class":104,"line":238},[102,403,405],{"emptyLinePlaceholder":404},true,"\n",[102,407,408,410,412,414,417],{"class":104,"line":247},[102,409,331],{"class":108},[102,411,335],{"class":334},[102,413,35],{"class":108},[102,415,416],{"class":334},"scripts",[102,418,338],{"class":108},[102,420,421],{"class":104,"line":260},[102,422,424],{"class":423},"s7hpK","{{ cookiecutter.command_name }} = \"{{ cookiecutter.package_name }}.cli:app\"\n",[102,426,427],{"class":104,"line":273},[102,428,405],{"emptyLinePlaceholder":404},[102,430,431,433,436],{"class":104,"line":284},[102,432,331],{"class":108},[102,434,435],{"class":334},"dependency-groups",[102,437,338],{"class":108},[102,439,440,443,446,448,451,453,456],{"class":104,"line":290},[102,441,442],{"class":108},"dev = [",[102,444,445],{"class":122},"\"pytest>=8\"",[102,447,206],{"class":108},[102,449,450],{"class":122},"\"ruff>=0.6\"",[102,452,206],{"class":108},[102,454,455],{"class":122},"\"mypy>=1.10\"",[102,457,338],{"class":108},[102,459,461],{"class":104,"line":460},16,[102,462,405],{"emptyLinePlaceholder":404},[102,464,466,468,471],{"class":104,"line":465},17,[102,467,331],{"class":108},[102,469,470],{"class":334},"build-system",[102,472,338],{"class":108},[102,474,476,479,482],{"class":104,"line":475},18,[102,477,478],{"class":108},"requires = [",[102,480,481],{"class":122},"\"hatchling\"",[102,483,338],{"class":108},[102,485,487,490],{"class":104,"line":486},19,[102,488,489],{"class":108},"build-backend = ",[102,491,492],{"class":122},"\"hatchling.build\"\n",[102,494,496],{"class":104,"line":495},20,[102,497,405],{"emptyLinePlaceholder":404},[102,499,501,503,506,508,511],{"class":104,"line":500},21,[102,502,331],{"class":108},[102,504,505],{"class":334},"tool",[102,507,35],{"class":108},[102,509,510],{"class":334},"ruff",[102,512,338],{"class":108},[102,514,516,519],{"class":104,"line":515},22,[102,517,518],{"class":108},"target-version = ",[102,520,521],{"class":122},"\"py{{ cookiecutter.python_min.replace('.', '') }}\"\n",[10,523,524],{},"And a first command that already follows the house conventions — a version flag from package metadata, output on the right streams, a callback so subcommands can be added without restructuring:",[78,526,530],{"className":527,"code":528,"language":529,"meta":84,"style":84},"language-python shiki shiki-themes github-light github-dark","# {{cookiecutter.project_slug}}\u002Fsrc\u002F{{cookiecutter.package_name}}\u002Fcli.py\nfrom importlib.metadata import version\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer(no_args_is_help=True, help=\"{{ cookiecutter.description }}\")\n\n\ndef _version(value: bool) -> None:\n    if value:\n        typer.echo(f\"{{ cookiecutter.command_name }} {version('{{ cookiecutter.project_slug }}')}\")\n        raise typer.Exit()\n\n\n@app.callback()\ndef main(\n    _: Annotated[bool, typer.Option(\"--version\", callback=_version, is_eager=True,\n                                    help=\"Show the version and exit.\")] = False,\n) -> None:\n    \"\"\"{{ cookiecutter.description }}\"\"\"\n\n\n@app.command()\ndef hello(name: Annotated[str, typer.Argument(help=\"Who to greet.\")] = \"world\") -> None:\n    \"\"\"Print a greeting (replace me with your first real command).\"\"\"\n    typer.echo(f\"Hello, {name}!\")\n","python",[14,531,532,537,552,564,568,575,579,623,627,631,654,662,707,715,719,723,731,741,773,793,801,806,810,815,823,860,866],{"__ignoreMap":84},[102,533,534],{"class":104,"line":105},[102,535,536],{"class":325},"# {{cookiecutter.project_slug}}\u002Fsrc\u002F{{cookiecutter.package_name}}\u002Fcli.py\n",[102,538,539,543,546,549],{"class":104,"line":112},[102,540,542],{"class":541},"szBVR","from",[102,544,545],{"class":108}," importlib.metadata ",[102,547,548],{"class":541},"import",[102,550,551],{"class":108}," version\n",[102,553,554,556,559,561],{"class":104,"line":129},[102,555,542],{"class":541},[102,557,558],{"class":108}," typing ",[102,560,548],{"class":541},[102,562,563],{"class":108}," Annotated\n",[102,565,566],{"class":104,"line":142},[102,567,405],{"emptyLinePlaceholder":404},[102,569,570,572],{"class":104,"line":155},[102,571,548],{"class":541},[102,573,574],{"class":108}," typer\n",[102,576,577],{"class":104,"line":168},[102,578,405],{"emptyLinePlaceholder":404},[102,580,581,584,587,590,594,596,599,601,604,606,609,612,615,618,620],{"class":104,"line":181},[102,582,583],{"class":108},"app ",[102,585,586],{"class":541},"=",[102,588,589],{"class":108}," typer.Typer(",[102,591,593],{"class":592},"s4XuR","no_args_is_help",[102,595,586],{"class":541},[102,597,598],{"class":115},"True",[102,600,206],{"class":108},[102,602,603],{"class":592},"help",[102,605,586],{"class":541},[102,607,608],{"class":122},"\"",[102,610,611],{"class":115},"{{",[102,613,614],{"class":122}," cookiecutter.description ",[102,616,617],{"class":115},"}}",[102,619,608],{"class":122},[102,621,622],{"class":108},")\n",[102,624,625],{"class":104,"line":194},[102,626,405],{"emptyLinePlaceholder":404},[102,628,629],{"class":104,"line":220},[102,630,405],{"emptyLinePlaceholder":404},[102,632,633,636,639,642,645,648,651],{"class":104,"line":238},[102,634,635],{"class":541},"def",[102,637,638],{"class":334}," _version",[102,640,641],{"class":108},"(value: ",[102,643,644],{"class":115},"bool",[102,646,647],{"class":108},") -> ",[102,649,650],{"class":115},"None",[102,652,653],{"class":108},":\n",[102,655,656,659],{"class":104,"line":247},[102,657,658],{"class":541},"    if",[102,660,661],{"class":108}," value:\n",[102,663,664,667,670,672,674,677,679,682,685,688,690,693,695,697,700,703,705],{"class":104,"line":260},[102,665,666],{"class":108},"        typer.echo(",[102,668,669],{"class":541},"f",[102,671,608],{"class":122},[102,673,611],{"class":115},[102,675,676],{"class":122}," cookiecutter.command_name ",[102,678,617],{"class":115},[102,680,681],{"class":115}," {",[102,683,684],{"class":108},"version(",[102,686,687],{"class":122},"'",[102,689,611],{"class":115},[102,691,692],{"class":122}," cookiecutter.project_slug ",[102,694,617],{"class":115},[102,696,687],{"class":122},[102,698,699],{"class":108},")",[102,701,702],{"class":115},"}",[102,704,608],{"class":122},[102,706,622],{"class":108},[102,708,709,712],{"class":104,"line":273},[102,710,711],{"class":541},"        raise",[102,713,714],{"class":108}," typer.Exit()\n",[102,716,717],{"class":104,"line":284},[102,718,405],{"emptyLinePlaceholder":404},[102,720,721],{"class":104,"line":290},[102,722,405],{"emptyLinePlaceholder":404},[102,724,725,728],{"class":104,"line":460},[102,726,727],{"class":334},"@app.callback",[102,729,730],{"class":108},"()\n",[102,732,733,735,738],{"class":104,"line":465},[102,734,635],{"class":541},[102,736,737],{"class":334}," main",[102,739,740],{"class":108},"(\n",[102,742,743,746,748,751,754,756,759,761,764,767,769,771],{"class":104,"line":475},[102,744,745],{"class":108},"    _: Annotated[",[102,747,644],{"class":115},[102,749,750],{"class":108},", typer.Option(",[102,752,753],{"class":122},"\"--version\"",[102,755,206],{"class":108},[102,757,758],{"class":592},"callback",[102,760,586],{"class":541},[102,762,763],{"class":108},"_version, ",[102,765,766],{"class":592},"is_eager",[102,768,586],{"class":541},[102,770,598],{"class":115},[102,772,126],{"class":108},[102,774,775,778,780,783,786,788,791],{"class":104,"line":486},[102,776,777],{"class":592},"                                    help",[102,779,586],{"class":541},[102,781,782],{"class":122},"\"Show the version and exit.\"",[102,784,785],{"class":108},")] ",[102,787,586],{"class":541},[102,789,790],{"class":115}," False",[102,792,126],{"class":108},[102,794,795,797,799],{"class":104,"line":495},[102,796,647],{"class":108},[102,798,650],{"class":115},[102,800,653],{"class":108},[102,802,803],{"class":104,"line":500},[102,804,805],{"class":122},"    \"\"\"{{ cookiecutter.description }}\"\"\"\n",[102,807,808],{"class":104,"line":515},[102,809,405],{"emptyLinePlaceholder":404},[102,811,813],{"class":104,"line":812},23,[102,814,405],{"emptyLinePlaceholder":404},[102,816,818,821],{"class":104,"line":817},24,[102,819,820],{"class":334},"@app.command",[102,822,730],{"class":108},[102,824,826,828,831,834,837,840,842,844,847,849,851,854,856,858],{"class":104,"line":825},25,[102,827,635],{"class":541},[102,829,830],{"class":334}," hello",[102,832,833],{"class":108},"(name: Annotated[",[102,835,836],{"class":115},"str",[102,838,839],{"class":108},", typer.Argument(",[102,841,603],{"class":592},[102,843,586],{"class":541},[102,845,846],{"class":122},"\"Who to greet.\"",[102,848,785],{"class":108},[102,850,586],{"class":541},[102,852,853],{"class":122}," \"world\"",[102,855,647],{"class":108},[102,857,650],{"class":115},[102,859,653],{"class":108},[102,861,863],{"class":104,"line":862},26,[102,864,865],{"class":122},"    \"\"\"Print a greeting (replace me with your first real command).\"\"\"\n",[102,867,869,872,874,877,880,883,885,888],{"class":104,"line":868},27,[102,870,871],{"class":108},"    typer.echo(",[102,873,669],{"class":541},[102,875,876],{"class":122},"\"Hello, ",[102,878,879],{"class":115},"{",[102,881,882],{"class":108},"name",[102,884,702],{"class":115},[102,886,887],{"class":122},"!\"",[102,889,622],{"class":108},[10,891,892,893,896,897,899,900,903,904,907,908,912,913,916,917,920,921,924,925,35],{},"The generated ",[14,894,895],{},"tests\u002Ftest_cli.py"," tests ",[14,898,16],{}," and ",[14,901,902],{},"hello"," with ",[14,905,906],{},"CliRunner",", so the new project starts with a passing suite and a pattern to copy — see ",[31,909,911],{"href":910},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","testing Click commands with CliRunner",". Including ",[14,914,915],{},"__main__.py"," (",[14,918,919],{},"from .cli import app; app()",") makes ",[14,922,923],{},"python -m package_name"," work too, as recommended in ",[31,926,928],{"href":927},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","best practices for Python CLI entry points",[930,931,933,934],"h3",{"id":932},"files-that-contain-their-own","Files that contain their own ",[14,935,936],{},"{{ }}",[10,938,939,940,943,944,947,948,951,952,954],{},"GitHub Actions workflows use ",[14,941,942],{},"${{ ... }}"," syntax, which Jinja will try to render. Wrap such files in ",[14,945,946],{},"{% raw %}...{% endraw %}",", or add them to ",[14,949,950],{},"_copy_without_render"," in ",[14,953,71],{},", so they are copied verbatim:",[78,956,958],{"className":96,"code":957,"language":98,"meta":84,"style":84},"\"_copy_without_render\": [\".github\u002Fworkflows\u002F*.yml\"]\n",[14,959,960],{"__ignoreMap":84},[102,961,962,965,967,970],{"class":104,"line":105},[102,963,964],{"class":122},"\"_copy_without_render\"",[102,966,200],{"class":108},[102,968,969],{"class":122},"\".github\u002Fworkflows\u002F*.yml\"",[102,971,338],{"class":108},[10,973,974,975,978],{},"If a workflow needs a template value as well — the Python version matrix, for instance — use ",[14,976,977],{},"{% raw %}"," around the Actions expressions only, and leave the cookiecutter expressions outside it.",[37,980,982],{"id":981},"evolving-the-template-over-time","Evolving the template over time",[10,984,985],{},"A template is never finished. The CI workflow gets a new job, the team adopts a new lint rule, Typer's recommended style changes, the oldest supported Python moves up. Treat the template like any other internal product:",[42,987,988,1003,1013,1019,1029],{},[45,989,990,994,995,998,999,1002],{},[991,992,993],"strong",{},"Version it with tags."," Users can then generate from a known-good version (",[14,996,997],{},"cookiecutter gh:acme\u002Fcookiecutter-typer-cli --checkout v3.2.0","), and a broken change on ",[14,1000,1001],{},"main"," does not affect everyone at once.",[45,1004,1005,1008,1009,1012],{},[991,1006,1007],{},"Keep a changelog."," Projects generated from older versions need to know what changed so they can adopt improvements by hand; a short entry per release — \"CI now tests Python 3.14\", \"added ",[14,1010,1011],{},"doctor"," command\" — is enough.",[45,1014,1015,1018],{},[991,1016,1017],{},"Extract from reality."," When a project generated from the template develops a better pattern, move it back into the template. The template should reflect the best current practice in your team's real tools, not an idealised design nobody uses.",[45,1020,1021,1024,1025,1028],{},[991,1022,1023],{},"Delete as readily as you add."," Every option multiplies the combinations you must test. If nobody chose ",[14,1026,1027],{},"include_docker = \"yes\""," in a year, remove the option.",[45,1030,1031,1034,1035,1038],{},[991,1032,1033],{},"Pin the tooling the template relies on."," If the post-generation hook runs ",[14,1036,1037],{},"uv lock",", document the minimum uv version the generated project expects, and test the template against it in CI.",[10,1040,1041],{},"A template that drifts out of date is worse than none, because it teaches outdated habits with the authority of an official starting point. A few minutes of maintenance per release keeps it the fastest way to start a correct CLI.",[37,1043,1045],{"id":1044},"ux-considerations","UX considerations",[10,1047,1048],{},"The users of a template are the developers who generate projects from it:",[42,1050,1051,1057,1067,1081],{},[45,1052,1053,1056],{},[991,1054,1055],{},"Ask little, derive a lot."," Every extra prompt is friction and a chance for inconsistency. Three or four questions is plenty.",[45,1058,1059,1062,1063,1066],{},[991,1060,1061],{},"Make the first run succeed."," The generated project should install, pass its tests and print ",[14,1064,1065],{},"--help"," immediately. A template whose output needs fixing teaches people to distrust it.",[45,1068,1069,1072,1073,1076,1077,35],{},[991,1070,1071],{},"Print the next steps."," A post-generation hook that prints ",[14,1074,1075],{},"cd my-tool && uv sync && uv run my-tool --help"," gets people moving; see ",[31,1078,1080],{"href":1079},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates\u002F","post-generation hooks in CLI templates",[45,1082,1083,1086],{},[991,1084,1085],{},"Keep opinions documented."," The generated README should say why the layout is what it is and link to the team's conventions, so the template teaches rather than just copies.",[74,1088],{"name":1089},"ck-tpl-terminal",[37,1091,1093],{"id":1092},"testing-the-behaviour","Testing the behaviour",[10,1095,1096,1097,1100],{},"A template is software, and it breaks like software: a renamed variable, a Jinja typo, a workflow that no longer parses. Test it by generating projects and running them. ",[14,1098,1099],{},"cookiecutter"," has a Python API that makes this straightforward in pytest:",[78,1102,1104],{"className":527,"code":1103,"language":529,"meta":84,"style":84},"# tests\u002Ftest_template.py\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nimport pytest\nfrom cookiecutter.main import cookiecutter\n\nTEMPLATE = Path(__file__).parent.parent\n\n\n@pytest.mark.parametrize(\"docker\", [\"no\", \"yes\"])\ndef test_generated_project_works(tmp_path, docker):\n    out = cookiecutter(\n        str(TEMPLATE), no_input=True, output_dir=str(tmp_path),\n        extra_context={\"project_name\": \"Deploy Helper\", \"command_name\": \"dh\",\n                       \"include_docker\": docker},\n    )\n    project = Path(out)\n    assert project.name == \"deploy-helper\"\n    assert (project \u002F \"src\" \u002F \"deploy_helper\" \u002F \"cli.py\").exists()\n    assert (project \u002F \"Dockerfile\").exists() == (docker == \"yes\")\n\n    def run(*cmd: str) -> subprocess.CompletedProcess[str]:\n        return subprocess.run(cmd, cwd=project, capture_output=True, text=True, check=True)\n\n    run(\"uv\", \"sync\", \"--group\", \"dev\")\n    run(\"uv\", \"run\", \"pytest\", \"-q\")\n    assert run(\"uv\", \"run\", \"dh\", \"--version\").stdout.strip() == \"dh 0.1.0\"\n    run(\"uv\", \"run\", \"ruff\", \"check\", \".\")\n",[14,1105,1106,1111,1118,1125,1137,1141,1148,1160,1164,1181,1185,1189,1212,1222,1232,1263,1292,1300,1305,1315,1329,1356,1382,1386,1412,1454,1458,1483,1507,1537],{"__ignoreMap":84},[102,1107,1108],{"class":104,"line":105},[102,1109,1110],{"class":325},"# tests\u002Ftest_template.py\n",[102,1112,1113,1115],{"class":104,"line":112},[102,1114,548],{"class":541},[102,1116,1117],{"class":108}," subprocess\n",[102,1119,1120,1122],{"class":104,"line":129},[102,1121,548],{"class":541},[102,1123,1124],{"class":108}," sys\n",[102,1126,1127,1129,1132,1134],{"class":104,"line":142},[102,1128,542],{"class":541},[102,1130,1131],{"class":108}," pathlib ",[102,1133,548],{"class":541},[102,1135,1136],{"class":108}," Path\n",[102,1138,1139],{"class":104,"line":155},[102,1140,405],{"emptyLinePlaceholder":404},[102,1142,1143,1145],{"class":104,"line":168},[102,1144,548],{"class":541},[102,1146,1147],{"class":108}," pytest\n",[102,1149,1150,1152,1155,1157],{"class":104,"line":181},[102,1151,542],{"class":541},[102,1153,1154],{"class":108}," cookiecutter.main ",[102,1156,548],{"class":541},[102,1158,1159],{"class":108}," cookiecutter\n",[102,1161,1162],{"class":104,"line":194},[102,1163,405],{"emptyLinePlaceholder":404},[102,1165,1166,1169,1172,1175,1178],{"class":104,"line":220},[102,1167,1168],{"class":115},"TEMPLATE",[102,1170,1171],{"class":541}," =",[102,1173,1174],{"class":108}," Path(",[102,1176,1177],{"class":115},"__file__",[102,1179,1180],{"class":108},").parent.parent\n",[102,1182,1183],{"class":104,"line":238},[102,1184,405],{"emptyLinePlaceholder":404},[102,1186,1187],{"class":104,"line":247},[102,1188,405],{"emptyLinePlaceholder":404},[102,1190,1191,1194,1197,1200,1203,1205,1207,1209],{"class":104,"line":260},[102,1192,1193],{"class":334},"@pytest.mark.parametrize",[102,1195,1196],{"class":108},"(",[102,1198,1199],{"class":122},"\"docker\"",[102,1201,1202],{"class":108},", [",[102,1204,228],{"class":122},[102,1206,206],{"class":108},[102,1208,233],{"class":122},[102,1210,1211],{"class":108},"])\n",[102,1213,1214,1216,1219],{"class":104,"line":273},[102,1215,635],{"class":541},[102,1217,1218],{"class":334}," test_generated_project_works",[102,1220,1221],{"class":108},"(tmp_path, docker):\n",[102,1223,1224,1227,1229],{"class":104,"line":284},[102,1225,1226],{"class":108},"    out ",[102,1228,586],{"class":541},[102,1230,1231],{"class":108}," cookiecutter(\n",[102,1233,1234,1237,1239,1241,1244,1247,1249,1251,1253,1256,1258,1260],{"class":104,"line":290},[102,1235,1236],{"class":115},"        str",[102,1238,1196],{"class":108},[102,1240,1168],{"class":115},[102,1242,1243],{"class":108},"), ",[102,1245,1246],{"class":592},"no_input",[102,1248,586],{"class":541},[102,1250,598],{"class":115},[102,1252,206],{"class":108},[102,1254,1255],{"class":592},"output_dir",[102,1257,586],{"class":541},[102,1259,836],{"class":115},[102,1261,1262],{"class":108},"(tmp_path),\n",[102,1264,1265,1268,1270,1272,1275,1277,1280,1282,1285,1287,1290],{"class":104,"line":460},[102,1266,1267],{"class":592},"        extra_context",[102,1269,586],{"class":541},[102,1271,879],{"class":108},[102,1273,1274],{"class":122},"\"project_name\"",[102,1276,119],{"class":108},[102,1278,1279],{"class":122},"\"Deploy Helper\"",[102,1281,206],{"class":108},[102,1283,1284],{"class":122},"\"command_name\"",[102,1286,119],{"class":108},[102,1288,1289],{"class":122},"\"dh\"",[102,1291,126],{"class":108},[102,1293,1294,1297],{"class":104,"line":465},[102,1295,1296],{"class":122},"                       \"include_docker\"",[102,1298,1299],{"class":108},": docker},\n",[102,1301,1302],{"class":104,"line":475},[102,1303,1304],{"class":108},"    )\n",[102,1306,1307,1310,1312],{"class":104,"line":486},[102,1308,1309],{"class":108},"    project ",[102,1311,586],{"class":541},[102,1313,1314],{"class":108}," Path(out)\n",[102,1316,1317,1320,1323,1326],{"class":104,"line":495},[102,1318,1319],{"class":541},"    assert",[102,1321,1322],{"class":108}," project.name ",[102,1324,1325],{"class":541},"==",[102,1327,1328],{"class":122}," \"deploy-helper\"\n",[102,1330,1331,1333,1336,1339,1342,1345,1348,1350,1353],{"class":104,"line":500},[102,1332,1319],{"class":541},[102,1334,1335],{"class":108}," (project ",[102,1337,1338],{"class":541},"\u002F",[102,1340,1341],{"class":122}," \"src\"",[102,1343,1344],{"class":541}," \u002F",[102,1346,1347],{"class":122}," \"deploy_helper\"",[102,1349,1344],{"class":541},[102,1351,1352],{"class":122}," \"cli.py\"",[102,1354,1355],{"class":108},").exists()\n",[102,1357,1358,1360,1362,1364,1367,1370,1372,1375,1377,1380],{"class":104,"line":515},[102,1359,1319],{"class":541},[102,1361,1335],{"class":108},[102,1363,1338],{"class":541},[102,1365,1366],{"class":122}," \"Dockerfile\"",[102,1368,1369],{"class":108},").exists() ",[102,1371,1325],{"class":541},[102,1373,1374],{"class":108}," (docker ",[102,1376,1325],{"class":541},[102,1378,1379],{"class":122}," \"yes\"",[102,1381,622],{"class":108},[102,1383,1384],{"class":104,"line":812},[102,1385,405],{"emptyLinePlaceholder":404},[102,1387,1388,1391,1394,1396,1399,1402,1404,1407,1409],{"class":104,"line":817},[102,1389,1390],{"class":541},"    def",[102,1392,1393],{"class":334}," run",[102,1395,1196],{"class":108},[102,1397,1398],{"class":541},"*",[102,1400,1401],{"class":108},"cmd: ",[102,1403,836],{"class":115},[102,1405,1406],{"class":108},") -> subprocess.CompletedProcess[",[102,1408,836],{"class":115},[102,1410,1411],{"class":108},"]:\n",[102,1413,1414,1417,1420,1423,1425,1428,1431,1433,1435,1437,1439,1441,1443,1445,1448,1450,1452],{"class":104,"line":825},[102,1415,1416],{"class":541},"        return",[102,1418,1419],{"class":108}," subprocess.run(cmd, ",[102,1421,1422],{"class":592},"cwd",[102,1424,586],{"class":541},[102,1426,1427],{"class":108},"project, ",[102,1429,1430],{"class":592},"capture_output",[102,1432,586],{"class":541},[102,1434,598],{"class":115},[102,1436,206],{"class":108},[102,1438,83],{"class":592},[102,1440,586],{"class":541},[102,1442,598],{"class":115},[102,1444,206],{"class":108},[102,1446,1447],{"class":592},"check",[102,1449,586],{"class":541},[102,1451,598],{"class":115},[102,1453,622],{"class":108},[102,1455,1456],{"class":104,"line":862},[102,1457,405],{"emptyLinePlaceholder":404},[102,1459,1460,1463,1466,1468,1471,1473,1476,1478,1481],{"class":104,"line":868},[102,1461,1462],{"class":108},"    run(",[102,1464,1465],{"class":122},"\"uv\"",[102,1467,206],{"class":108},[102,1469,1470],{"class":122},"\"sync\"",[102,1472,206],{"class":108},[102,1474,1475],{"class":122},"\"--group\"",[102,1477,206],{"class":108},[102,1479,1480],{"class":122},"\"dev\"",[102,1482,622],{"class":108},[102,1484,1486,1488,1490,1492,1495,1497,1500,1502,1505],{"class":104,"line":1485},28,[102,1487,1462],{"class":108},[102,1489,1465],{"class":122},[102,1491,206],{"class":108},[102,1493,1494],{"class":122},"\"run\"",[102,1496,206],{"class":108},[102,1498,1499],{"class":122},"\"pytest\"",[102,1501,206],{"class":108},[102,1503,1504],{"class":122},"\"-q\"",[102,1506,622],{"class":108},[102,1508,1510,1512,1515,1517,1519,1521,1523,1525,1527,1529,1532,1534],{"class":104,"line":1509},29,[102,1511,1319],{"class":541},[102,1513,1514],{"class":108}," run(",[102,1516,1465],{"class":122},[102,1518,206],{"class":108},[102,1520,1494],{"class":122},[102,1522,206],{"class":108},[102,1524,1289],{"class":122},[102,1526,206],{"class":108},[102,1528,753],{"class":122},[102,1530,1531],{"class":108},").stdout.strip() ",[102,1533,1325],{"class":541},[102,1535,1536],{"class":122}," \"dh 0.1.0\"\n",[102,1538,1540,1542,1544,1546,1548,1550,1553,1555,1558,1560,1563],{"class":104,"line":1539},30,[102,1541,1462],{"class":108},[102,1543,1465],{"class":122},[102,1545,206],{"class":108},[102,1547,1494],{"class":122},[102,1549,206],{"class":108},[102,1551,1552],{"class":122},"\"ruff\"",[102,1554,206],{"class":108},[102,1556,1557],{"class":122},"\"check\"",[102,1559,206],{"class":108},[102,1561,1562],{"class":122},"\".\"",[102,1564,622],{"class":108},[10,1566,1567,1568,1570],{},"This single test catches almost every class of template bug: rendering errors, broken derived names, a ",[14,1569,28],{}," that does not build, a first command that fails its own tests, and lint violations in the generated code. Run it in the template repository's CI across the same Python versions the template offers. The Docker parameter shows the pattern for optional features: every combination the template supports should be generated at least once.",[37,1572,1574],{"id":1573},"conclusion","Conclusion",[10,1576,1577,1578,1580],{},"A good CLI template is a small, opinionated, tested program that generates other programs. Ask for a project name and derive the rest, generate a ",[14,1579,28],{}," with a console script, a first command that already follows your conventions, tests and CI, protect workflow files from Jinja, and test the template by generating projects and running their tests. New CLIs then start from the team's best current practice instead of from whichever old project was copied last.",[37,1582,1584],{"id":1583},"frequently-asked-questions","Frequently asked questions",[930,1586,1588],{"id":1587},"how-do-existing-projects-get-template-updates","How do existing projects get template updates?",[10,1590,1591,1592,1595],{},"Cookiecutter generates once and forgets. Tools like ",[14,1593,1594],{},"cruft"," record the template version and can apply later template changes as a diff. If keeping many projects in sync matters, Copier's built-in update support is the stronger reason to choose it.",[930,1597,1599],{"id":1598},"should-the-template-pin-dependency-versions","Should the template pin dependency versions?",[10,1601,1602,1603,916,1605,1608,1609,1611],{},"Use lower bounds in ",[14,1604,28],{},[14,1606,1607],{},"typer>=0.12",") and let the post-generation hook run ",[14,1610,1037],{}," so each new project starts with current, locked versions. Pinning exact versions in the template makes every new project start out of date.",[930,1613,1615],{"id":1614},"can-i-host-the-template-privately","Can I host the template privately?",[10,1617,1618,1619,1622],{},"Yes. ",[14,1620,1621],{},"cookiecutter gh:org\u002Ftemplate"," works with private GitHub repositories if your git credentials allow access, and any git URL or local path works too. A zip file served internally is another option for restricted networks.",[930,1624,1626],{"id":1625},"how-do-i-include-a-license-choice","How do I include a license choice?",[10,1628,1629,1630,1633],{},"Add a choice variable (",[14,1631,1632],{},"\"license\": [\"MIT\", \"Apache-2.0\", \"Proprietary\"]","), keep one license text per option in the template, and have the post-generation hook delete the files for the options not chosen.",[37,1635,1637],{"id":1636},"related","Related",[42,1639,1640,1646,1651,1655,1661],{},[45,1641,1642,1643],{},"Up: ",[31,1644,1645],{"href":33},"CLI project scaffolding with cookiecutter",[45,1647,1648],{},[31,1649,1650],{"href":1079},"Post-generation hooks in CLI templates",[45,1652,1653],{},[31,1654,60],{"href":59},[45,1656,1657],{},[31,1658,1660],{"href":1659},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project\u002F","How to structure a large Python CLI project",[45,1662,1663],{},[31,1664,1666],{"href":1665},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli\u002F","Writing pyproject.toml metadata for a CLI",[1668,1669,1670],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":84,"searchDepth":112,"depth":112,"links":1672},[1673,1674,1675,1676,1680,1681,1682,1683,1684,1690],{"id":39,"depth":112,"text":40},{"id":64,"depth":112,"text":65},{"id":89,"depth":112,"text":90},{"id":306,"depth":112,"text":307,"children":1677},[1678],{"id":932,"depth":129,"text":1679},"Files that contain their own {{ }}",{"id":981,"depth":112,"text":982},{"id":1044,"depth":112,"text":1045},{"id":1092,"depth":112,"text":1093},{"id":1573,"depth":112,"text":1574},{"id":1583,"depth":112,"text":1584,"children":1685},[1686,1687,1688,1689],{"id":1587,"depth":129,"text":1588},{"id":1598,"depth":129,"text":1599},{"id":1614,"depth":129,"text":1615},{"id":1625,"depth":129,"text":1626},{"id":1636,"depth":112,"text":1637},"2026-09-18","Create a cookiecutter template that generates a working Typer CLI: derived names, pyproject with entry points, tests, CI, and a test suite for the template itself.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis",{"title":5,"description":1692},"project-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis\u002Findex",[1099,1701,1702,1703],"templates","typer","scaffolding","Gs4s8tOKTQ8CysN6evHaNaBASqbcz2dq23ulBE-B5Xc",[1706,1709,1712,1715,1718,1721,1724,1727,1730,1733,1736,1739,1742,1745,1748,1751,1754,1757,1760,1763,1766,1769,1772,1775,1778,1781,1784,1787,1790,1793,1796,1799,1802,1805,1808,1811,1814,1817,1820,1823,1826,1829,1832,1835,1838,1841,1844,1847,1850,1853,1856,1859,1862,1865,1868,1871,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1981,1984,1987,1990,1993,1996,1999,2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2092,2095,2098,2101,2104,2107,2110,2113,2116,2119,2122,2125,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195,2198,2201,2204,2207,2210,2213,2216,2219,2222,2225,2228,2231,2234,2237,2240,2243,2246,2249],{"path":1707,"title":1708},"\u002Fabout","About Python CLI Toolcraft",{"path":1710,"title":1711},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1713,"title":1714},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1716,"title":1717},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1719,"title":1720},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1722,"title":1723},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1725,"title":1726},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1728,"title":1729},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1731,"title":1732},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1734,"title":1735},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1737,"title":1738},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":1740,"title":1741},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":1743,"title":1744},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1746,"title":1747},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1749,"title":1750},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1752,"title":1753},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1755,"title":1756},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1758,"title":1759},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1761,"title":1762},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1764,"title":1765},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1767,"title":1768},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1770,"title":1771},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1773,"title":1774},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1776,"title":1777},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1779,"title":1780},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1782,"title":1783},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1785,"title":1786},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1788,"title":1789},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1791,"title":1792},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1794,"title":1795},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":1797,"title":1798},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1800,"title":1801},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1803,"title":1804},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1806,"title":1807},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":1809,"title":1810},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1812,"title":1813},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1815,"title":1816},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1818,"title":1819},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1821,"title":1822},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1824,"title":1825},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1827,"title":1828},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":1830,"title":1831},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1833,"title":1834},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":1836,"title":1837},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1839,"title":1840},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1842,"title":1843},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1845,"title":1846},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1848,"title":1849},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1851,"title":1852},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":1854,"title":1855},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1857,"title":1858},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1860,"title":1861},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1863,"title":1864},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1866,"title":1867},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":1869,"title":1870},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1872,"title":1873},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":1875,"title":1876},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1878,"title":1879},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1881,"title":1882},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1884,"title":1885},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1887,"title":1888},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":1890,"title":1891},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":1893,"title":1894},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1896,"title":1897},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1899,"title":1900},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1902,"title":1903},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1905,"title":1906},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":1908,"title":1909},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1911,"title":1912},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1914,"title":1915},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1917,"title":1918},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1920,"title":1921},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1923,"title":1924},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1926,"title":1927},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":1929,"title":1930},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":1932,"title":1933},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1935,"title":1936},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":1938,"title":1939},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1941,"title":1942},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":1944,"title":1945},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1947,"title":1948},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1950,"title":1951},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1953,"title":1954},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1956,"title":1957},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1959,"title":1960},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":1962,"title":1963},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1965,"title":1966},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1968,"title":1969},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":1971,"title":1972},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":1974,"title":1975},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1977,"title":1978},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1338,"title":1980},"Python CLI Toolcraft",{"path":1982,"title":1983},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":1985,"title":1986},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1988,"title":1989},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1991,"title":1992},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1994,"title":1995},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1997,"title":1998},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2000,"title":2001},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2003,"title":2004},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2006,"title":2007},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2009,"title":2010},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2012,"title":2013},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2015,"title":2016},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2018,"title":2019},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2021,"title":2022},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2024,"title":2025},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2027,"title":2028},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2030,"title":2031},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2033,"title":2034},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2036,"title":2037},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2039,"title":2040},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2042,"title":2043},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2045,"title":2046},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2048,"title":2049},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2051,"title":2052},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2054,"title":2055},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2057,"title":2058},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2060,"title":2061},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2063,"title":2064},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2066,"title":2067},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2069,"title":2070},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2072,"title":2073},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2075,"title":2076},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2078,"title":2079},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2081,"title":2082},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2084,"title":2085},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2087,"title":2088},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2090,"title":2091},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2093,"title":2094},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2096,"title":2097},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2099,"title":2100},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2102,"title":2103},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2105,"title":2106},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2108,"title":2109},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2111,"title":2112},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2114,"title":2115},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2117,"title":2118},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2120,"title":2121},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2123,"title":2124},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":1697,"title":5},{"path":2127,"title":2128},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2130,"title":2131},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2133,"title":2134},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2136,"title":2137},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2139,"title":2140},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2142,"title":2143},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2145,"title":2146},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2148,"title":2149},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2151,"title":2152},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2154,"title":2155},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2157,"title":2158},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2160,"title":2161},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2163,"title":2164},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2166,"title":2167},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2169,"title":2170},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2172,"title":2173},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2175,"title":2176},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2178,"title":2179},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2181,"title":2182},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2184,"title":2185},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2187,"title":2188},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2190,"title":2191},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2193,"title":2194},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2196,"title":2197},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2199,"title":2200},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2202,"title":2203},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2205,"title":2206},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2208,"title":2209},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2211,"title":2212},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2214,"title":2215},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2217,"title":2218},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2220,"title":2221},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2223,"title":2224},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2226,"title":2227},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2229,"title":2230},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2232,"title":2233},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2235,"title":2236},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2238,"title":2239},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2241,"title":2242},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2244,"title":2245},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2247,"title":2248},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2250,"title":2251},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907474]