[{"data":1,"prerenderedAt":2856},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002F":3,"content-directory":2310},{"id":4,"title":5,"body":6,"date":2295,"description":2296,"difficulty":2297,"draft":2298,"extension":2299,"meta":2300,"navigation":117,"path":2301,"seo":2302,"stem":2303,"tags":2304,"updated":2295,"__hash__":2309},"content\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002Findex.md","Using Annotated Options in Typer",{"type":7,"value":8,"toc":2273},"minimark",[9,40,45,63,67,71,440,443,501,505,1004,1007,1084,1088,1094,1270,1389,1392,1408,1413,1419,1683,1701,1705,1708,1761,1767,1771,1776,1779,1806,1810,1817,2131,2142,2146,2152,2156,2160,2166,2177,2183,2187,2194,2198,2212,2219,2234,2238,2269],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"Early Typer code declared options by putting ",[14,15,16],"code",{},"typer.Option(...)"," in the default value: ",[14,19,20],{},"retries: int = typer.Option(3, \"--retries\", min=0)",". It works, but the real default is hidden inside a function call, type checkers see the parameter's default as a Typer object rather than an ",[14,23,24],{},"int",", and the function cannot be called directly from Python without Typer supplying the values. Since version 0.9, Typer supports — and its documentation recommends — the ",[14,27,28],{},"Annotated"," style: ",[14,31,32],{},"retries: Annotated[int, typer.Option(min=0)] = 3",". The type and the CLI metadata travel together in the annotation, and the default is an ordinary Python default. This guide shows the style for arguments and options, the reusable type aliases it makes possible, the handful of gotchas, and how to migrate an existing Typer app. It belongs to the ",[35,36,38],"a",{"href":37},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click topic",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,48,60],"ul",{},[49,50,51,52,55,56,59],"li",{},"Python 3.10+ (for ",[14,53,54],{},"X | None","; ",[14,57,58],{},"typing.Annotated"," itself exists from 3.9) and Typer 0.12 or newer.",[49,61,62],{},"A Typer app, new or existing.",[41,64,66],{"id":65},"the-two-styles-side-by-side","The two styles side by side",[68,69],"inline-diagram",{"name":70},"an-compare",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"language-python shiki shiki-themes github-light github-dark","from pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer()\n\n\n# Older style: the default is inside typer.Option(...)\n@app.command()\ndef old(\n    site: Path = typer.Argument(..., exists=True, file_okay=False, help=\"Directory to deploy.\"),\n    retries: int = typer.Option(3, \"--retries\", \"-r\", min=0, max=10, help=\"Retry attempts.\"),\n) -> None:\n    ...\n\n\n# Annotated style: type + metadata in the annotation, default after =\n@app.command()\ndef new(\n    site: Annotated[Path, typer.Argument(exists=True, file_okay=False, help=\"Directory to deploy.\")],\n    retries: Annotated[int, typer.Option(\"--retries\", \"-r\", min=0, max=10, help=\"Retry attempts.\")] = 3,\n) -> None:\n    ...\n","python","",[14,79,80,99,112,119,127,132,144,149,154,161,171,183,234,292,304,310,315,320,326,333,343,374,426,435],{"__ignoreMap":77},[81,82,85,89,93,96],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"szBVR","from",[81,90,92],{"class":91},"sVt8B"," pathlib ",[81,94,95],{"class":87},"import",[81,97,98],{"class":91}," Path\n",[81,100,102,104,107,109],{"class":83,"line":101},2,[81,103,88],{"class":87},[81,105,106],{"class":91}," typing ",[81,108,95],{"class":87},[81,110,111],{"class":91}," Annotated\n",[81,113,115],{"class":83,"line":114},3,[81,116,118],{"emptyLinePlaceholder":117},true,"\n",[81,120,122,124],{"class":83,"line":121},4,[81,123,95],{"class":87},[81,125,126],{"class":91}," typer\n",[81,128,130],{"class":83,"line":129},5,[81,131,118],{"emptyLinePlaceholder":117},[81,133,135,138,141],{"class":83,"line":134},6,[81,136,137],{"class":91},"app ",[81,139,140],{"class":87},"=",[81,142,143],{"class":91}," typer.Typer()\n",[81,145,147],{"class":83,"line":146},7,[81,148,118],{"emptyLinePlaceholder":117},[81,150,152],{"class":83,"line":151},8,[81,153,118],{"emptyLinePlaceholder":117},[81,155,157],{"class":83,"line":156},9,[81,158,160],{"class":159},"sJ8bj","# Older style: the default is inside typer.Option(...)\n",[81,162,164,168],{"class":83,"line":163},10,[81,165,167],{"class":166},"sScJk","@app.command",[81,169,170],{"class":91},"()\n",[81,172,174,177,180],{"class":83,"line":173},11,[81,175,176],{"class":87},"def",[81,178,179],{"class":166}," old",[81,181,182],{"class":91},"(\n",[81,184,186,189,191,194,198,201,205,207,210,212,215,217,220,222,225,227,231],{"class":83,"line":185},12,[81,187,188],{"class":91},"    site: Path ",[81,190,140],{"class":87},[81,192,193],{"class":91}," typer.Argument(",[81,195,197],{"class":196},"sj4cs","...",[81,199,200],{"class":91},", ",[81,202,204],{"class":203},"s4XuR","exists",[81,206,140],{"class":87},[81,208,209],{"class":196},"True",[81,211,200],{"class":91},[81,213,214],{"class":203},"file_okay",[81,216,140],{"class":87},[81,218,219],{"class":196},"False",[81,221,200],{"class":91},[81,223,224],{"class":203},"help",[81,226,140],{"class":87},[81,228,230],{"class":229},"sZZnC","\"Directory to deploy.\"",[81,232,233],{"class":91},"),\n",[81,235,237,240,242,245,248,251,253,256,258,261,263,266,268,271,273,276,278,281,283,285,287,290],{"class":83,"line":236},13,[81,238,239],{"class":91},"    retries: ",[81,241,24],{"class":196},[81,243,244],{"class":87}," =",[81,246,247],{"class":91}," typer.Option(",[81,249,250],{"class":196},"3",[81,252,200],{"class":91},[81,254,255],{"class":229},"\"--retries\"",[81,257,200],{"class":91},[81,259,260],{"class":229},"\"-r\"",[81,262,200],{"class":91},[81,264,265],{"class":203},"min",[81,267,140],{"class":87},[81,269,270],{"class":196},"0",[81,272,200],{"class":91},[81,274,275],{"class":203},"max",[81,277,140],{"class":87},[81,279,280],{"class":196},"10",[81,282,200],{"class":91},[81,284,224],{"class":203},[81,286,140],{"class":87},[81,288,289],{"class":229},"\"Retry attempts.\"",[81,291,233],{"class":91},[81,293,295,298,301],{"class":83,"line":294},14,[81,296,297],{"class":91},") -> ",[81,299,300],{"class":196},"None",[81,302,303],{"class":91},":\n",[81,305,307],{"class":83,"line":306},15,[81,308,309],{"class":196},"    ...\n",[81,311,313],{"class":83,"line":312},16,[81,314,118],{"emptyLinePlaceholder":117},[81,316,318],{"class":83,"line":317},17,[81,319,118],{"emptyLinePlaceholder":117},[81,321,323],{"class":83,"line":322},18,[81,324,325],{"class":159},"# Annotated style: type + metadata in the annotation, default after =\n",[81,327,329,331],{"class":83,"line":328},19,[81,330,167],{"class":166},[81,332,170],{"class":91},[81,334,336,338,341],{"class":83,"line":335},20,[81,337,176],{"class":87},[81,339,340],{"class":166}," new",[81,342,182],{"class":91},[81,344,346,349,351,353,355,357,359,361,363,365,367,369,371],{"class":83,"line":345},21,[81,347,348],{"class":91},"    site: Annotated[Path, typer.Argument(",[81,350,204],{"class":203},[81,352,140],{"class":87},[81,354,209],{"class":196},[81,356,200],{"class":91},[81,358,214],{"class":203},[81,360,140],{"class":87},[81,362,219],{"class":196},[81,364,200],{"class":91},[81,366,224],{"class":203},[81,368,140],{"class":87},[81,370,230],{"class":229},[81,372,373],{"class":91},")],\n",[81,375,377,380,382,385,387,389,391,393,395,397,399,401,403,405,407,409,411,413,415,418,420,423],{"class":83,"line":376},22,[81,378,379],{"class":91},"    retries: Annotated[",[81,381,24],{"class":196},[81,383,384],{"class":91},", typer.Option(",[81,386,255],{"class":229},[81,388,200],{"class":91},[81,390,260],{"class":229},[81,392,200],{"class":91},[81,394,265],{"class":203},[81,396,140],{"class":87},[81,398,270],{"class":196},[81,400,200],{"class":91},[81,402,275],{"class":203},[81,404,140],{"class":87},[81,406,280],{"class":196},[81,408,200],{"class":91},[81,410,224],{"class":203},[81,412,140],{"class":87},[81,414,289],{"class":229},[81,416,417],{"class":91},")] ",[81,419,140],{"class":87},[81,421,422],{"class":196}," 3",[81,424,425],{"class":91},",\n",[81,427,429,431,433],{"class":83,"line":428},23,[81,430,297],{"class":91},[81,432,300],{"class":196},[81,434,303],{"class":91},[81,436,438],{"class":83,"line":437},24,[81,439,309],{"class":196},[10,441,442],{},"Both produce identical command-line behaviour and help. The differences are in the code:",[46,444,445,458,482],{},[49,446,447,451,452,454,455,457],{},[448,449,450],"strong",{},"The default is visible where Python programmers look for it",", after ",[14,453,140],{},". A required argument simply has no default, instead of the ",[14,456,197],{}," sentinel.",[49,459,460,463,464,467,468,470,471,473,474,477,478,39],{},[448,461,462],{},"Type checkers see real types."," mypy and pyright understand that ",[14,465,466],{},"retries"," is an ",[14,469,24],{}," defaulting to ",[14,472,250],{},". In the old style they see a default of type ",[14,475,476],{},"OptionInfo",", so type errors around defaults go unnoticed — covered further in ",[35,479,481],{"href":480},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy\u002F","type-checking Click and Typer code with mypy",[49,483,484,487,488,491,492,495,496,498,499,39],{},[448,485,486],{},"The function is callable as plain Python."," ",[14,489,490],{},"new(Path(\"site\"))"," works in a test or from another module, using real defaults. Calling ",[14,493,494],{},"old(Path(\"site\"))"," would pass an ",[14,497,476],{}," object as ",[14,500,466],{},[41,502,504],{"id":503},"the-recipe-a-command-in-annotated-style","The recipe: a command in Annotated style",[72,506,508],{"className":74,"code":507,"language":76,"meta":77,"style":77},"# src\u002Fmytool\u002Fcli.py\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer(no_args_is_help=True)\n\n\nclass Env(str, Enum):\n    dev = \"dev\"\n    staging = \"staging\"\n    prod = \"prod\"\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment tool.\"\"\"\n\n\n@app.command()\ndef deploy(\n    site: Annotated[Path, typer.Argument(exists=True, file_okay=False, help=\"Directory to deploy.\")],\n    env: Annotated[Env, typer.Option(help=\"Target environment.\")] = Env.dev,\n    retries: Annotated[int, typer.Option(min=0, max=10, envvar=\"MYTOOL_RETRIES\",\n                                         help=\"Retry attempts.\")] = 3,\n    tags: Annotated[list[str] | None, typer.Option(\"--tag\", help=\"Tag the release (repeatable).\")] = None,\n    dry_run: Annotated[bool, typer.Option(\"--dry-run\", \"-n\", help=\"Show what would happen.\")] = False,\n) -> None:\n    \"\"\"Deploy SITE to an environment.\"\"\"\n    tags = tags or []\n    typer.echo(f\"deploy {site.name} to {env.value} retries={retries} tags={tags} dry_run={dry_run}\")\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,509,510,515,527,537,547,551,557,561,580,584,588,610,620,630,640,644,648,655,669,674,678,682,688,697,725,745,780,798,837,874,883,889,906,971,976,981,998],{"__ignoreMap":77},[81,511,512],{"class":83,"line":84},[81,513,514],{"class":159},"# src\u002Fmytool\u002Fcli.py\n",[81,516,517,519,522,524],{"class":83,"line":101},[81,518,88],{"class":87},[81,520,521],{"class":91}," enum ",[81,523,95],{"class":87},[81,525,526],{"class":91}," Enum\n",[81,528,529,531,533,535],{"class":83,"line":114},[81,530,88],{"class":87},[81,532,92],{"class":91},[81,534,95],{"class":87},[81,536,98],{"class":91},[81,538,539,541,543,545],{"class":83,"line":121},[81,540,88],{"class":87},[81,542,106],{"class":91},[81,544,95],{"class":87},[81,546,111],{"class":91},[81,548,549],{"class":83,"line":129},[81,550,118],{"emptyLinePlaceholder":117},[81,552,553,555],{"class":83,"line":134},[81,554,95],{"class":87},[81,556,126],{"class":91},[81,558,559],{"class":83,"line":146},[81,560,118],{"emptyLinePlaceholder":117},[81,562,563,565,567,570,573,575,577],{"class":83,"line":151},[81,564,137],{"class":91},[81,566,140],{"class":87},[81,568,569],{"class":91}," typer.Typer(",[81,571,572],{"class":203},"no_args_is_help",[81,574,140],{"class":87},[81,576,209],{"class":196},[81,578,579],{"class":91},")\n",[81,581,582],{"class":83,"line":156},[81,583,118],{"emptyLinePlaceholder":117},[81,585,586],{"class":83,"line":163},[81,587,118],{"emptyLinePlaceholder":117},[81,589,590,593,596,599,602,604,607],{"class":83,"line":173},[81,591,592],{"class":87},"class",[81,594,595],{"class":166}," Env",[81,597,598],{"class":91},"(",[81,600,601],{"class":196},"str",[81,603,200],{"class":91},[81,605,606],{"class":166},"Enum",[81,608,609],{"class":91},"):\n",[81,611,612,615,617],{"class":83,"line":185},[81,613,614],{"class":91},"    dev ",[81,616,140],{"class":87},[81,618,619],{"class":229}," \"dev\"\n",[81,621,622,625,627],{"class":83,"line":236},[81,623,624],{"class":91},"    staging ",[81,626,140],{"class":87},[81,628,629],{"class":229}," \"staging\"\n",[81,631,632,635,637],{"class":83,"line":294},[81,633,634],{"class":91},"    prod ",[81,636,140],{"class":87},[81,638,639],{"class":229}," \"prod\"\n",[81,641,642],{"class":83,"line":306},[81,643,118],{"emptyLinePlaceholder":117},[81,645,646],{"class":83,"line":312},[81,647,118],{"emptyLinePlaceholder":117},[81,649,650,653],{"class":83,"line":317},[81,651,652],{"class":166},"@app.callback",[81,654,170],{"class":91},[81,656,657,659,662,665,667],{"class":83,"line":322},[81,658,176],{"class":87},[81,660,661],{"class":166}," main",[81,663,664],{"class":91},"() -> ",[81,666,300],{"class":196},[81,668,303],{"class":91},[81,670,671],{"class":83,"line":328},[81,672,673],{"class":229},"    \"\"\"Deployment tool.\"\"\"\n",[81,675,676],{"class":83,"line":335},[81,677,118],{"emptyLinePlaceholder":117},[81,679,680],{"class":83,"line":345},[81,681,118],{"emptyLinePlaceholder":117},[81,683,684,686],{"class":83,"line":376},[81,685,167],{"class":166},[81,687,170],{"class":91},[81,689,690,692,695],{"class":83,"line":428},[81,691,176],{"class":87},[81,693,694],{"class":166}," deploy",[81,696,182],{"class":91},[81,698,699,701,703,705,707,709,711,713,715,717,719,721,723],{"class":83,"line":437},[81,700,348],{"class":91},[81,702,204],{"class":203},[81,704,140],{"class":87},[81,706,209],{"class":196},[81,708,200],{"class":91},[81,710,214],{"class":203},[81,712,140],{"class":87},[81,714,219],{"class":196},[81,716,200],{"class":91},[81,718,224],{"class":203},[81,720,140],{"class":87},[81,722,230],{"class":229},[81,724,373],{"class":91},[81,726,728,731,733,735,738,740,742],{"class":83,"line":727},25,[81,729,730],{"class":91},"    env: Annotated[Env, typer.Option(",[81,732,224],{"class":203},[81,734,140],{"class":87},[81,736,737],{"class":229},"\"Target environment.\"",[81,739,417],{"class":91},[81,741,140],{"class":87},[81,743,744],{"class":91}," Env.dev,\n",[81,746,748,750,752,754,756,758,760,762,764,766,768,770,773,775,778],{"class":83,"line":747},26,[81,749,379],{"class":91},[81,751,24],{"class":196},[81,753,384],{"class":91},[81,755,265],{"class":203},[81,757,140],{"class":87},[81,759,270],{"class":196},[81,761,200],{"class":91},[81,763,275],{"class":203},[81,765,140],{"class":87},[81,767,280],{"class":196},[81,769,200],{"class":91},[81,771,772],{"class":203},"envvar",[81,774,140],{"class":87},[81,776,777],{"class":229},"\"MYTOOL_RETRIES\"",[81,779,425],{"class":91},[81,781,783,786,788,790,792,794,796],{"class":83,"line":782},27,[81,784,785],{"class":203},"                                         help",[81,787,140],{"class":87},[81,789,289],{"class":229},[81,791,417],{"class":91},[81,793,140],{"class":87},[81,795,422],{"class":196},[81,797,425],{"class":91},[81,799,801,804,806,809,812,815,817,820,822,824,826,829,831,833,835],{"class":83,"line":800},28,[81,802,803],{"class":91},"    tags: Annotated[list[",[81,805,601],{"class":196},[81,807,808],{"class":91},"] ",[81,810,811],{"class":87},"|",[81,813,814],{"class":196}," None",[81,816,384],{"class":91},[81,818,819],{"class":229},"\"--tag\"",[81,821,200],{"class":91},[81,823,224],{"class":203},[81,825,140],{"class":87},[81,827,828],{"class":229},"\"Tag the release (repeatable).\"",[81,830,417],{"class":91},[81,832,140],{"class":87},[81,834,814],{"class":196},[81,836,425],{"class":91},[81,838,840,843,846,848,851,853,856,858,860,862,865,867,869,872],{"class":83,"line":839},29,[81,841,842],{"class":91},"    dry_run: Annotated[",[81,844,845],{"class":196},"bool",[81,847,384],{"class":91},[81,849,850],{"class":229},"\"--dry-run\"",[81,852,200],{"class":91},[81,854,855],{"class":229},"\"-n\"",[81,857,200],{"class":91},[81,859,224],{"class":203},[81,861,140],{"class":87},[81,863,864],{"class":229},"\"Show what would happen.\"",[81,866,417],{"class":91},[81,868,140],{"class":87},[81,870,871],{"class":196}," False",[81,873,425],{"class":91},[81,875,877,879,881],{"class":83,"line":876},30,[81,878,297],{"class":91},[81,880,300],{"class":196},[81,882,303],{"class":91},[81,884,886],{"class":83,"line":885},31,[81,887,888],{"class":229},"    \"\"\"Deploy SITE to an environment.\"\"\"\n",[81,890,892,895,897,900,903],{"class":83,"line":891},32,[81,893,894],{"class":91},"    tags ",[81,896,140],{"class":87},[81,898,899],{"class":91}," tags ",[81,901,902],{"class":87},"or",[81,904,905],{"class":91}," []\n",[81,907,909,912,915,918,921,924,927,930,932,935,937,940,942,944,946,949,951,954,956,959,961,964,966,969],{"class":83,"line":908},33,[81,910,911],{"class":91},"    typer.echo(",[81,913,914],{"class":87},"f",[81,916,917],{"class":229},"\"deploy ",[81,919,920],{"class":196},"{",[81,922,923],{"class":91},"site.name",[81,925,926],{"class":196},"}",[81,928,929],{"class":229}," to ",[81,931,920],{"class":196},[81,933,934],{"class":91},"env.value",[81,936,926],{"class":196},[81,938,939],{"class":229}," retries=",[81,941,920],{"class":196},[81,943,466],{"class":91},[81,945,926],{"class":196},[81,947,948],{"class":229}," tags=",[81,950,920],{"class":196},[81,952,953],{"class":91},"tags",[81,955,926],{"class":196},[81,957,958],{"class":229}," dry_run=",[81,960,920],{"class":196},[81,962,963],{"class":91},"dry_run",[81,965,926],{"class":196},[81,967,968],{"class":229},"\"",[81,970,579],{"class":91},[81,972,974],{"class":83,"line":973},34,[81,975,118],{"emptyLinePlaceholder":117},[81,977,979],{"class":83,"line":978},35,[81,980,118],{"emptyLinePlaceholder":117},[81,982,984,987,990,993,996],{"class":83,"line":983},36,[81,985,986],{"class":87},"if",[81,988,989],{"class":196}," __name__",[81,991,992],{"class":87}," ==",[81,994,995],{"class":229}," \"__main__\"",[81,997,303],{"class":91},[81,999,1001],{"class":83,"line":1000},37,[81,1002,1003],{"class":91},"    app()\n",[10,1005,1006],{},"A few details worth knowing:",[46,1008,1009,1025,1035,1055,1065],{},[49,1010,1011,487,1014,1016,1017,1020,1021,1024],{},[448,1012,1013],{},"Parameter names become option names.",[14,1015,963],{}," becomes ",[14,1018,1019],{},"--dry-run"," automatically; pass explicit names (",[14,1022,1023],{},"\"--dry-run\", \"-n\"",") when you want a short flag or a different spelling.",[49,1026,1027,1030,1031,1034],{},[448,1028,1029],{},"Booleans become flag pairs"," (",[14,1032,1033],{},"--dry-run\u002F--no-dry-run",") unless you give explicit names, as above, which produce a single flag.",[49,1036,1037,487,1040,1043,1044,1047,1048,1050,1051,1054],{},[448,1038,1039],{},"Lists are repeatable options.",[14,1041,1042],{},"list[str] | None = None"," accepts ",[14,1045,1046],{},"--tag a --tag b",". Use ",[14,1049,300],{}," rather than ",[14,1052,1053],{},"[]"," as the default, to avoid a mutable default argument.",[49,1056,1057,1060,1061,1064],{},[448,1058,1059],{},"Enums become choices",", listed in ",[14,1062,1063],{},"--help"," and validated automatically.",[49,1066,1067,1083],{},[448,1068,1069,200,1072,1075,1076,200,1079,1082],{},[14,1070,1071],{},"envvar=",[14,1073,1074],{},"min=","\u002F",[14,1077,1078],{},"max=",[14,1080,1081],{},"exists="," and every other option setting"," work exactly as before; they just live inside the annotation.",[41,1085,1087],{"id":1086},"the-recipe-reusable-option-aliases","The recipe: reusable option aliases",[10,1089,1090,1091,1093],{},"The biggest practical win of ",[14,1092,28],{}," is that an annotated type is a value you can name and reuse. Define common options once:",[72,1095,1097],{"className":74,"code":1096,"language":76,"meta":77,"style":77},"# src\u002Fmytool\u002Foptions.py\nfrom typing import Annotated\n\nimport typer\n\nVerbose = Annotated[int, typer.Option(\"--verbose\", \"-v\", count=True, help=\"More output (repeatable).\")]\nLimit = Annotated[int, typer.Option(\"--limit\", \"-n\", min=1, help=\"Show at most N items.\")]\nJson = Annotated[bool, typer.Option(\"--json\", help=\"Machine-readable output.\")]\nYes = Annotated[bool, typer.Option(\"--yes\", \"-y\", help=\"Do not ask for confirmation.\")]\n",[14,1098,1099,1104,1114,1118,1124,1128,1171,1211,1238],{"__ignoreMap":77},[81,1100,1101],{"class":83,"line":84},[81,1102,1103],{"class":159},"# src\u002Fmytool\u002Foptions.py\n",[81,1105,1106,1108,1110,1112],{"class":83,"line":101},[81,1107,88],{"class":87},[81,1109,106],{"class":91},[81,1111,95],{"class":87},[81,1113,111],{"class":91},[81,1115,1116],{"class":83,"line":114},[81,1117,118],{"emptyLinePlaceholder":117},[81,1119,1120,1122],{"class":83,"line":121},[81,1121,95],{"class":87},[81,1123,126],{"class":91},[81,1125,1126],{"class":83,"line":129},[81,1127,118],{"emptyLinePlaceholder":117},[81,1129,1130,1133,1135,1138,1140,1142,1145,1147,1150,1152,1155,1157,1159,1161,1163,1165,1168],{"class":83,"line":134},[81,1131,1132],{"class":91},"Verbose ",[81,1134,140],{"class":87},[81,1136,1137],{"class":91}," Annotated[",[81,1139,24],{"class":196},[81,1141,384],{"class":91},[81,1143,1144],{"class":229},"\"--verbose\"",[81,1146,200],{"class":91},[81,1148,1149],{"class":229},"\"-v\"",[81,1151,200],{"class":91},[81,1153,1154],{"class":203},"count",[81,1156,140],{"class":87},[81,1158,209],{"class":196},[81,1160,200],{"class":91},[81,1162,224],{"class":203},[81,1164,140],{"class":87},[81,1166,1167],{"class":229},"\"More output (repeatable).\"",[81,1169,1170],{"class":91},")]\n",[81,1172,1173,1176,1178,1180,1182,1184,1187,1189,1191,1193,1195,1197,1200,1202,1204,1206,1209],{"class":83,"line":146},[81,1174,1175],{"class":91},"Limit ",[81,1177,140],{"class":87},[81,1179,1137],{"class":91},[81,1181,24],{"class":196},[81,1183,384],{"class":91},[81,1185,1186],{"class":229},"\"--limit\"",[81,1188,200],{"class":91},[81,1190,855],{"class":229},[81,1192,200],{"class":91},[81,1194,265],{"class":203},[81,1196,140],{"class":87},[81,1198,1199],{"class":196},"1",[81,1201,200],{"class":91},[81,1203,224],{"class":203},[81,1205,140],{"class":87},[81,1207,1208],{"class":229},"\"Show at most N items.\"",[81,1210,1170],{"class":91},[81,1212,1213,1216,1218,1220,1222,1224,1227,1229,1231,1233,1236],{"class":83,"line":151},[81,1214,1215],{"class":91},"Json ",[81,1217,140],{"class":87},[81,1219,1137],{"class":91},[81,1221,845],{"class":196},[81,1223,384],{"class":91},[81,1225,1226],{"class":229},"\"--json\"",[81,1228,200],{"class":91},[81,1230,224],{"class":203},[81,1232,140],{"class":87},[81,1234,1235],{"class":229},"\"Machine-readable output.\"",[81,1237,1170],{"class":91},[81,1239,1240,1243,1245,1247,1249,1251,1254,1256,1259,1261,1263,1265,1268],{"class":83,"line":156},[81,1241,1242],{"class":91},"Yes ",[81,1244,140],{"class":87},[81,1246,1137],{"class":91},[81,1248,845],{"class":196},[81,1250,384],{"class":91},[81,1252,1253],{"class":229},"\"--yes\"",[81,1255,200],{"class":91},[81,1257,1258],{"class":229},"\"-y\"",[81,1260,200],{"class":91},[81,1262,224],{"class":203},[81,1264,140],{"class":87},[81,1266,1267],{"class":229},"\"Do not ask for confirmation.\"",[81,1269,1170],{"class":91},[72,1271,1273],{"className":74,"code":1272,"language":76,"meta":77,"style":77},"from mytool.options import Json, Limit\n\n\n@app.command(\"list\")\ndef list_sites(limit: Limit = 20, json: Json = False) -> None:\n    \"\"\"List sites.\"\"\"\n\n\n@app.command(\"builds\")\ndef list_builds(limit: Limit = 50, json: Json = False) -> None:\n    \"\"\"List builds.\"\"\"\n",[14,1274,1275,1287,1291,1295,1306,1334,1339,1343,1347,1358,1384],{"__ignoreMap":77},[81,1276,1277,1279,1282,1284],{"class":83,"line":84},[81,1278,88],{"class":87},[81,1280,1281],{"class":91}," mytool.options ",[81,1283,95],{"class":87},[81,1285,1286],{"class":91}," Json, Limit\n",[81,1288,1289],{"class":83,"line":101},[81,1290,118],{"emptyLinePlaceholder":117},[81,1292,1293],{"class":83,"line":114},[81,1294,118],{"emptyLinePlaceholder":117},[81,1296,1297,1299,1301,1304],{"class":83,"line":121},[81,1298,167],{"class":166},[81,1300,598],{"class":91},[81,1302,1303],{"class":229},"\"list\"",[81,1305,579],{"class":91},[81,1307,1308,1310,1313,1316,1318,1321,1324,1326,1328,1330,1332],{"class":83,"line":129},[81,1309,176],{"class":87},[81,1311,1312],{"class":166}," list_sites",[81,1314,1315],{"class":91},"(limit: Limit ",[81,1317,140],{"class":87},[81,1319,1320],{"class":196}," 20",[81,1322,1323],{"class":91},", json: Json ",[81,1325,140],{"class":87},[81,1327,871],{"class":196},[81,1329,297],{"class":91},[81,1331,300],{"class":196},[81,1333,303],{"class":91},[81,1335,1336],{"class":83,"line":134},[81,1337,1338],{"class":229},"    \"\"\"List sites.\"\"\"\n",[81,1340,1341],{"class":83,"line":146},[81,1342,118],{"emptyLinePlaceholder":117},[81,1344,1345],{"class":83,"line":151},[81,1346,118],{"emptyLinePlaceholder":117},[81,1348,1349,1351,1353,1356],{"class":83,"line":156},[81,1350,167],{"class":166},[81,1352,598],{"class":91},[81,1354,1355],{"class":229},"\"builds\"",[81,1357,579],{"class":91},[81,1359,1360,1362,1365,1367,1369,1372,1374,1376,1378,1380,1382],{"class":83,"line":163},[81,1361,176],{"class":87},[81,1363,1364],{"class":166}," list_builds",[81,1366,1315],{"class":91},[81,1368,140],{"class":87},[81,1370,1371],{"class":196}," 50",[81,1373,1323],{"class":91},[81,1375,140],{"class":87},[81,1377,871],{"class":196},[81,1379,297],{"class":91},[81,1381,300],{"class":196},[81,1383,303],{"class":91},[81,1385,1386],{"class":83,"line":173},[81,1387,1388],{"class":229},"    \"\"\"List builds.\"\"\"\n",[68,1390],{"name":1391},"an-alias-flow",[10,1393,1394,1395,1398,1399,1403,1404,39],{},"Every command that uses ",[14,1396,1397],{},"Limit"," gets the same flag names, help text and validation, while each keeps its own default. This is the cleanest way to enforce the naming consistency described in ",[35,1400,1402],{"href":1401},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently\u002F","naming commands and flags consistently",", and it composes with the decorator approach in ",[35,1405,1407],{"href":1406},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands\u002F","sharing common options across commands",[1409,1410,1412],"h3",{"id":1411},"aliases-with-shared-validation","Aliases with shared validation",[10,1414,1415,1416,1418],{},"Because an alias is just a type, it can carry behaviour as well as names. A parser or callback inside the ",[14,1417,16],{}," travels with the alias, so every command that uses it validates the value the same way:",[72,1420,1422],{"className":74,"code":1421,"language":76,"meta":77,"style":77},"import re\nfrom typing import Annotated\n\nimport typer\n\n\ndef parse_duration(value: str | float) -> float:\n    if isinstance(value, (int, float)):          # the Python default passes through the parser too\n        return float(value)\n    m = re.fullmatch(r\"(\\d+)([smh])\", value)\n    if not m:\n        raise typer.BadParameter(\"use a number and a unit, e.g. 30s, 5m, 2h\")\n    return float(m[1]) * {\"s\": 1, \"m\": 60, \"h\": 3600}[m[2]]\n\n\nTimeout = Annotated[float, typer.Option(\"--timeout\", parser=parse_duration, metavar=\"DURATION\",\n                                        help=\"How long to wait, e.g. 30s or 5m.\")]\n",[14,1423,1424,1431,1441,1445,1451,1455,1459,1484,1507,1517,1546,1556,1569,1627,1631,1635,1671],{"__ignoreMap":77},[81,1425,1426,1428],{"class":83,"line":84},[81,1427,95],{"class":87},[81,1429,1430],{"class":91}," re\n",[81,1432,1433,1435,1437,1439],{"class":83,"line":101},[81,1434,88],{"class":87},[81,1436,106],{"class":91},[81,1438,95],{"class":87},[81,1440,111],{"class":91},[81,1442,1443],{"class":83,"line":114},[81,1444,118],{"emptyLinePlaceholder":117},[81,1446,1447,1449],{"class":83,"line":121},[81,1448,95],{"class":87},[81,1450,126],{"class":91},[81,1452,1453],{"class":83,"line":129},[81,1454,118],{"emptyLinePlaceholder":117},[81,1456,1457],{"class":83,"line":134},[81,1458,118],{"emptyLinePlaceholder":117},[81,1460,1461,1463,1466,1469,1471,1474,1477,1479,1482],{"class":83,"line":146},[81,1462,176],{"class":87},[81,1464,1465],{"class":166}," parse_duration",[81,1467,1468],{"class":91},"(value: ",[81,1470,601],{"class":196},[81,1472,1473],{"class":87}," |",[81,1475,1476],{"class":196}," float",[81,1478,297],{"class":91},[81,1480,1481],{"class":196},"float",[81,1483,303],{"class":91},[81,1485,1486,1489,1492,1495,1497,1499,1501,1504],{"class":83,"line":151},[81,1487,1488],{"class":87},"    if",[81,1490,1491],{"class":196}," isinstance",[81,1493,1494],{"class":91},"(value, (",[81,1496,24],{"class":196},[81,1498,200],{"class":91},[81,1500,1481],{"class":196},[81,1502,1503],{"class":91},")):          ",[81,1505,1506],{"class":159},"# the Python default passes through the parser too\n",[81,1508,1509,1512,1514],{"class":83,"line":156},[81,1510,1511],{"class":87},"        return",[81,1513,1476],{"class":196},[81,1515,1516],{"class":91},"(value)\n",[81,1518,1519,1522,1524,1527,1530,1532,1535,1538,1541,1543],{"class":83,"line":163},[81,1520,1521],{"class":91},"    m ",[81,1523,140],{"class":87},[81,1525,1526],{"class":91}," re.fullmatch(",[81,1528,1529],{"class":87},"r",[81,1531,968],{"class":229},[81,1533,1534],{"class":196},"(\\d",[81,1536,1537],{"class":87},"+",[81,1539,1540],{"class":196},")([smh])",[81,1542,968],{"class":229},[81,1544,1545],{"class":91},", value)\n",[81,1547,1548,1550,1553],{"class":83,"line":173},[81,1549,1488],{"class":87},[81,1551,1552],{"class":87}," not",[81,1554,1555],{"class":91}," m:\n",[81,1557,1558,1561,1564,1567],{"class":83,"line":185},[81,1559,1560],{"class":87},"        raise",[81,1562,1563],{"class":91}," typer.BadParameter(",[81,1565,1566],{"class":229},"\"use a number and a unit, e.g. 30s, 5m, 2h\"",[81,1568,579],{"class":91},[81,1570,1571,1574,1576,1579,1581,1584,1587,1590,1593,1596,1598,1600,1603,1605,1608,1610,1613,1615,1618,1621,1624],{"class":83,"line":236},[81,1572,1573],{"class":87},"    return",[81,1575,1476],{"class":196},[81,1577,1578],{"class":91},"(m[",[81,1580,1199],{"class":196},[81,1582,1583],{"class":91},"]) ",[81,1585,1586],{"class":87},"*",[81,1588,1589],{"class":91}," {",[81,1591,1592],{"class":229},"\"s\"",[81,1594,1595],{"class":91},": ",[81,1597,1199],{"class":196},[81,1599,200],{"class":91},[81,1601,1602],{"class":229},"\"m\"",[81,1604,1595],{"class":91},[81,1606,1607],{"class":196},"60",[81,1609,200],{"class":91},[81,1611,1612],{"class":229},"\"h\"",[81,1614,1595],{"class":91},[81,1616,1617],{"class":196},"3600",[81,1619,1620],{"class":91},"}[m[",[81,1622,1623],{"class":196},"2",[81,1625,1626],{"class":91},"]]\n",[81,1628,1629],{"class":83,"line":294},[81,1630,118],{"emptyLinePlaceholder":117},[81,1632,1633],{"class":83,"line":306},[81,1634,118],{"emptyLinePlaceholder":117},[81,1636,1637,1640,1642,1644,1646,1648,1651,1653,1656,1658,1661,1664,1666,1669],{"class":83,"line":312},[81,1638,1639],{"class":91},"Timeout ",[81,1641,140],{"class":87},[81,1643,1137],{"class":91},[81,1645,1481],{"class":196},[81,1647,384],{"class":91},[81,1649,1650],{"class":229},"\"--timeout\"",[81,1652,200],{"class":91},[81,1654,1655],{"class":203},"parser",[81,1657,140],{"class":87},[81,1659,1660],{"class":91},"parse_duration, ",[81,1662,1663],{"class":203},"metavar",[81,1665,140],{"class":87},[81,1667,1668],{"class":229},"\"DURATION\"",[81,1670,425],{"class":91},[81,1672,1673,1676,1678,1681],{"class":83,"line":317},[81,1674,1675],{"class":203},"                                        help",[81,1677,140],{"class":87},[81,1679,1680],{"class":229},"\"How long to wait, e.g. 30s or 5m.\"",[81,1682,1170],{"class":91},[10,1684,1685,1686,1689,1690,1692,1693,1696,1697,39],{},"A command declares ",[14,1687,1688],{},"timeout: Timeout = 30.0"," and receives seconds as a ",[14,1691,1481],{},", however the user wrote it. When the accepted format changes, it changes in one place. For richer conversions with their own error messages and completion, a custom Click parameter type plugged in with ",[14,1694,1695],{},"click_type="," does the same job; see ",[35,1698,1700],{"href":1699},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types\u002F","writing custom Click parameter types",[41,1702,1704],{"id":1703},"migrating-an-existing-app","Migrating an existing app",[10,1706,1707],{},"The conversion is mechanical and can be done one command at a time, since both styles coexist in one app:",[1709,1710,1711,1724,1736,1742,1752],"ol",{},[49,1712,1713,1714,1716,1717,1720,1721,39],{},"Move ",[14,1715,16],{}," \u002F ",[14,1718,1719],{},"typer.Argument(...)"," into ",[14,1722,1723],{},"Annotated[type, ...]",[49,1725,1726,1727,1075,1730,1733,1734,39],{},"Take the first positional argument out of the ",[14,1728,1729],{},"Option",[14,1731,1732],{},"Argument"," call — that was the default — and put it after ",[14,1735,140],{},[49,1737,1738,1739,1741],{},"For required parameters, delete the ",[14,1740,197],{}," and give no default.",[49,1743,1744,1745,200,1747,200,1749,1751],{},"Keep every other setting (names, ",[14,1746,224],{},[14,1748,772],{},[14,1750,265],{},", callbacks) inside the call unchanged.",[49,1753,1754,1755,1757,1758,1760],{},"Replace mutable defaults (",[14,1756,1053],{},") with ",[14,1759,300],{}," and handle it in the body.",[10,1762,1763,1764,1766],{},"Run the command tests after each conversion; behaviour should be identical. A helpful extra check is diffing ",[14,1765,1063],{}," output before and after, which catches accidentally dropped settings.",[41,1768,1770],{"id":1769},"ux-considerations","UX considerations",[10,1772,1773,1775],{},[14,1774,28],{}," changes the code, not the interface, but it improves the interface indirectly:",[68,1777],{"name":1778},"an-terminal",[46,1780,1781,1787,1800],{},[49,1782,1783,1786],{},[448,1784,1785],{},"Consistency by construction."," Shared aliases make identical flags truly identical across commands, including help wording.",[49,1788,1789,1792,1793,1796,1797,1799],{},[448,1790,1791],{},"Fewer default-related bugs."," Because type checkers understand the defaults, mistakes like an ",[14,1794,1795],{},"Optional"," path used without a ",[14,1798,300],{}," check are caught before users hit them.",[49,1801,1802,1805],{},[448,1803,1804],{},"Easier reuse of command functions."," Commands that are plain functions with plain defaults can be called from other commands or from a Python API without faking Typer's parameter objects.",[41,1807,1809],{"id":1808},"testing-the-behaviour","Testing the behaviour",[10,1811,1812,1813,1816],{},"Test through ",[14,1814,1815],{},"CliRunner"," as usual, and — newly possible — call command functions directly as Python functions:",[72,1818,1820],{"className":74,"code":1819,"language":76,"meta":77,"style":77},"# tests\u002Ftest_deploy.py\nfrom pathlib import Path\n\nfrom typer.testing import CliRunner\n\nfrom mytool.cli import Env, app, deploy\n\nrunner = CliRunner()\n\n\ndef test_cli_defaults(tmp_path: Path):\n    result = runner.invoke(app, [\"deploy\", str(tmp_path)])\n    assert result.exit_code == 0\n    assert \"to dev retries=3 tags=[] dry_run=False\" in result.output\n\n\ndef test_repeatable_tags_and_env_var(tmp_path: Path):\n    result = runner.invoke(app, [\"deploy\", str(tmp_path), \"--tag\", \"a\", \"--tag\", \"b\"],\n                           env={\"MYTOOL_RETRIES\": \"5\"})\n    assert \"retries=5 tags=['a', 'b']\" in result.output\n\n\ndef test_validation_still_applies(tmp_path: Path):\n    result = runner.invoke(app, [\"deploy\", str(tmp_path), \"--retries\", \"99\"])\n    assert result.exit_code == 2\n\n\ndef test_callable_as_plain_python(tmp_path: Path, capsys):\n    deploy(tmp_path, env=Env.prod)                     # real defaults, no Typer involved\n    assert \"to prod retries=3\" in capsys.readouterr().out\n",[14,1821,1822,1827,1837,1841,1853,1857,1869,1873,1883,1887,1891,1901,1921,1935,1948,1952,1956,1965,2001,2020,2031,2035,2039,2048,2074,2085,2089,2093,2103,2119],{"__ignoreMap":77},[81,1823,1824],{"class":83,"line":84},[81,1825,1826],{"class":159},"# tests\u002Ftest_deploy.py\n",[81,1828,1829,1831,1833,1835],{"class":83,"line":101},[81,1830,88],{"class":87},[81,1832,92],{"class":91},[81,1834,95],{"class":87},[81,1836,98],{"class":91},[81,1838,1839],{"class":83,"line":114},[81,1840,118],{"emptyLinePlaceholder":117},[81,1842,1843,1845,1848,1850],{"class":83,"line":121},[81,1844,88],{"class":87},[81,1846,1847],{"class":91}," typer.testing ",[81,1849,95],{"class":87},[81,1851,1852],{"class":91}," CliRunner\n",[81,1854,1855],{"class":83,"line":129},[81,1856,118],{"emptyLinePlaceholder":117},[81,1858,1859,1861,1864,1866],{"class":83,"line":134},[81,1860,88],{"class":87},[81,1862,1863],{"class":91}," mytool.cli ",[81,1865,95],{"class":87},[81,1867,1868],{"class":91}," Env, app, deploy\n",[81,1870,1871],{"class":83,"line":146},[81,1872,118],{"emptyLinePlaceholder":117},[81,1874,1875,1878,1880],{"class":83,"line":151},[81,1876,1877],{"class":91},"runner ",[81,1879,140],{"class":87},[81,1881,1882],{"class":91}," CliRunner()\n",[81,1884,1885],{"class":83,"line":156},[81,1886,118],{"emptyLinePlaceholder":117},[81,1888,1889],{"class":83,"line":163},[81,1890,118],{"emptyLinePlaceholder":117},[81,1892,1893,1895,1898],{"class":83,"line":173},[81,1894,176],{"class":87},[81,1896,1897],{"class":166}," test_cli_defaults",[81,1899,1900],{"class":91},"(tmp_path: Path):\n",[81,1902,1903,1906,1908,1911,1914,1916,1918],{"class":83,"line":185},[81,1904,1905],{"class":91},"    result ",[81,1907,140],{"class":87},[81,1909,1910],{"class":91}," runner.invoke(app, [",[81,1912,1913],{"class":229},"\"deploy\"",[81,1915,200],{"class":91},[81,1917,601],{"class":196},[81,1919,1920],{"class":91},"(tmp_path)])\n",[81,1922,1923,1926,1929,1932],{"class":83,"line":236},[81,1924,1925],{"class":87},"    assert",[81,1927,1928],{"class":91}," result.exit_code ",[81,1930,1931],{"class":87},"==",[81,1933,1934],{"class":196}," 0\n",[81,1936,1937,1939,1942,1945],{"class":83,"line":294},[81,1938,1925],{"class":87},[81,1940,1941],{"class":229}," \"to dev retries=3 tags=[] dry_run=False\"",[81,1943,1944],{"class":87}," in",[81,1946,1947],{"class":91}," result.output\n",[81,1949,1950],{"class":83,"line":306},[81,1951,118],{"emptyLinePlaceholder":117},[81,1953,1954],{"class":83,"line":312},[81,1955,118],{"emptyLinePlaceholder":117},[81,1957,1958,1960,1963],{"class":83,"line":317},[81,1959,176],{"class":87},[81,1961,1962],{"class":166}," test_repeatable_tags_and_env_var",[81,1964,1900],{"class":91},[81,1966,1967,1969,1971,1973,1975,1977,1979,1982,1984,1986,1989,1991,1993,1995,1998],{"class":83,"line":322},[81,1968,1905],{"class":91},[81,1970,140],{"class":87},[81,1972,1910],{"class":91},[81,1974,1913],{"class":229},[81,1976,200],{"class":91},[81,1978,601],{"class":196},[81,1980,1981],{"class":91},"(tmp_path), ",[81,1983,819],{"class":229},[81,1985,200],{"class":91},[81,1987,1988],{"class":229},"\"a\"",[81,1990,200],{"class":91},[81,1992,819],{"class":229},[81,1994,200],{"class":91},[81,1996,1997],{"class":229},"\"b\"",[81,1999,2000],{"class":91},"],\n",[81,2002,2003,2006,2008,2010,2012,2014,2017],{"class":83,"line":328},[81,2004,2005],{"class":203},"                           env",[81,2007,140],{"class":87},[81,2009,920],{"class":91},[81,2011,777],{"class":229},[81,2013,1595],{"class":91},[81,2015,2016],{"class":229},"\"5\"",[81,2018,2019],{"class":91},"})\n",[81,2021,2022,2024,2027,2029],{"class":83,"line":335},[81,2023,1925],{"class":87},[81,2025,2026],{"class":229}," \"retries=5 tags=['a', 'b']\"",[81,2028,1944],{"class":87},[81,2030,1947],{"class":91},[81,2032,2033],{"class":83,"line":345},[81,2034,118],{"emptyLinePlaceholder":117},[81,2036,2037],{"class":83,"line":376},[81,2038,118],{"emptyLinePlaceholder":117},[81,2040,2041,2043,2046],{"class":83,"line":428},[81,2042,176],{"class":87},[81,2044,2045],{"class":166}," test_validation_still_applies",[81,2047,1900],{"class":91},[81,2049,2050,2052,2054,2056,2058,2060,2062,2064,2066,2068,2071],{"class":83,"line":437},[81,2051,1905],{"class":91},[81,2053,140],{"class":87},[81,2055,1910],{"class":91},[81,2057,1913],{"class":229},[81,2059,200],{"class":91},[81,2061,601],{"class":196},[81,2063,1981],{"class":91},[81,2065,255],{"class":229},[81,2067,200],{"class":91},[81,2069,2070],{"class":229},"\"99\"",[81,2072,2073],{"class":91},"])\n",[81,2075,2076,2078,2080,2082],{"class":83,"line":727},[81,2077,1925],{"class":87},[81,2079,1928],{"class":91},[81,2081,1931],{"class":87},[81,2083,2084],{"class":196}," 2\n",[81,2086,2087],{"class":83,"line":747},[81,2088,118],{"emptyLinePlaceholder":117},[81,2090,2091],{"class":83,"line":782},[81,2092,118],{"emptyLinePlaceholder":117},[81,2094,2095,2097,2100],{"class":83,"line":800},[81,2096,176],{"class":87},[81,2098,2099],{"class":166}," test_callable_as_plain_python",[81,2101,2102],{"class":91},"(tmp_path: Path, capsys):\n",[81,2104,2105,2108,2111,2113,2116],{"class":83,"line":839},[81,2106,2107],{"class":91},"    deploy(tmp_path, ",[81,2109,2110],{"class":203},"env",[81,2112,140],{"class":87},[81,2114,2115],{"class":91},"Env.prod)                     ",[81,2117,2118],{"class":159},"# real defaults, no Typer involved\n",[81,2120,2121,2123,2126,2128],{"class":83,"line":876},[81,2122,1925],{"class":87},[81,2124,2125],{"class":229}," \"to prod retries=3\"",[81,2127,1944],{"class":87},[81,2129,2130],{"class":91}," capsys.readouterr().out\n",[10,2132,2133,2134,2136,2137,2139,2140,39],{},"The last test would fail in the old style, because ",[14,2135,466],{}," would default to an ",[14,2138,476],{}," object rather than ",[14,2141,250],{},[41,2143,2145],{"id":2144},"conclusion","Conclusion",[10,2147,2148,2149,2151],{},"The ",[14,2150,28],{}," style puts a parameter's type and its command-line metadata in one place and leaves the default where Python expects it. Type checkers see real types, command functions become callable as plain Python, and common options become reusable type aliases that keep flags consistent across a whole CLI. Behaviour and help output are unchanged, both styles coexist, and migration is a mechanical, command-by-command change — which makes it an easy improvement to adopt in any Typer codebase.",[41,2153,2155],{"id":2154},"frequently-asked-questions","Frequently asked questions",[1409,2157,2159],{"id":2158},"is-the-old-style-deprecated","Is the old style deprecated?",[10,2161,2162,2163,2165],{},"It still works and is not scheduled for removal, but Typer's documentation uses ",[14,2164,28],{}," throughout and recommends it for new code. Mixing both in one app is fine during migration.",[1409,2167,2169,2170,2173,2174,2176],{"id":2168},"can-i-put-a-default-inside-typeroption-and-after-at-the-same-time","Can I put a default inside ",[14,2171,2172],{},"typer.Option"," and after ",[14,2175,140],{}," at the same time?",[10,2178,2179,2180,2182],{},"No — Typer raises an error if a default appears in both places, because it would be ambiguous. Put it after ",[14,2181,140],{}," only.",[1409,2184,2186],{"id":2185},"how-do-i-declare-a-required-option-not-argument","How do I declare a required option (not argument)?",[10,2188,2189,2190,2193],{},"Give it no default: ",[14,2191,2192],{},"token: Annotated[str, typer.Option(help=\"API token.\")]",". Typer makes it required and reports \"Missing option '--token'\" when it is absent. Consider whether it should be a positional argument instead.",[1409,2195,2197],{"id":2196},"why-does-my-options-parser-receive-the-default-value","Why does my option's parser receive the default value?",[10,2199,2200,2201,2204,2205,2207,2208,2211],{},"Click runs defaults through the same conversion as values typed on the command line, so a ",[14,2202,2203],{},"parser="," function sees the Python default too — a ",[14,2206,1481],{}," rather than a string, in the duration example above. Either make the parser accept already-converted values, as shown, or express the default in the command-line form (",[14,2209,2210],{},"\"30s\"","), which keeps conversion in one place but makes the annotation's type and the default's type differ.",[1409,2213,2215,2216,2218],{"id":2214},"does-annotated-work-with-callbacks-and-autocompletion","Does ",[14,2217,28],{}," work with callbacks and autocompletion?",[10,2220,2221,2222,200,2225,200,2228,2230,2231,2233],{},"Yes. ",[14,2223,2224],{},"callback=",[14,2226,2227],{},"autocompletion=",[14,2229,2203],{}," and every other setting go inside the ",[14,2232,16],{}," in the annotation, exactly as in the old style.",[41,2235,2237],{"id":2236},"related","Related",[46,2239,2240,2246,2252,2258,2264],{},[49,2241,2242,2243],{},"Up: ",[35,2244,2245],{"href":37},"Typer vs Click: when to use each",[49,2247,2248],{},[35,2249,2251],{"href":2250},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained\u002F","Typer callback functions explained",[49,2253,2254],{},[35,2255,2257],{"href":2256},"\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",[49,2259,2260],{},[35,2261,2263],{"href":2262},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click\u002F","Building dynamic commands in Click",[49,2265,2266],{},[35,2267,2268],{"href":480},"Type-checking Click and Typer code with mypy",[2270,2271,2272],"style",{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":77,"searchDepth":101,"depth":101,"links":2274},[2275,2276,2277,2278,2281,2282,2283,2284,2285,2294],{"id":43,"depth":101,"text":44},{"id":65,"depth":101,"text":66},{"id":503,"depth":101,"text":504},{"id":1086,"depth":101,"text":1087,"children":2279},[2280],{"id":1411,"depth":114,"text":1412},{"id":1703,"depth":101,"text":1704},{"id":1769,"depth":101,"text":1770},{"id":1808,"depth":101,"text":1809},{"id":2144,"depth":101,"text":2145},{"id":2154,"depth":101,"text":2155,"children":2286},[2287,2288,2290,2291,2292],{"id":2158,"depth":114,"text":2159},{"id":2168,"depth":114,"text":2289},"Can I put a default inside typer.Option and after = at the same time?",{"id":2185,"depth":114,"text":2186},{"id":2196,"depth":114,"text":2197},{"id":2214,"depth":114,"text":2293},"Does Annotated work with callbacks and autocompletion?",{"id":2236,"depth":101,"text":2237},"2026-09-18","Declare Typer arguments and options with typing.Annotated: real defaults, type-checker friendly signatures, reusable option aliases, and migrating from the old style.","beginner",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer",{"title":5,"description":2296},"modern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002Findex",[2305,2306,2307,2308],"typer","annotated","typing","options","y_TGHO5Gk_JirjnOPtIURWsgc5zddrNhWe83HdzZTzc",[2311,2314,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,2575,2578,2581,2584,2586,2589,2592,2595,2598,2601,2604,2607,2610,2613,2616,2619,2622,2625,2628,2631,2634,2637,2640,2643,2646,2649,2652,2655,2658,2661,2664,2667,2670,2673,2676,2679,2682,2685,2688,2691,2694,2697,2700,2703,2706,2708,2709,2712,2715,2718,2721,2724,2727,2730,2733,2736,2739,2742,2745,2748,2751,2754,2757,2760,2763,2766,2769,2772,2775,2778,2781,2784,2787,2790,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853],{"path":2312,"title":2313},"\u002Fabout","About Python CLI Toolcraft",{"path":2315,"title":2316},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2318,"title":2319},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2321,"title":2322},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2324,"title":2325},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2327,"title":2328},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2330,"title":2331},"\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":2333,"title":2334},"\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":2336,"title":2337},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2339,"title":2340},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2342,"title":2343},"\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":2345,"title":2346},"\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":2348,"title":2349},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2351,"title":2352},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2354,"title":2355},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2357,"title":2358},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2360,"title":2361},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2363,"title":2364},"\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":2366,"title":2367},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2369,"title":2370},"\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":2372,"title":2373},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2375,"title":2376},"\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":2378,"title":2379},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2381,"title":2382},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2384,"title":2385},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2387,"title":2388},"\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":2390,"title":2391},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2393,"title":2394},"\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":2396,"title":2397},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2399,"title":2400},"\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":2402,"title":2403},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2405,"title":2406},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2408,"title":2409},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2411,"title":2412},"\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":2414,"title":2415},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2417,"title":2418},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2420,"title":2421},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2423,"title":2424},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2426,"title":2427},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2429,"title":2430},"\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":2432,"title":2433},"\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":2435,"title":2436},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2438,"title":2439},"\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":2441,"title":2442},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2444,"title":2445},"\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":2447,"title":2448},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2450,"title":2451},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2453,"title":2454},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2456,"title":2457},"\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":2459,"title":2460},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2462,"title":2463},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2465,"title":2466},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2468,"title":2469},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2471,"title":2472},"\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":2474,"title":2475},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2477,"title":2478},"\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":2480,"title":2481},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2483,"title":2484},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2486,"title":2487},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2489,"title":2490},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2492,"title":2493},"\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":2495,"title":2496},"\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":2498,"title":2499},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2501,"title":2502},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2504,"title":2505},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2507,"title":2508},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2510,"title":2511},"\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":2513,"title":2514},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2516,"title":2517},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2519,"title":2520},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2522,"title":2523},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2525,"title":2526},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2528,"title":2529},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2531,"title":2532},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2534,"title":2535},"\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":2537,"title":2538},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2540,"title":2541},"\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":2543,"title":2544},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2546,"title":2547},"\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":2549,"title":2550},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2552,"title":2553},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2555,"title":2556},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2558,"title":2559},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2561,"title":2562},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2564,"title":2565},"\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":2567,"title":2568},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2570,"title":2571},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2573,"title":2574},"\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":2576,"title":2577},"\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":2579,"title":2580},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2582,"title":2583},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1075,"title":2585},"Python CLI Toolcraft",{"path":2587,"title":2588},"\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":2590,"title":2591},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2593,"title":2594},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2596,"title":2597},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2599,"title":2600},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2602,"title":2603},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2605,"title":2606},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2608,"title":2609},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2611,"title":2612},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2614,"title":2615},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2617,"title":2618},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2620,"title":2621},"\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":2623,"title":2624},"\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":2626,"title":2627},"\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":2629,"title":2630},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2632,"title":2633},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2635,"title":2636},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2638,"title":2639},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2641,"title":2642},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2644,"title":2645},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2647,"title":2648},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2650,"title":2651},"\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":2653,"title":2654},"\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":2656,"title":2657},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2659,"title":2660},"\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":2662,"title":2663},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2665,"title":2666},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2668,"title":2669},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2671,"title":2672},"\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":2674,"title":2675},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2677,"title":2678},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2680,"title":2681},"\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":2683,"title":2684},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2686,"title":2687},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2689,"title":2690},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2692,"title":2693},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2695,"title":2696},"\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":2698,"title":2699},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2701,"title":2702},"\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":2704,"title":2705},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2707,"title":2251},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained",{"path":2301,"title":5},{"path":2710,"title":2711},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2713,"title":2714},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2716,"title":2717},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2719,"title":2720},"\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":2722,"title":2723},"\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":2725,"title":2726},"\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":2728,"title":2729},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2731,"title":2732},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2734,"title":2735},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2737,"title":2738},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2740,"title":2741},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2743,"title":2744},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2746,"title":2747},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2749,"title":2750},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2752,"title":2753},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2755,"title":2756},"\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":2758,"title":2759},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2761,"title":2762},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2764,"title":2765},"\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":2767,"title":2768},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2770,"title":2771},"\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":2773,"title":2774},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2776,"title":2777},"\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":2779,"title":2780},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2782,"title":2783},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2785,"title":2786},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2788,"title":2789},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2791,"title":2792},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2794,"title":2795},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2797,"title":2798},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2800,"title":2801},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2803,"title":2804},"\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":2806,"title":2807},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2809,"title":2810},"\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":2812,"title":2813},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2815,"title":2816},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2818,"title":2819},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2821,"title":2822},"\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":2824,"title":2825},"\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":2827,"title":2828},"\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":2830,"title":2831},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2833,"title":2834},"\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":2836,"title":2837},"\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":2839,"title":2840},"\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":2842,"title":2843},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2845,"title":2846},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2848,"title":2849},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2851,"title":2852},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2854,"title":2855},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907464]