[{"data":1,"prerenderedAt":2575},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click\u002F":3,"content-directory":1731},{"id":4,"title":5,"body":6,"date":1715,"description":1716,"difficulty":1717,"draft":1718,"extension":1719,"meta":1720,"navigation":169,"path":1721,"seo":1722,"stem":1723,"tags":1724,"updated":1715,"__hash__":1730},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click\u002Findex.md","Choices and Enums in Typer and Click Options",{"type":7,"value":8,"toc":1692},"minimark",[9,59,64,82,86,90,112,116,516,519,527,533,568,578,594,597,601,838,868,872,875,886,920,935,939,962,1077,1084,1088,1132,1136,1139,1529,1532,1536,1555,1559,1563,1570,1574,1584,1592,1613,1617,1637,1641,1655,1659,1688],[10,11,12,13,17,18,17,21,24,25,17,28,31,32,35,36,39,40,43,44,47,48,52,53,58],"p",{},"Many options accept one of a fixed set of values: an environment (",[14,15,16],"code",{},"dev",", ",[14,19,20],{},"staging",[14,22,23],{},"production","), an output format (",[14,26,27],{},"table",[14,29,30],{},"json","), a log level. Declaring that set — instead of accepting any string and checking it later — gives you validation with a clear error, the choices listed in ",[14,33,34],{},"--help",", shell completion for free, and a typed value inside the command. Typer and Click both support it, through Python ",[14,37,38],{},"Enum"," and ",[14,41,42],{},"Literal"," types in Typer and ",[14,45,46],{},"click.Choice"," in Click. They differ in one detail that bites during migrations and refactors: ",[49,50,51],"strong",{},"Typer matches an enum's values; Click matches its names."," This guide shows each approach, that difference, case-insensitive matching, repeatable choices and how to test them. It belongs to the ",[54,55,57],"a",{"href":56},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click topic",".",[60,61,63],"h2",{"id":62},"prerequisites","Prerequisites",[65,66,67,71],"ul",{},[68,69,70],"li",{},"Typer 0.12+ (examples checked with 0.27) or Click 8.2+ (checked with 8.5).",[68,72,73,74,77,78,58],{},"The ",[14,75,76],{},"Annotated"," style from ",[54,79,81],{"href":80},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002F","using Annotated options in Typer",[60,83,85],{"id":84},"three-ways-to-declare-a-choice","Three ways to declare a choice",[87,88],"inline-diagram",{"name":89},"choice-ways",[10,91,92,97,98,103,104,108,109,111],{},[49,93,94,95],{},"An ",[14,96,38],{}," is the richest: the command receives an enum member, so code can attach behaviour or extra data to each choice, and type checkers know every possible value. ",[49,99,100,101],{},"A ",[14,102,42],{}," (in Typer) is the lightest: the command receives a plain string, and the allowed values live right in the signature. ",[49,105,106],{},[14,107,46],{}," with a list of strings is Click's classic form; since Click 8.2 it also accepts an ",[14,110,38],{}," class directly.",[60,113,115],{"id":114},"the-recipe-typer","The recipe: Typer",[117,118,123],"pre",{"className":119,"code":120,"language":121,"meta":122,"style":122},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcli.py\nfrom enum import Enum\nfrom typing import Annotated, Literal\n\nimport typer\n\napp = typer.Typer()\n\n\nclass Env(str, Enum):\n    dev = \"dev\"\n    staging = \"staging\"\n    prod = \"production\"\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment tool.\"\"\"\n\n\n@app.command()\ndef deploy(\n    env: Annotated[Env, typer.Option(case_sensitive=False, help=\"Target environment.\")] = Env.dev,\n    fmt: Annotated[Literal[\"table\", \"json\"], typer.Option(\"--format\")] = \"table\",\n    regions: Annotated[list[Env], typer.Option(\"--region\", help=\"Repeat for several.\")] = [],\n) -> None:\n    \"\"\"Deploy to an environment.\"\"\"\n    typer.echo(f\"deploying to {env.value} ({fmt}); extra regions: \"\n               f\"{', '.join(r.value for r in regions) or 'none'}\")\n","python","",[14,124,125,134,151,164,171,179,184,196,201,206,230,242,253,264,269,274,283,301,307,312,317,325,336,369,399,424,434,440,474],{"__ignoreMap":122},[126,127,130],"span",{"class":128,"line":129},"line",1,[126,131,133],{"class":132},"sJ8bj","# src\u002Fmytool\u002Fcli.py\n",[126,135,137,141,145,148],{"class":128,"line":136},2,[126,138,140],{"class":139},"szBVR","from",[126,142,144],{"class":143},"sVt8B"," enum ",[126,146,147],{"class":139},"import",[126,149,150],{"class":143}," Enum\n",[126,152,154,156,159,161],{"class":128,"line":153},3,[126,155,140],{"class":139},[126,157,158],{"class":143}," typing ",[126,160,147],{"class":139},[126,162,163],{"class":143}," Annotated, Literal\n",[126,165,167],{"class":128,"line":166},4,[126,168,170],{"emptyLinePlaceholder":169},true,"\n",[126,172,174,176],{"class":128,"line":173},5,[126,175,147],{"class":139},[126,177,178],{"class":143}," typer\n",[126,180,182],{"class":128,"line":181},6,[126,183,170],{"emptyLinePlaceholder":169},[126,185,187,190,193],{"class":128,"line":186},7,[126,188,189],{"class":143},"app ",[126,191,192],{"class":139},"=",[126,194,195],{"class":143}," typer.Typer()\n",[126,197,199],{"class":128,"line":198},8,[126,200,170],{"emptyLinePlaceholder":169},[126,202,204],{"class":128,"line":203},9,[126,205,170],{"emptyLinePlaceholder":169},[126,207,209,212,216,219,223,225,227],{"class":128,"line":208},10,[126,210,211],{"class":139},"class",[126,213,215],{"class":214},"sScJk"," Env",[126,217,218],{"class":143},"(",[126,220,222],{"class":221},"sj4cs","str",[126,224,17],{"class":143},[126,226,38],{"class":214},[126,228,229],{"class":143},"):\n",[126,231,233,236,238],{"class":128,"line":232},11,[126,234,235],{"class":143},"    dev ",[126,237,192],{"class":139},[126,239,241],{"class":240},"sZZnC"," \"dev\"\n",[126,243,245,248,250],{"class":128,"line":244},12,[126,246,247],{"class":143},"    staging ",[126,249,192],{"class":139},[126,251,252],{"class":240}," \"staging\"\n",[126,254,256,259,261],{"class":128,"line":255},13,[126,257,258],{"class":143},"    prod ",[126,260,192],{"class":139},[126,262,263],{"class":240}," \"production\"\n",[126,265,267],{"class":128,"line":266},14,[126,268,170],{"emptyLinePlaceholder":169},[126,270,272],{"class":128,"line":271},15,[126,273,170],{"emptyLinePlaceholder":169},[126,275,277,280],{"class":128,"line":276},16,[126,278,279],{"class":214},"@app.callback",[126,281,282],{"class":143},"()\n",[126,284,286,289,292,295,298],{"class":128,"line":285},17,[126,287,288],{"class":139},"def",[126,290,291],{"class":214}," main",[126,293,294],{"class":143},"() -> ",[126,296,297],{"class":221},"None",[126,299,300],{"class":143},":\n",[126,302,304],{"class":128,"line":303},18,[126,305,306],{"class":240},"    \"\"\"Deployment tool.\"\"\"\n",[126,308,310],{"class":128,"line":309},19,[126,311,170],{"emptyLinePlaceholder":169},[126,313,315],{"class":128,"line":314},20,[126,316,170],{"emptyLinePlaceholder":169},[126,318,320,323],{"class":128,"line":319},21,[126,321,322],{"class":214},"@app.command",[126,324,282],{"class":143},[126,326,328,330,333],{"class":128,"line":327},22,[126,329,288],{"class":139},[126,331,332],{"class":214}," deploy",[126,334,335],{"class":143},"(\n",[126,337,339,342,346,348,351,353,356,358,361,364,366],{"class":128,"line":338},23,[126,340,341],{"class":143},"    env: Annotated[Env, typer.Option(",[126,343,345],{"class":344},"s4XuR","case_sensitive",[126,347,192],{"class":139},[126,349,350],{"class":221},"False",[126,352,17],{"class":143},[126,354,355],{"class":344},"help",[126,357,192],{"class":139},[126,359,360],{"class":240},"\"Target environment.\"",[126,362,363],{"class":143},")] ",[126,365,192],{"class":139},[126,367,368],{"class":143}," Env.dev,\n",[126,370,372,375,378,380,383,386,389,391,393,396],{"class":128,"line":371},24,[126,373,374],{"class":143},"    fmt: Annotated[Literal[",[126,376,377],{"class":240},"\"table\"",[126,379,17],{"class":143},[126,381,382],{"class":240},"\"json\"",[126,384,385],{"class":143},"], typer.Option(",[126,387,388],{"class":240},"\"--format\"",[126,390,363],{"class":143},[126,392,192],{"class":139},[126,394,395],{"class":240}," \"table\"",[126,397,398],{"class":143},",\n",[126,400,402,405,408,410,412,414,417,419,421],{"class":128,"line":401},25,[126,403,404],{"class":143},"    regions: Annotated[list[Env], typer.Option(",[126,406,407],{"class":240},"\"--region\"",[126,409,17],{"class":143},[126,411,355],{"class":344},[126,413,192],{"class":139},[126,415,416],{"class":240},"\"Repeat for several.\"",[126,418,363],{"class":143},[126,420,192],{"class":139},[126,422,423],{"class":143}," [],\n",[126,425,427,430,432],{"class":128,"line":426},26,[126,428,429],{"class":143},") -> ",[126,431,297],{"class":221},[126,433,300],{"class":143},[126,435,437],{"class":128,"line":436},27,[126,438,439],{"class":240},"    \"\"\"Deploy to an environment.\"\"\"\n",[126,441,443,446,449,452,455,458,461,464,466,469,471],{"class":128,"line":442},28,[126,444,445],{"class":143},"    typer.echo(",[126,447,448],{"class":139},"f",[126,450,451],{"class":240},"\"deploying to ",[126,453,454],{"class":221},"{",[126,456,457],{"class":143},"env.value",[126,459,460],{"class":221},"}",[126,462,463],{"class":240}," (",[126,465,454],{"class":221},[126,467,468],{"class":143},"fmt",[126,470,460],{"class":221},[126,472,473],{"class":240},"); extra regions: \"\n",[126,475,477,480,483,485,488,491,494,497,500,503,506,509,511,513],{"class":128,"line":476},29,[126,478,479],{"class":139},"               f",[126,481,482],{"class":240},"\"",[126,484,454],{"class":221},[126,486,487],{"class":240},"', '",[126,489,490],{"class":143},".join(r.value ",[126,492,493],{"class":139},"for",[126,495,496],{"class":143}," r ",[126,498,499],{"class":139},"in",[126,501,502],{"class":143}," regions) ",[126,504,505],{"class":139},"or",[126,507,508],{"class":240}," 'none'",[126,510,460],{"class":221},[126,512,482],{"class":240},[126,514,515],{"class":143},")\n",[10,517,518],{},"The help lists each set of choices, and invalid input is a usage error with exit code 2:",[117,520,525],{"className":521,"code":523,"language":524,"meta":122},[522],"language-text","╭─ Options ──────────────────────────────────────────────────────────╮\n│ --env           \u003Cdev|staging|production>  Target environment.      │\n│                                           [default: dev]           │\n│ --format        \u003Ctable|json>              [default: table]         │\n│ --region        \u003Cdev|staging|production>  Repeat for several.      │\n╰────────────────────────────────────────────────────────────────────╯\n","text",[14,526,523],{"__ignoreMap":122},[117,528,531],{"className":529,"code":530,"language":524,"meta":122},[522],"$ mytool deploy --env prod\n│ Invalid value for '--env': 'prod' is not one of 'dev', 'staging', 'production'. │\n",[14,532,530],{"__ignoreMap":122},[10,534,535,536,463,539,541,542,545,546,549,550,553,554,557,558,561,562,565,566,58],{},"Note what Typer accepted and rejected: the user types the enum's ",[49,537,538],{},"value",[14,540,23],{},"), not its name (",[14,543,544],{},"prod","). ",[14,547,548],{},"case_sensitive=False"," lets ",[14,551,552],{},"PRODUCTION"," through too. A ",[14,555,556],{},"list[Env]"," option can be repeated (",[14,559,560],{},"--region dev --region staging","), each value validated against the same set. Using a mutable default (",[14,563,564],{},"[]",") is safe here because Typer builds a fresh list for each invocation; with plain functions you would use ",[14,567,297],{},[569,570,572,573,575,576],"h3",{"id":571},"when-to-choose-enum-vs-literal","When to choose ",[14,574,38],{}," vs ",[14,577,42],{},[10,579,580,581,583,584,586,587,589,590,593],{},"Use ",[14,582,42],{}," for a handful of format-like strings the code only compares against. Use ",[14,585,38],{}," when choices carry meaning beyond their spelling — a mapping to API identifiers, a display label, a method that does the work — or when the same set is used by several commands and should live in one place. Subclassing ",[14,588,222],{}," (as ",[14,591,592],{},"Env(str, Enum)"," does) keeps the members usable wherever a string is expected, including JSON serialisation.",[87,595],{"name":596},"choice-terminal",[60,598,600],{"id":599},"the-recipe-click","The recipe: Click",[117,602,604],{"className":119,"code":603,"language":121,"meta":122,"style":122},"# src\u002Fmytool\u002Fclick_cli.py\nfrom enum import Enum\n\nimport click\n\n\nclass Env(Enum):\n    dev = \"dev\"\n    staging = \"staging\"\n    prod = \"production\"\n\n\n@click.command()\n@click.option(\"--env\", type=click.Choice(Env, case_sensitive=False), default=\"dev\",\n              show_default=True, help=\"Target environment.\")\n@click.option(\"--format\", \"fmt\", type=click.Choice([\"table\", \"json\"]), default=\"table\")\ndef deploy(env: Env, fmt: str) -> None:\n    \"\"\"Deploy to an environment.\"\"\"\n    click.echo(f\"deploying to {env.value} ({fmt})\")\n",[14,605,606,611,621,625,632,636,640,652,660,668,676,680,684,691,730,750,789,806,810],{"__ignoreMap":122},[126,607,608],{"class":128,"line":129},[126,609,610],{"class":132},"# src\u002Fmytool\u002Fclick_cli.py\n",[126,612,613,615,617,619],{"class":128,"line":136},[126,614,140],{"class":139},[126,616,144],{"class":143},[126,618,147],{"class":139},[126,620,150],{"class":143},[126,622,623],{"class":128,"line":153},[126,624,170],{"emptyLinePlaceholder":169},[126,626,627,629],{"class":128,"line":166},[126,628,147],{"class":139},[126,630,631],{"class":143}," click\n",[126,633,634],{"class":128,"line":173},[126,635,170],{"emptyLinePlaceholder":169},[126,637,638],{"class":128,"line":181},[126,639,170],{"emptyLinePlaceholder":169},[126,641,642,644,646,648,650],{"class":128,"line":186},[126,643,211],{"class":139},[126,645,215],{"class":214},[126,647,218],{"class":143},[126,649,38],{"class":214},[126,651,229],{"class":143},[126,653,654,656,658],{"class":128,"line":198},[126,655,235],{"class":143},[126,657,192],{"class":139},[126,659,241],{"class":240},[126,661,662,664,666],{"class":128,"line":203},[126,663,247],{"class":143},[126,665,192],{"class":139},[126,667,252],{"class":240},[126,669,670,672,674],{"class":128,"line":208},[126,671,258],{"class":143},[126,673,192],{"class":139},[126,675,263],{"class":240},[126,677,678],{"class":128,"line":232},[126,679,170],{"emptyLinePlaceholder":169},[126,681,682],{"class":128,"line":244},[126,683,170],{"emptyLinePlaceholder":169},[126,685,686,689],{"class":128,"line":255},[126,687,688],{"class":214},"@click.command",[126,690,282],{"class":143},[126,692,693,696,698,701,703,706,708,711,713,715,717,720,723,725,728],{"class":128,"line":266},[126,694,695],{"class":214},"@click.option",[126,697,218],{"class":143},[126,699,700],{"class":240},"\"--env\"",[126,702,17],{"class":143},[126,704,705],{"class":344},"type",[126,707,192],{"class":139},[126,709,710],{"class":143},"click.Choice(Env, ",[126,712,345],{"class":344},[126,714,192],{"class":139},[126,716,350],{"class":221},[126,718,719],{"class":143},"), ",[126,721,722],{"class":344},"default",[126,724,192],{"class":139},[126,726,727],{"class":240},"\"dev\"",[126,729,398],{"class":143},[126,731,732,735,737,740,742,744,746,748],{"class":128,"line":271},[126,733,734],{"class":344},"              show_default",[126,736,192],{"class":139},[126,738,739],{"class":221},"True",[126,741,17],{"class":143},[126,743,355],{"class":344},[126,745,192],{"class":139},[126,747,360],{"class":240},[126,749,515],{"class":143},[126,751,752,754,756,758,760,763,765,767,769,772,774,776,778,781,783,785,787],{"class":128,"line":276},[126,753,695],{"class":214},[126,755,218],{"class":143},[126,757,388],{"class":240},[126,759,17],{"class":143},[126,761,762],{"class":240},"\"fmt\"",[126,764,17],{"class":143},[126,766,705],{"class":344},[126,768,192],{"class":139},[126,770,771],{"class":143},"click.Choice([",[126,773,377],{"class":240},[126,775,17],{"class":143},[126,777,382],{"class":240},[126,779,780],{"class":143},"]), ",[126,782,722],{"class":344},[126,784,192],{"class":139},[126,786,377],{"class":240},[126,788,515],{"class":143},[126,790,791,793,795,798,800,802,804],{"class":128,"line":285},[126,792,288],{"class":139},[126,794,332],{"class":214},[126,796,797],{"class":143},"(env: Env, fmt: ",[126,799,222],{"class":221},[126,801,429],{"class":143},[126,803,297],{"class":221},[126,805,300],{"class":143},[126,807,808],{"class":128,"line":303},[126,809,439],{"class":240},[126,811,812,815,817,819,821,823,825,827,829,831,833,836],{"class":128,"line":309},[126,813,814],{"class":143},"    click.echo(",[126,816,448],{"class":139},[126,818,451],{"class":240},[126,820,454],{"class":221},[126,822,457],{"class":143},[126,824,460],{"class":221},[126,826,463],{"class":240},[126,828,454],{"class":221},[126,830,468],{"class":143},[126,832,460],{"class":221},[126,834,835],{"class":240},")\"",[126,837,515],{"class":143},[10,839,840,841,843,844,846,847,850,851,854,855,858,859,862,863,39,865,867],{},"With an ",[14,842,38],{}," class, ",[14,845,46],{}," lists and matches the member ",[49,848,849],{},"names"," — ",[14,852,853],{},"--env [dev|staging|prod]"," — and passes the member to the function. So in Click, ",[14,856,857],{},"--env prod"," works and ",[14,860,861],{},"--env production"," is rejected; in Typer it is the other way round. When names and values are identical, as for ",[14,864,16],{},[14,866,20],{}," here, the difference is invisible, which is exactly why it causes surprises: everything works until someone adds a member whose value differs from its name.",[60,869,871],{"id":870},"avoiding-the-mismatch","Avoiding the mismatch",[87,873],{"name":874},"choice-names-values",[10,876,877,878,881,882,885],{},"The simplest defence is a convention: ",[49,879,880],{},"make enum names and values identical"," for anything used as a CLI choice (",[14,883,884],{},"production = \"production\"","), or use lower-case strings that are valid identifiers. Then both frameworks accept the same input, migrations between them are safe, and help output matches what users type. If you need different spellings — a short CLI value mapping to a long API identifier — keep the CLI-facing enum simple and translate in code:",[117,887,889],{"className":119,"code":888,"language":121,"meta":122,"style":122},"API_NAMES = {Env.dev: \"development-eu1\", Env.staging: \"staging-eu1\", Env.prod: \"prod-eu1\"}\n",[14,890,891],{"__ignoreMap":122},[126,892,893,896,899,902,905,908,911,914,917],{"class":128,"line":129},[126,894,895],{"class":221},"API_NAMES",[126,897,898],{"class":139}," =",[126,900,901],{"class":143}," {Env.dev: ",[126,903,904],{"class":240},"\"development-eu1\"",[126,906,907],{"class":143},", Env.staging: ",[126,909,910],{"class":240},"\"staging-eu1\"",[126,912,913],{"class":143},", Env.prod: ",[126,915,916],{"class":240},"\"prod-eu1\"",[126,918,919],{"class":143},"}\n",[10,921,922,923,925,926,930,931,934],{},"That keeps the user-visible contract (the choices in ",[14,924,34],{},") independent from internal identifiers that may change. ",[54,927,929],{"href":928},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer\u002F","Converting a Click app to Typer"," is the moment this matters most: check every ",[14,932,933],{},"click.Choice(SomeEnum)"," before moving it to a Typer annotation.",[60,936,938],{"id":937},"the-same-choices-from-environment-variables-and-config-files","The same choices from environment variables and config files",[10,940,941,942,945,946,949,950,954,955,958,959,961],{},"An option's value does not always come from the command line. With ",[14,943,944],{},"typer.Option(envvar=\"MYTOOL_ENV\")"," (or ",[14,947,948],{},"envvar="," in Click), the framework reads the environment variable and runs it through the ",[951,952,953],"em",{},"same"," validation, so ",[14,956,957],{},"MYTOOL_ENV=prod"," fails exactly like ",[14,960,857],{}," would — good, because a typo in a CI variable should not silently select a default. Values from a configuration file are different: the framework never sees them, so validate them against the same enum when loading the file:",[117,963,965],{"className":119,"code":964,"language":121,"meta":122,"style":122},"def env_from_config(raw: str) -> Env:\n    try:\n        return Env(raw.lower())                       # match on value, like Typer does\n    except ValueError:\n        allowed = \", \".join(e.value for e in Env)\n        raise ValueError(f\"config: environment must be one of {allowed}, got {raw!r}\") from None\n",[14,966,967,982,989,1000,1010,1033],{"__ignoreMap":122},[126,968,969,971,974,977,979],{"class":128,"line":129},[126,970,288],{"class":139},[126,972,973],{"class":214}," env_from_config",[126,975,976],{"class":143},"(raw: ",[126,978,222],{"class":221},[126,980,981],{"class":143},") -> Env:\n",[126,983,984,987],{"class":128,"line":136},[126,985,986],{"class":139},"    try",[126,988,300],{"class":143},[126,990,991,994,997],{"class":128,"line":153},[126,992,993],{"class":139},"        return",[126,995,996],{"class":143}," Env(raw.lower())                       ",[126,998,999],{"class":132},"# match on value, like Typer does\n",[126,1001,1002,1005,1008],{"class":128,"line":166},[126,1003,1004],{"class":139},"    except",[126,1006,1007],{"class":221}," ValueError",[126,1009,300],{"class":143},[126,1011,1012,1015,1017,1020,1023,1025,1028,1030],{"class":128,"line":173},[126,1013,1014],{"class":143},"        allowed ",[126,1016,192],{"class":139},[126,1018,1019],{"class":240}," \", \"",[126,1021,1022],{"class":143},".join(e.value ",[126,1024,493],{"class":139},[126,1026,1027],{"class":143}," e ",[126,1029,499],{"class":139},[126,1031,1032],{"class":143}," Env)\n",[126,1034,1035,1038,1040,1042,1044,1047,1049,1052,1054,1057,1059,1062,1065,1067,1069,1072,1074],{"class":128,"line":181},[126,1036,1037],{"class":139},"        raise",[126,1039,1007],{"class":221},[126,1041,218],{"class":143},[126,1043,448],{"class":139},[126,1045,1046],{"class":240},"\"config: environment must be one of ",[126,1048,454],{"class":221},[126,1050,1051],{"class":143},"allowed",[126,1053,460],{"class":221},[126,1055,1056],{"class":240},", got ",[126,1058,454],{"class":221},[126,1060,1061],{"class":143},"raw",[126,1063,1064],{"class":139},"!r",[126,1066,460],{"class":221},[126,1068,482],{"class":240},[126,1070,1071],{"class":143},") ",[126,1073,140],{"class":139},[126,1075,1076],{"class":221}," None\n",[10,1078,1079,1080,58],{},"Using the enum as the single definition of valid choices — for flags, environment variables and config alike — keeps the three sources consistent and gives every error message the same list of allowed values. The precedence between them is covered in ",[54,1081,1083],{"href":1082},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence: flags, env, files, defaults",[60,1085,1087],{"id":1086},"ux-considerations","UX considerations",[65,1089,1090,1100,1106,1112,1122],{},[68,1091,1092,1095,1096,1099],{},[49,1093,1094],{},"Accept case-insensitively"," when the choices are words users type (",[14,1097,1098],{},"--env Production","); keep it sensitive when case is meaningful.",[68,1101,1102,1105],{},[49,1103,1104],{},"Put the most common choice first"," in the enum; it becomes the first item in help and completion.",[68,1107,1108,1111],{},[49,1109,1110],{},"Choose names users already know"," — the ones in your web UI, docs and API — rather than internal abbreviations.",[68,1113,1114,1117,1118,58],{},[49,1115,1116],{},"Keep the set stable."," Removing a choice breaks scripts like removing a flag; deprecate it first, as in ",[54,1119,1121],{"href":1120},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F","versioning and deprecating CLI flags",[68,1123,1124,1127,1128,58],{},[49,1125,1126],{},"Let completion do the work."," Both frameworks complete choices automatically once completion is installed; see ",[54,1129,1131],{"href":1130},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer\u002F","enabling tab completion in Click and Typer",[60,1133,1135],{"id":1134},"testing-the-behaviour","Testing the behaviour",[10,1137,1138],{},"Test the accepted spellings, the rejected ones and the typed value the command receives — in both frameworks if you use both:",[117,1140,1142],{"className":119,"code":1141,"language":121,"meta":122,"style":122},"# tests\u002Ftest_choices.py\nimport pytest\nfrom click.testing import CliRunner as ClickRunner\nfrom typer.testing import CliRunner\n\nfrom mytool import click_cli\nfrom mytool.cli import app\n\n\n@pytest.mark.parametrize(\"value\", [\"production\", \"PRODUCTION\"])\ndef test_typer_accepts_values_case_insensitively(value):\n    result = CliRunner().invoke(app, [\"deploy\", \"--env\", value])\n    assert result.exit_code == 0 and \"deploying to production\" in result.output\n\n\ndef test_typer_rejects_enum_names():\n    assert CliRunner().invoke(app, [\"deploy\", \"--env\", \"prod\"]).exit_code == 2\n\n\ndef test_click_accepts_names_not_values():\n    runner = ClickRunner()\n    assert runner.invoke(click_cli.deploy, [\"--env\", \"prod\"]).exit_code == 0\n    assert runner.invoke(click_cli.deploy, [\"--env\", \"production\"]).exit_code == 2\n\n\ndef test_repeated_choices_are_collected():\n    result = CliRunner().invoke(app, [\"deploy\", \"--region\", \"dev\", \"--region\", \"staging\"])\n    assert \"extra regions: dev, staging\" in result.output\n\n\ndef test_literal_rejects_unknown_format():\n    result = CliRunner().invoke(app, [\"deploy\", \"--format\", \"xml\"])\n    assert result.exit_code == 2 and \"'xml' is not one of\" in result.output\n",[14,1143,1144,1149,1156,1174,1186,1190,1202,1214,1218,1222,1246,1256,1276,1302,1306,1310,1320,1345,1349,1353,1362,1372,1392,1410,1414,1418,1427,1456,1467,1471,1476,1486,1508],{"__ignoreMap":122},[126,1145,1146],{"class":128,"line":129},[126,1147,1148],{"class":132},"# tests\u002Ftest_choices.py\n",[126,1150,1151,1153],{"class":128,"line":136},[126,1152,147],{"class":139},[126,1154,1155],{"class":143}," pytest\n",[126,1157,1158,1160,1163,1165,1168,1171],{"class":128,"line":153},[126,1159,140],{"class":139},[126,1161,1162],{"class":143}," click.testing ",[126,1164,147],{"class":139},[126,1166,1167],{"class":143}," CliRunner ",[126,1169,1170],{"class":139},"as",[126,1172,1173],{"class":143}," ClickRunner\n",[126,1175,1176,1178,1181,1183],{"class":128,"line":166},[126,1177,140],{"class":139},[126,1179,1180],{"class":143}," typer.testing ",[126,1182,147],{"class":139},[126,1184,1185],{"class":143}," CliRunner\n",[126,1187,1188],{"class":128,"line":173},[126,1189,170],{"emptyLinePlaceholder":169},[126,1191,1192,1194,1197,1199],{"class":128,"line":181},[126,1193,140],{"class":139},[126,1195,1196],{"class":143}," mytool ",[126,1198,147],{"class":139},[126,1200,1201],{"class":143}," click_cli\n",[126,1203,1204,1206,1209,1211],{"class":128,"line":186},[126,1205,140],{"class":139},[126,1207,1208],{"class":143}," mytool.cli ",[126,1210,147],{"class":139},[126,1212,1213],{"class":143}," app\n",[126,1215,1216],{"class":128,"line":198},[126,1217,170],{"emptyLinePlaceholder":169},[126,1219,1220],{"class":128,"line":203},[126,1221,170],{"emptyLinePlaceholder":169},[126,1223,1224,1227,1229,1232,1235,1238,1240,1243],{"class":128,"line":208},[126,1225,1226],{"class":214},"@pytest.mark.parametrize",[126,1228,218],{"class":143},[126,1230,1231],{"class":240},"\"value\"",[126,1233,1234],{"class":143},", [",[126,1236,1237],{"class":240},"\"production\"",[126,1239,17],{"class":143},[126,1241,1242],{"class":240},"\"PRODUCTION\"",[126,1244,1245],{"class":143},"])\n",[126,1247,1248,1250,1253],{"class":128,"line":232},[126,1249,288],{"class":139},[126,1251,1252],{"class":214}," test_typer_accepts_values_case_insensitively",[126,1254,1255],{"class":143},"(value):\n",[126,1257,1258,1261,1263,1266,1269,1271,1273],{"class":128,"line":244},[126,1259,1260],{"class":143},"    result ",[126,1262,192],{"class":139},[126,1264,1265],{"class":143}," CliRunner().invoke(app, [",[126,1267,1268],{"class":240},"\"deploy\"",[126,1270,17],{"class":143},[126,1272,700],{"class":240},[126,1274,1275],{"class":143},", value])\n",[126,1277,1278,1281,1284,1287,1290,1293,1296,1299],{"class":128,"line":255},[126,1279,1280],{"class":139},"    assert",[126,1282,1283],{"class":143}," result.exit_code ",[126,1285,1286],{"class":139},"==",[126,1288,1289],{"class":221}," 0",[126,1291,1292],{"class":139}," and",[126,1294,1295],{"class":240}," \"deploying to production\"",[126,1297,1298],{"class":139}," in",[126,1300,1301],{"class":143}," result.output\n",[126,1303,1304],{"class":128,"line":266},[126,1305,170],{"emptyLinePlaceholder":169},[126,1307,1308],{"class":128,"line":271},[126,1309,170],{"emptyLinePlaceholder":169},[126,1311,1312,1314,1317],{"class":128,"line":276},[126,1313,288],{"class":139},[126,1315,1316],{"class":214}," test_typer_rejects_enum_names",[126,1318,1319],{"class":143},"():\n",[126,1321,1322,1324,1326,1328,1330,1332,1334,1337,1340,1342],{"class":128,"line":285},[126,1323,1280],{"class":139},[126,1325,1265],{"class":143},[126,1327,1268],{"class":240},[126,1329,17],{"class":143},[126,1331,700],{"class":240},[126,1333,17],{"class":143},[126,1335,1336],{"class":240},"\"prod\"",[126,1338,1339],{"class":143},"]).exit_code ",[126,1341,1286],{"class":139},[126,1343,1344],{"class":221}," 2\n",[126,1346,1347],{"class":128,"line":303},[126,1348,170],{"emptyLinePlaceholder":169},[126,1350,1351],{"class":128,"line":309},[126,1352,170],{"emptyLinePlaceholder":169},[126,1354,1355,1357,1360],{"class":128,"line":314},[126,1356,288],{"class":139},[126,1358,1359],{"class":214}," test_click_accepts_names_not_values",[126,1361,1319],{"class":143},[126,1363,1364,1367,1369],{"class":128,"line":319},[126,1365,1366],{"class":143},"    runner ",[126,1368,192],{"class":139},[126,1370,1371],{"class":143}," ClickRunner()\n",[126,1373,1374,1376,1379,1381,1383,1385,1387,1389],{"class":128,"line":327},[126,1375,1280],{"class":139},[126,1377,1378],{"class":143}," runner.invoke(click_cli.deploy, [",[126,1380,700],{"class":240},[126,1382,17],{"class":143},[126,1384,1336],{"class":240},[126,1386,1339],{"class":143},[126,1388,1286],{"class":139},[126,1390,1391],{"class":221}," 0\n",[126,1393,1394,1396,1398,1400,1402,1404,1406,1408],{"class":128,"line":338},[126,1395,1280],{"class":139},[126,1397,1378],{"class":143},[126,1399,700],{"class":240},[126,1401,17],{"class":143},[126,1403,1237],{"class":240},[126,1405,1339],{"class":143},[126,1407,1286],{"class":139},[126,1409,1344],{"class":221},[126,1411,1412],{"class":128,"line":371},[126,1413,170],{"emptyLinePlaceholder":169},[126,1415,1416],{"class":128,"line":401},[126,1417,170],{"emptyLinePlaceholder":169},[126,1419,1420,1422,1425],{"class":128,"line":426},[126,1421,288],{"class":139},[126,1423,1424],{"class":214}," test_repeated_choices_are_collected",[126,1426,1319],{"class":143},[126,1428,1429,1431,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1454],{"class":128,"line":436},[126,1430,1260],{"class":143},[126,1432,192],{"class":139},[126,1434,1265],{"class":143},[126,1436,1268],{"class":240},[126,1438,17],{"class":143},[126,1440,407],{"class":240},[126,1442,17],{"class":143},[126,1444,727],{"class":240},[126,1446,17],{"class":143},[126,1448,407],{"class":240},[126,1450,17],{"class":143},[126,1452,1453],{"class":240},"\"staging\"",[126,1455,1245],{"class":143},[126,1457,1458,1460,1463,1465],{"class":128,"line":442},[126,1459,1280],{"class":139},[126,1461,1462],{"class":240}," \"extra regions: dev, staging\"",[126,1464,1298],{"class":139},[126,1466,1301],{"class":143},[126,1468,1469],{"class":128,"line":476},[126,1470,170],{"emptyLinePlaceholder":169},[126,1472,1474],{"class":128,"line":1473},30,[126,1475,170],{"emptyLinePlaceholder":169},[126,1477,1479,1481,1484],{"class":128,"line":1478},31,[126,1480,288],{"class":139},[126,1482,1483],{"class":214}," test_literal_rejects_unknown_format",[126,1485,1319],{"class":143},[126,1487,1489,1491,1493,1495,1497,1499,1501,1503,1506],{"class":128,"line":1488},32,[126,1490,1260],{"class":143},[126,1492,192],{"class":139},[126,1494,1265],{"class":143},[126,1496,1268],{"class":240},[126,1498,17],{"class":143},[126,1500,388],{"class":240},[126,1502,17],{"class":143},[126,1504,1505],{"class":240},"\"xml\"",[126,1507,1245],{"class":143},[126,1509,1511,1513,1515,1517,1520,1522,1525,1527],{"class":128,"line":1510},33,[126,1512,1280],{"class":139},[126,1514,1283],{"class":143},[126,1516,1286],{"class":139},[126,1518,1519],{"class":221}," 2",[126,1521,1292],{"class":139},[126,1523,1524],{"class":240}," \"'xml' is not one of\"",[126,1526,1298],{"class":139},[126,1528,1301],{"class":143},[10,1530,1531],{},"The pair of name-versus-value tests documents the framework difference in executable form, so nobody \"simplifies\" one side without noticing.",[60,1533,1535],{"id":1534},"conclusion","Conclusion",[10,1537,1538,1539,1541,1542,1544,1545,1547,1548,1551,1552,1554],{},"Declare fixed sets of values as choices rather than validating strings by hand: ",[14,1540,38],{}," or ",[14,1543,42],{}," annotations in Typer, ",[14,1546,46],{}," in Click. Remember that Typer matches enum values while ",[14,1549,1550],{},"click.Choice(Enum)"," matches names, keep names and values identical for CLI-facing enums to sidestep the difference, use ",[14,1553,548],{}," for words people type, and test both the accepted spellings and the rejected ones.",[60,1556,1558],{"id":1557},"frequently-asked-questions","Frequently asked questions",[569,1560,1562],{"id":1561},"how-do-i-show-a-description-for-each-choice-in-help","How do I show a description for each choice in help?",[10,1564,1565,1566,58],{},"Neither framework does this per value out of the box. Describe the choices in the option's help text (\"dev: shared sandbox; staging: pre-release; production: live\"), or, for longer explanations, in the command's epilog — see ",[54,1567,1569],{"href":1568},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output\u002F","adding examples and epilogs to help output",[569,1571,1573],{"id":1572},"can-the-set-of-choices-come-from-configuration-or-an-api","Can the set of choices come from configuration or an API?",[10,1575,1576,1577,1579,1580,58],{},"Not as a static ",[14,1578,38],{},". Accept a string, validate it against the dynamic set in a callback or at the start of the command, and offer dynamic shell completion — the techniques are in ",[54,1581,1583],{"href":1582},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files\u002F","dynamic completion values from APIs and files",[569,1585,1587,1588,1591],{"id":1586},"does-typer-support-intenum-or-numeric-choices","Does Typer support ",[14,1589,1590],{},"IntEnum"," or numeric choices?",[10,1593,1594,1595,1597,1598,1601,1602,1605,1606,1609,1610,1612],{},"Typer works with ",[14,1596,222],{},"-based enums most reliably. For numeric choices, use ",[14,1599,1600],{},"int"," with ",[14,1603,1604],{},"min","\u002F",[14,1607,1608],{},"max"," bounds when the values form a range, or a ",[14,1611,42],{}," of strings and convert inside the command.",[569,1614,1616],{"id":1615},"what-about-argparse","What about argparse?",[10,1618,1619,1622,1623,1625,1626,39,1629,1632,1633,1636],{},[14,1620,1621],{},"add_argument(\"--env\", choices=[\"dev\", \"staging\", \"production\"])"," gives the same validation and help; with an ",[14,1624,38],{},", pass ",[14,1627,1628],{},"type=Env",[14,1630,1631],{},"choices=list(Env)",", and give the enum a ",[14,1634,1635],{},"__str__"," that returns the value so help shows the right spellings.",[569,1638,1640],{"id":1639},"should-help-show-the-default-choice","Should help show the default choice?",[10,1642,1643,1644,1647,1648,1651,1652,1654],{},"Yes — Typer does by default, and in Click pass ",[14,1645,1646],{},"show_default=True",". Knowing that ",[14,1649,1650],{},"--env"," defaults to ",[14,1653,16],{}," is often the single most useful fact in the option's help, and it prevents the nasty surprise of a deployment that went somewhere unexpected because nobody passed the flag.",[60,1656,1658],{"id":1657},"related","Related",[65,1660,1661,1667,1672,1676,1682],{},[68,1662,1663,1664],{},"Up: ",[54,1665,1666],{"href":56},"Typer vs Click: when to use each",[68,1668,1669],{},[54,1670,1671],{"href":80},"Using Annotated options in Typer",[68,1673,1674],{},[54,1675,929],{"href":928},[68,1677,1678],{},[54,1679,1681],{"href":1680},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options\u002F","Click option callbacks and eager options",[68,1683,1684],{},[54,1685,1687],{"href":1686},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types\u002F","Writing custom Click parameter types",[1689,1690,1691],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":122,"searchDepth":136,"depth":136,"links":1693},[1694,1695,1696,1700,1701,1702,1703,1704,1705,1706,1714],{"id":62,"depth":136,"text":63},{"id":84,"depth":136,"text":85},{"id":114,"depth":136,"text":115,"children":1697},[1698],{"id":571,"depth":153,"text":1699},"When to choose Enum vs Literal",{"id":599,"depth":136,"text":600},{"id":870,"depth":136,"text":871},{"id":937,"depth":136,"text":938},{"id":1086,"depth":136,"text":1087},{"id":1134,"depth":136,"text":1135},{"id":1534,"depth":136,"text":1535},{"id":1557,"depth":136,"text":1558,"children":1707},[1708,1709,1710,1712,1713],{"id":1561,"depth":153,"text":1562},{"id":1572,"depth":153,"text":1573},{"id":1586,"depth":153,"text":1711},"Does Typer support IntEnum or numeric choices?",{"id":1615,"depth":153,"text":1616},{"id":1639,"depth":153,"text":1640},{"id":1657,"depth":136,"text":1658},"2026-10-02","Restrict option values with Enum, Literal and click.Choice: how Typer matches enum values while Click matches names, case-insensitivity, repeated choices, completion and tests.","beginner",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click",{"title":5,"description":1716},"modern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click\u002Findex",[1725,1726,1727,1728,1729],"typer","click","enums","validation","options","lB7rZ-Fx7H28sLBT8DJlEr64QzJqWzY1IuXlscrnp-E",[1732,1735,1738,1741,1744,1747,1750,1753,1756,1759,1762,1765,1768,1771,1774,1777,1780,1783,1786,1789,1792,1795,1798,1801,1804,1807,1810,1813,1816,1819,1822,1825,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1864,1867,1870,1873,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1918,1921,1924,1927,1930,1933,1936,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978,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,2128,2131,2134,2137,2140,2143,2146,2149,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2461,2464,2467,2470,2473,2476,2479,2482,2485,2488,2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572],{"path":1733,"title":1734},"\u002Fabout","About Python CLI Toolcraft",{"path":1736,"title":1737},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1739,"title":1740},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1742,"title":1743},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":1745,"title":1746},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1748,"title":1749},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1751,"title":1752},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1754,"title":1755},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1757,"title":1758},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1760,"title":1761},"\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":1763,"title":1764},"\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":1766,"title":1767},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1769,"title":1770},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":1772,"title":1773},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":1775,"title":1776},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1778,"title":1779},"\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":1781,"title":1782},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":1784,"title":1785},"\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":1787,"title":1788},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1790,"title":1791},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":1793,"title":1794},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1796,"title":1797},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1799,"title":1800},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1802,"title":1803},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1805,"title":1806},"\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":1808,"title":1809},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":1811,"title":1812},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1814,"title":1815},"\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":1817,"title":1818},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":1820,"title":1821},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1823,"title":1824},"\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":1826,"title":1827},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":1829,"title":1830},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1832,"title":1833},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1835,"title":1836},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1838,"title":1839},"\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":1841,"title":1842},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":1844,"title":1845},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1847,"title":1848},"\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":1850,"title":1851},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1853,"title":1854},"\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":1856,"title":1857},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1859,"title":1860},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1862,"title":1863},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":1865,"title":1866},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":1868,"title":1869},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1871,"title":1872},"\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":1874,"title":1875},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1877,"title":1878},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":1880,"title":1881},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1883,"title":1884},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1886,"title":1887},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":1889,"title":1890},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1892,"title":1893},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1895,"title":1896},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":1898,"title":1899},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":1901,"title":1902},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":1904,"title":1905},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":1907,"title":1908},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":1910,"title":1911},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":1913,"title":1914},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":1916,"title":1917},"\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":1919,"title":1920},"\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":1922,"title":1923},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1925,"title":1926},"\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":1928,"title":1929},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":1931,"title":1932},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1934,"title":1935},"\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":1937,"title":1938},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1943,"title":1944},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":1946,"title":1947},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":1949,"title":1950},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1952,"title":1953},"\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":1955,"title":1956},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1958,"title":1959},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1961,"title":1962},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1964,"title":1965},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1967,"title":1968},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":1970,"title":1971},"\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":1973,"title":1974},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1976,"title":1977},"\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":1979,"title":1980},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1982,"title":1983},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1985,"title":1986},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":1988,"title":1989},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1991,"title":1992},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1994,"title":1995},"\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":1997,"title":1998},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2000,"title":2001},"\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":2003,"title":2004},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2006,"title":2007},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2009,"title":2010},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2012,"title":2013},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2015,"title":2016},"\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":2018,"title":2019},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2021,"title":2022},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2024,"title":2025},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2027,"title":2028},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2030,"title":2031},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2033,"title":2034},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2036,"title":2037},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2039,"title":2040},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2042,"title":2043},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2045,"title":2046},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2048,"title":2049},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2051,"title":2052},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2054,"title":2055},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2057,"title":2058},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2060,"title":2061},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2063,"title":2064},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2066,"title":2067},"\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":2069,"title":2070},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2072,"title":2073},"\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":2075,"title":2076},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2078,"title":2079},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2081,"title":2082},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2084,"title":2085},"\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":2087,"title":2088},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2090,"title":2091},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2093,"title":2094},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2096,"title":2097},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2099,"title":2100},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2102,"title":2103},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2105,"title":2106},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2108,"title":2109},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2111,"title":2112},"\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":2114,"title":2115},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2117,"title":2118},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2120,"title":2121},"\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":2123,"title":2124},"\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":2126,"title":2127},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2129,"title":2130},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2132,"title":2133},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2135,"title":2136},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2138,"title":2139},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2141,"title":2142},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2144,"title":2145},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2147,"title":2148},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":1605,"title":2150},"Python CLI Toolcraft",{"path":2152,"title":2153},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2155,"title":2156},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2158,"title":2159},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2161,"title":2162},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2164,"title":2165},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2167,"title":2168},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2170,"title":2171},"\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":2173,"title":2174},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2176,"title":2177},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2179,"title":2180},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2182,"title":2183},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2185,"title":2186},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2188,"title":2189},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2191,"title":2192},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2194,"title":2195},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2197,"title":2198},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2200,"title":2201},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2203,"title":2204},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2206,"title":2207},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2209,"title":2210},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2212,"title":2213},"\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":2215,"title":2216},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2218,"title":2219},"\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":2221,"title":2222},"\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":2224,"title":2225},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2227,"title":2228},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2230,"title":2231},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2233,"title":2234},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2236,"title":2237},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2239,"title":2240},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2242,"title":2243},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2245,"title":2246},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2248,"title":2249},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2251,"title":2252},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2254,"title":2255},"\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":2257,"title":2258},"\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":2260,"title":2261},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2263,"title":2264},"\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":2266,"title":2267},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2269,"title":2270},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2272,"title":2273},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2275,"title":2276},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2278,"title":2279},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2281,"title":2282},"\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":2284,"title":2285},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2287,"title":2288},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2290,"title":2291},"\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":2293,"title":2294},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2296,"title":2297},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2299,"title":2300},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2302,"title":2303},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2305,"title":2306},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2308,"title":2309},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2311,"title":2312},"\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":2314,"title":2315},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1721,"title":5},{"path":2318,"title":2319},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2321,"title":2322},"\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":2324,"title":2325},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2327,"title":2328},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2330,"title":2331},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2333,"title":2334},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2345,"title":2346},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2348,"title":2349},"\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":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2354,"title":2355},"\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":2357,"title":2358},"\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":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2363,"title":2364},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2366,"title":2367},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2369,"title":2370},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2375,"title":2376},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2378,"title":2379},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2381,"title":2382},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2384,"title":2385},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2387,"title":2388},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2390,"title":2391},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2393,"title":2394},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2396,"title":2397},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2399,"title":2400},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2402,"title":2403},"\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":2405,"title":2406},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2408,"title":2409},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2411,"title":2412},"\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":2414,"title":2415},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":2417,"title":2418},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2420,"title":2421},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":2423,"title":2424},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":2426,"title":2427},"\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":2429,"title":2430},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2432,"title":2433},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":2435,"title":2436},"\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":2438,"title":2439},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2441,"title":2442},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2444,"title":2445},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2447,"title":2448},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":2450,"title":2451},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2453,"title":2454},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2456,"title":2457},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":2459,"title":2460},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2462,"title":2463},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2465,"title":2466},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2468,"title":2469},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2471,"title":2472},"\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":2474,"title":2475},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2477,"title":2478},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2480,"title":2481},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2483,"title":2484},"\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":2486,"title":2487},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2489,"title":2490},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2492,"title":2493},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2495,"title":2496},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2498,"title":2499},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":2501,"title":2502},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":2504,"title":2505},"\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":2507,"title":2508},"\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":2510,"title":2511},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":2513,"title":2514},"\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":2516,"title":2517},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":2519,"title":2520},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":2522,"title":2523},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2525,"title":2526},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2528,"title":2529},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2531,"title":2532},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":2534,"title":2535},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2537,"title":2538},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":2540,"title":2541},"\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":2543,"title":2544},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":2546,"title":2547},"\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":2549,"title":2550},"\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":2552,"title":2553},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2555,"title":2556},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":2558,"title":2559},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":2561,"title":2562},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2564,"title":2565},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2567,"title":2568},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2570,"title":2571},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2573,"title":2574},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967541910]