[{"data":1,"prerenderedAt":3213},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts\u002F":3,"content-directory":2368},{"id":4,"title":5,"body":6,"date":2354,"description":2355,"difficulty":2356,"draft":2357,"extension":2358,"meta":2359,"navigation":195,"path":2360,"seo":2361,"stem":2362,"tags":2363,"updated":2354,"__hash__":2367},"content\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts\u002Findex.md","Building a Type-Hinted Python CLI with Cyclopts",{"type":7,"value":8,"toc":2334},"minimark",[9,28,33,63,67,70,74,150,154,1156,1210,1213,1218,1238,1242,1245,1443,1458,1462,1527,1530,1534,1580,1584,1590,2188,2196,2200,2206,2210,2218,2245,2249,2260,2264,2276,2280,2295,2299,2330],[10,11,12,13,17,18,21,22,27],"p",{},"Cyclopts builds a command-line interface from ordinary Python functions: positional parameters become arguments, keyword-only parameters become options, type hints drive conversion and validation, and the docstring becomes the help text. If that sounds like Typer, it is the same idea — and Cyclopts was written by someone who liked Typer's idea and wanted it to go further: native support for ",[14,15,16],"code",{},"Literal",", unions and dataclasses, help parsed from docstrings rather than repeated in ",[14,19,20],{},"help="," strings, and configuration sources built in. This guide builds a small notes tool with two commands, a sub-app, validation and environment-variable defaults, then tests it with pytest. It belongs to the ",[23,24,26],"a",{"href":25},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002F","alternative Python CLI frameworks"," topic.",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,48,55],"ul",{},[37,38,39,40,43,44,47],"li",{},"Python 3.10+ and ",[14,41,42],{},"cyclopts"," (",[14,45,46],{},"uv add cyclopts","). The examples were checked against Cyclopts 5.",[37,49,50,51,54],{},"Comfort with type hints and ",[14,52,53],{},"typing.Annotated",".",[37,56,57,58,62],{},"If you know Typer, keep ",[23,59,61],{"href":60},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer\u002F","using Annotated options in Typer"," nearby for comparison.",[29,64,66],{"id":65},"how-cyclopts-reads-a-function","How Cyclopts reads a function",[10,68,69],{},"The mapping rules are short, and knowing them makes every later example predictable.",[71,72],"inline-diagram",{"name":73},"cyclopts-signature-map",[34,75,76,92,102,108,123,131,143],{},[37,77,78,79,83,84,87,88,91],{},"A ",[80,81,82],"strong",{},"positional-or-keyword parameter"," becomes a positional argument that can also be passed by name (",[14,85,86],{},"notes add \"milk\""," or ",[14,89,90],{},"notes add --text \"milk\"",").",[37,93,78,94,97,98,101],{},[80,95,96],{},"keyword-only parameter"," (after ",[14,99,100],{},"*",") becomes an option only.",[37,103,78,104,107],{},[80,105,106],{},"default value"," makes it optional; no default makes it required.",[37,109,78,110,115,116,119,120,91],{},[80,111,112],{},[14,113,114],{},"bool"," becomes a flag with an automatic negative form (",[14,117,118],{},"--dry-run"," \u002F ",[14,121,122],{},"--no-dry-run",[37,124,78,125,130],{},[80,126,127],{},[14,128,129],{},"list[...]"," accepts the option repeatedly.",[37,132,133,138,139,142],{},[80,134,135],{},[14,136,137],{},"Literal[\"json\", \"csv\"]"," or an ",[14,140,141],{},"Enum"," restricts choices and lists them in help.",[37,144,145,146,149],{},"The ",[80,147,148],{},"docstring's parameter section"," (NumPy, Google, Sphinx or Epydoc style) supplies the per-parameter help.",[29,151,153],{"id":152},"the-recipe","The recipe",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-python shiki shiki-themes github-light github-dark","# src\u002Fnotes\u002Fcli.py\nfrom __future__ import annotations\n\nfrom pathlib import Path\nfrom typing import Annotated, Literal\n\nfrom cyclopts import App, Parameter, config, validators\n\napp = App(\n    name=\"notes\",\n    help=\"Keep short notes from the terminal.\",\n    version=\"1.2.0\",\n    config=config.Env(\"NOTES_\", command=False),   # NOTES_PRIORITY=4 sets --priority\n    result_action=\"return_int_as_exit_code_else_zero\",  # print in commands, return data\n)\n\nSTORE: list[dict] = []\n\n\n@app.command\ndef add(\n    text: str,\n    *,\n    tag: Annotated[list[str] | None, Parameter(name=[\"--tag\", \"-t\"])] = None,\n    priority: Annotated[int, Parameter(validator=validators.Number(gte=1, lte=5))] = 3,\n) -> dict:\n    \"\"\"Add a note.\n\n    Parameters\n    ----------\n    text\n        The note text.\n    tag\n        Tags to attach. Repeat the option for several tags.\n    priority\n        1 (low) to 5 (high).\n    \"\"\"\n    note = {\"id\": len(STORE) + 1, \"text\": text, \"tags\": tag or [], \"priority\": priority}\n    STORE.append(note)\n    print(f\"added note {note['id']}\")\n    return note\n\n\n@app.command\ndef export(\n    dest: Annotated[Path, Parameter(validator=validators.Path(dir_okay=False))],\n    *,\n    fmt: Literal[\"json\", \"csv\"] = \"json\",\n    dry_run: bool = False,\n) -> None:\n    \"\"\"Export all notes to a file.\n\n    Parameters\n    ----------\n    dest\n        File to write.\n    fmt\n        Output format.\n    dry_run\n        Print what would be written without writing it.\n    \"\"\"\n    action = \"would write\" if dry_run else \"wrote\"\n    print(f\"{action} {len(STORE)} notes to {dest} as {fmt}\")\n\n\ntags = App(name=\"tags\", help=\"Inspect tags.\")\napp.command(tags)\n\n\n@tags.command(name=\"list\")\ndef list_tags() -> list[str]:\n    \"\"\"List every tag in use.\"\"\"\n    names = sorted({t for note in STORE for t in note[\"tags\"]})\n    for name in names:\n        print(name)\n    return names\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(app())\n","python","",[14,162,163,172,190,197,211,224,229,242,247,259,275,288,301,332,349,355,360,380,385,390,397,409,420,428,472,519,530,536,541,547,553,559,565,571,577,583,589,595,655,664,698,707,712,717,722,732,755,762,785,801,811,817,822,827,832,838,844,850,856,862,868,873,896,950,955,960,989,995,1000,1005,1022,1038,1044,1086,1100,1109,1117,1122,1127,1144],{"__ignoreMap":160},[164,165,168],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"sJ8bj","# src\u002Fnotes\u002Fcli.py\n",[164,173,175,179,183,186],{"class":166,"line":174},2,[164,176,178],{"class":177},"szBVR","from",[164,180,182],{"class":181},"sj4cs"," __future__",[164,184,185],{"class":177}," import",[164,187,189],{"class":188},"sVt8B"," annotations\n",[164,191,193],{"class":166,"line":192},3,[164,194,196],{"emptyLinePlaceholder":195},true,"\n",[164,198,200,202,205,208],{"class":166,"line":199},4,[164,201,178],{"class":177},[164,203,204],{"class":188}," pathlib ",[164,206,207],{"class":177},"import",[164,209,210],{"class":188}," Path\n",[164,212,214,216,219,221],{"class":166,"line":213},5,[164,215,178],{"class":177},[164,217,218],{"class":188}," typing ",[164,220,207],{"class":177},[164,222,223],{"class":188}," Annotated, Literal\n",[164,225,227],{"class":166,"line":226},6,[164,228,196],{"emptyLinePlaceholder":195},[164,230,232,234,237,239],{"class":166,"line":231},7,[164,233,178],{"class":177},[164,235,236],{"class":188}," cyclopts ",[164,238,207],{"class":177},[164,240,241],{"class":188}," App, Parameter, config, validators\n",[164,243,245],{"class":166,"line":244},8,[164,246,196],{"emptyLinePlaceholder":195},[164,248,250,253,256],{"class":166,"line":249},9,[164,251,252],{"class":188},"app ",[164,254,255],{"class":177},"=",[164,257,258],{"class":188}," App(\n",[164,260,262,266,268,272],{"class":166,"line":261},10,[164,263,265],{"class":264},"s4XuR","    name",[164,267,255],{"class":177},[164,269,271],{"class":270},"sZZnC","\"notes\"",[164,273,274],{"class":188},",\n",[164,276,278,281,283,286],{"class":166,"line":277},11,[164,279,280],{"class":264},"    help",[164,282,255],{"class":177},[164,284,285],{"class":270},"\"Keep short notes from the terminal.\"",[164,287,274],{"class":188},[164,289,291,294,296,299],{"class":166,"line":290},12,[164,292,293],{"class":264},"    version",[164,295,255],{"class":177},[164,297,298],{"class":270},"\"1.2.0\"",[164,300,274],{"class":188},[164,302,304,307,309,312,315,318,321,323,326,329],{"class":166,"line":303},13,[164,305,306],{"class":264},"    config",[164,308,255],{"class":177},[164,310,311],{"class":188},"config.Env(",[164,313,314],{"class":270},"\"NOTES_\"",[164,316,317],{"class":188},", ",[164,319,320],{"class":264},"command",[164,322,255],{"class":177},[164,324,325],{"class":181},"False",[164,327,328],{"class":188},"),   ",[164,330,331],{"class":170},"# NOTES_PRIORITY=4 sets --priority\n",[164,333,335,338,340,343,346],{"class":166,"line":334},14,[164,336,337],{"class":264},"    result_action",[164,339,255],{"class":177},[164,341,342],{"class":270},"\"return_int_as_exit_code_else_zero\"",[164,344,345],{"class":188},",  ",[164,347,348],{"class":170},"# print in commands, return data\n",[164,350,352],{"class":166,"line":351},15,[164,353,354],{"class":188},")\n",[164,356,358],{"class":166,"line":357},16,[164,359,196],{"emptyLinePlaceholder":195},[164,361,363,366,369,372,375,377],{"class":166,"line":362},17,[164,364,365],{"class":181},"STORE",[164,367,368],{"class":188},": list[",[164,370,371],{"class":181},"dict",[164,373,374],{"class":188},"] ",[164,376,255],{"class":177},[164,378,379],{"class":188}," []\n",[164,381,383],{"class":166,"line":382},18,[164,384,196],{"emptyLinePlaceholder":195},[164,386,388],{"class":166,"line":387},19,[164,389,196],{"emptyLinePlaceholder":195},[164,391,393],{"class":166,"line":392},20,[164,394,396],{"class":395},"sScJk","@app.command\n",[164,398,400,403,406],{"class":166,"line":399},21,[164,401,402],{"class":177},"def",[164,404,405],{"class":395}," add",[164,407,408],{"class":188},"(\n",[164,410,412,415,418],{"class":166,"line":411},22,[164,413,414],{"class":188},"    text: ",[164,416,417],{"class":181},"str",[164,419,274],{"class":188},[164,421,423,426],{"class":166,"line":422},23,[164,424,425],{"class":177},"    *",[164,427,274],{"class":188},[164,429,431,434,436,438,441,444,447,450,452,455,458,460,463,466,468,470],{"class":166,"line":430},24,[164,432,433],{"class":188},"    tag: Annotated[list[",[164,435,417],{"class":181},[164,437,374],{"class":188},[164,439,440],{"class":177},"|",[164,442,443],{"class":181}," None",[164,445,446],{"class":188},", Parameter(",[164,448,449],{"class":264},"name",[164,451,255],{"class":177},[164,453,454],{"class":188},"[",[164,456,457],{"class":270},"\"--tag\"",[164,459,317],{"class":188},[164,461,462],{"class":270},"\"-t\"",[164,464,465],{"class":188},"])] ",[164,467,255],{"class":177},[164,469,443],{"class":181},[164,471,274],{"class":188},[164,473,475,478,481,483,486,488,491,494,496,499,501,504,506,509,512,514,517],{"class":166,"line":474},25,[164,476,477],{"class":188},"    priority: Annotated[",[164,479,480],{"class":181},"int",[164,482,446],{"class":188},[164,484,485],{"class":264},"validator",[164,487,255],{"class":177},[164,489,490],{"class":188},"validators.Number(",[164,492,493],{"class":264},"gte",[164,495,255],{"class":177},[164,497,498],{"class":181},"1",[164,500,317],{"class":188},[164,502,503],{"class":264},"lte",[164,505,255],{"class":177},[164,507,508],{"class":181},"5",[164,510,511],{"class":188},"))] ",[164,513,255],{"class":177},[164,515,516],{"class":181}," 3",[164,518,274],{"class":188},[164,520,522,525,527],{"class":166,"line":521},26,[164,523,524],{"class":188},") -> ",[164,526,371],{"class":181},[164,528,529],{"class":188},":\n",[164,531,533],{"class":166,"line":532},27,[164,534,535],{"class":270},"    \"\"\"Add a note.\n",[164,537,539],{"class":166,"line":538},28,[164,540,196],{"emptyLinePlaceholder":195},[164,542,544],{"class":166,"line":543},29,[164,545,546],{"class":270},"    Parameters\n",[164,548,550],{"class":166,"line":549},30,[164,551,552],{"class":270},"    ----------\n",[164,554,556],{"class":166,"line":555},31,[164,557,558],{"class":270},"    text\n",[164,560,562],{"class":166,"line":561},32,[164,563,564],{"class":270},"        The note text.\n",[164,566,568],{"class":166,"line":567},33,[164,569,570],{"class":270},"    tag\n",[164,572,574],{"class":166,"line":573},34,[164,575,576],{"class":270},"        Tags to attach. Repeat the option for several tags.\n",[164,578,580],{"class":166,"line":579},35,[164,581,582],{"class":270},"    priority\n",[164,584,586],{"class":166,"line":585},36,[164,587,588],{"class":270},"        1 (low) to 5 (high).\n",[164,590,592],{"class":166,"line":591},37,[164,593,594],{"class":270},"    \"\"\"\n",[164,596,598,601,603,606,609,612,615,618,620,623,626,629,631,634,637,640,643,646,649,652],{"class":166,"line":597},38,[164,599,600],{"class":188},"    note ",[164,602,255],{"class":177},[164,604,605],{"class":188}," {",[164,607,608],{"class":270},"\"id\"",[164,610,611],{"class":188},": ",[164,613,614],{"class":181},"len",[164,616,617],{"class":188},"(",[164,619,365],{"class":181},[164,621,622],{"class":188},") ",[164,624,625],{"class":177},"+",[164,627,628],{"class":181}," 1",[164,630,317],{"class":188},[164,632,633],{"class":270},"\"text\"",[164,635,636],{"class":188},": text, ",[164,638,639],{"class":270},"\"tags\"",[164,641,642],{"class":188},": tag ",[164,644,645],{"class":177},"or",[164,647,648],{"class":188}," [], ",[164,650,651],{"class":270},"\"priority\"",[164,653,654],{"class":188},": priority}\n",[164,656,658,661],{"class":166,"line":657},39,[164,659,660],{"class":181},"    STORE",[164,662,663],{"class":188},".append(note)\n",[164,665,667,670,672,675,678,681,684,687,690,693,696],{"class":166,"line":666},40,[164,668,669],{"class":181},"    print",[164,671,617],{"class":188},[164,673,674],{"class":177},"f",[164,676,677],{"class":270},"\"added note ",[164,679,680],{"class":181},"{",[164,682,683],{"class":188},"note[",[164,685,686],{"class":270},"'id'",[164,688,689],{"class":188},"]",[164,691,692],{"class":181},"}",[164,694,695],{"class":270},"\"",[164,697,354],{"class":188},[164,699,701,704],{"class":166,"line":700},41,[164,702,703],{"class":177},"    return",[164,705,706],{"class":188}," note\n",[164,708,710],{"class":166,"line":709},42,[164,711,196],{"emptyLinePlaceholder":195},[164,713,715],{"class":166,"line":714},43,[164,716,196],{"emptyLinePlaceholder":195},[164,718,720],{"class":166,"line":719},44,[164,721,396],{"class":395},[164,723,725,727,730],{"class":166,"line":724},45,[164,726,402],{"class":177},[164,728,729],{"class":395}," export",[164,731,408],{"class":188},[164,733,735,738,740,742,745,748,750,752],{"class":166,"line":734},46,[164,736,737],{"class":188},"    dest: Annotated[Path, Parameter(",[164,739,485],{"class":264},[164,741,255],{"class":177},[164,743,744],{"class":188},"validators.Path(",[164,746,747],{"class":264},"dir_okay",[164,749,255],{"class":177},[164,751,325],{"class":181},[164,753,754],{"class":188},"))],\n",[164,756,758,760],{"class":166,"line":757},47,[164,759,425],{"class":177},[164,761,274],{"class":188},[164,763,765,768,771,773,776,778,780,783],{"class":166,"line":764},48,[164,766,767],{"class":188},"    fmt: Literal[",[164,769,770],{"class":270},"\"json\"",[164,772,317],{"class":188},[164,774,775],{"class":270},"\"csv\"",[164,777,374],{"class":188},[164,779,255],{"class":177},[164,781,782],{"class":270}," \"json\"",[164,784,274],{"class":188},[164,786,788,791,793,796,799],{"class":166,"line":787},49,[164,789,790],{"class":188},"    dry_run: ",[164,792,114],{"class":181},[164,794,795],{"class":177}," =",[164,797,798],{"class":181}," False",[164,800,274],{"class":188},[164,802,804,806,809],{"class":166,"line":803},50,[164,805,524],{"class":188},[164,807,808],{"class":181},"None",[164,810,529],{"class":188},[164,812,814],{"class":166,"line":813},51,[164,815,816],{"class":270},"    \"\"\"Export all notes to a file.\n",[164,818,820],{"class":166,"line":819},52,[164,821,196],{"emptyLinePlaceholder":195},[164,823,825],{"class":166,"line":824},53,[164,826,546],{"class":270},[164,828,830],{"class":166,"line":829},54,[164,831,552],{"class":270},[164,833,835],{"class":166,"line":834},55,[164,836,837],{"class":270},"    dest\n",[164,839,841],{"class":166,"line":840},56,[164,842,843],{"class":270},"        File to write.\n",[164,845,847],{"class":166,"line":846},57,[164,848,849],{"class":270},"    fmt\n",[164,851,853],{"class":166,"line":852},58,[164,854,855],{"class":270},"        Output format.\n",[164,857,859],{"class":166,"line":858},59,[164,860,861],{"class":270},"    dry_run\n",[164,863,865],{"class":166,"line":864},60,[164,866,867],{"class":270},"        Print what would be written without writing it.\n",[164,869,871],{"class":166,"line":870},61,[164,872,594],{"class":270},[164,874,876,879,881,884,887,890,893],{"class":166,"line":875},62,[164,877,878],{"class":188},"    action ",[164,880,255],{"class":177},[164,882,883],{"class":270}," \"would write\"",[164,885,886],{"class":177}," if",[164,888,889],{"class":188}," dry_run ",[164,891,892],{"class":177},"else",[164,894,895],{"class":270}," \"wrote\"\n",[164,897,899,901,903,905,907,909,912,914,917,919,921,924,926,929,931,934,936,939,941,944,946,948],{"class":166,"line":898},63,[164,900,669],{"class":181},[164,902,617],{"class":188},[164,904,674],{"class":177},[164,906,695],{"class":270},[164,908,680],{"class":181},[164,910,911],{"class":188},"action",[164,913,692],{"class":181},[164,915,916],{"class":181}," {len",[164,918,617],{"class":188},[164,920,365],{"class":181},[164,922,923],{"class":188},")",[164,925,692],{"class":181},[164,927,928],{"class":270}," notes to ",[164,930,680],{"class":181},[164,932,933],{"class":188},"dest",[164,935,692],{"class":181},[164,937,938],{"class":270}," as ",[164,940,680],{"class":181},[164,942,943],{"class":188},"fmt",[164,945,692],{"class":181},[164,947,695],{"class":270},[164,949,354],{"class":188},[164,951,953],{"class":166,"line":952},64,[164,954,196],{"emptyLinePlaceholder":195},[164,956,958],{"class":166,"line":957},65,[164,959,196],{"emptyLinePlaceholder":195},[164,961,963,966,968,971,973,975,977,979,982,984,987],{"class":166,"line":962},66,[164,964,965],{"class":188},"tags ",[164,967,255],{"class":177},[164,969,970],{"class":188}," App(",[164,972,449],{"class":264},[164,974,255],{"class":177},[164,976,639],{"class":270},[164,978,317],{"class":188},[164,980,981],{"class":264},"help",[164,983,255],{"class":177},[164,985,986],{"class":270},"\"Inspect tags.\"",[164,988,354],{"class":188},[164,990,992],{"class":166,"line":991},67,[164,993,994],{"class":188},"app.command(tags)\n",[164,996,998],{"class":166,"line":997},68,[164,999,196],{"emptyLinePlaceholder":195},[164,1001,1003],{"class":166,"line":1002},69,[164,1004,196],{"emptyLinePlaceholder":195},[164,1006,1008,1011,1013,1015,1017,1020],{"class":166,"line":1007},70,[164,1009,1010],{"class":395},"@tags.command",[164,1012,617],{"class":188},[164,1014,449],{"class":264},[164,1016,255],{"class":177},[164,1018,1019],{"class":270},"\"list\"",[164,1021,354],{"class":188},[164,1023,1025,1027,1030,1033,1035],{"class":166,"line":1024},71,[164,1026,402],{"class":177},[164,1028,1029],{"class":395}," list_tags",[164,1031,1032],{"class":188},"() -> list[",[164,1034,417],{"class":181},[164,1036,1037],{"class":188},"]:\n",[164,1039,1041],{"class":166,"line":1040},72,[164,1042,1043],{"class":270},"    \"\"\"List every tag in use.\"\"\"\n",[164,1045,1047,1050,1052,1055,1058,1061,1064,1067,1070,1073,1076,1078,1081,1083],{"class":166,"line":1046},73,[164,1048,1049],{"class":188},"    names ",[164,1051,255],{"class":177},[164,1053,1054],{"class":181}," sorted",[164,1056,1057],{"class":188},"({t ",[164,1059,1060],{"class":177},"for",[164,1062,1063],{"class":188}," note ",[164,1065,1066],{"class":177},"in",[164,1068,1069],{"class":181}," STORE",[164,1071,1072],{"class":177}," for",[164,1074,1075],{"class":188}," t ",[164,1077,1066],{"class":177},[164,1079,1080],{"class":188}," note[",[164,1082,639],{"class":270},[164,1084,1085],{"class":188},"]})\n",[164,1087,1089,1092,1095,1097],{"class":166,"line":1088},74,[164,1090,1091],{"class":177},"    for",[164,1093,1094],{"class":188}," name ",[164,1096,1066],{"class":177},[164,1098,1099],{"class":188}," names:\n",[164,1101,1103,1106],{"class":166,"line":1102},75,[164,1104,1105],{"class":181},"        print",[164,1107,1108],{"class":188},"(name)\n",[164,1110,1112,1114],{"class":166,"line":1111},76,[164,1113,703],{"class":177},[164,1115,1116],{"class":188}," names\n",[164,1118,1120],{"class":166,"line":1119},77,[164,1121,196],{"emptyLinePlaceholder":195},[164,1123,1125],{"class":166,"line":1124},78,[164,1126,196],{"emptyLinePlaceholder":195},[164,1128,1130,1133,1136,1139,1142],{"class":166,"line":1129},79,[164,1131,1132],{"class":177},"if",[164,1134,1135],{"class":181}," __name__",[164,1137,1138],{"class":177}," ==",[164,1140,1141],{"class":270}," \"__main__\"",[164,1143,529],{"class":188},[164,1145,1147,1150,1153],{"class":166,"line":1146},80,[164,1148,1149],{"class":177},"    raise",[164,1151,1152],{"class":181}," SystemExit",[164,1154,1155],{"class":188},"(app())\n",[10,1157,1158,1159,1162,1163,1165,1166,1169,1170,1173,1174,1177,1178,1181,1182,1185,1186,1189,1190,1194,1195,1198,1199,1202,1203,1206,1207,54],{},"A few things are worth noticing. ",[14,1160,1161],{},"list[str] | None"," with a ",[14,1164,808],{}," default gives a repeatable ",[14,1167,1168],{},"--tag"," option that is optional; Cyclopts also generates an ",[14,1171,1172],{},"--empty-tag"," form for explicitly passing an empty list. The range check runs before ",[14,1175,1176],{},"add"," is called, so the function body never sees ",[14,1179,1180],{},"priority=9",". The ",[14,1183,1184],{},"tags"," sub-app is just another ",[14,1187,1188],{},"App"," registered as a command, which is how Cyclopts builds nested groups — compare ",[23,1191,1193],{"href":1192},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click\u002F","building a CLI with subcommands in Click",". And ",[14,1196,1197],{},"config.Env(\"NOTES_\", command=False)"," reads defaults from ",[14,1200,1201],{},"NOTES_\u003CPARAMETER>","; with the default ",[14,1204,1205],{},"command=True"," the variable name also includes the command, as in ",[14,1208,1209],{},"NOTES_ADD_PRIORITY",[71,1211],{"name":1212},"cyclopts-terminal",[1214,1215,1217],"h3",{"id":1216},"return-values-and-exit-codes","Return values and exit codes",[10,1219,1220,1221,1225,1226,1229,1230,1233,1234,1237],{},"Cyclopts passes each command's return value to a ",[1222,1223,1224],"em",{},"result action",". The default prints any non-integer return value and treats an integer as the exit code — convenient for tiny scripts, but it means a command that prints its own output ",[1222,1227,1228],{},"and"," returns data shows everything twice. The recipe sets ",[14,1231,1232],{},"result_action=\"return_int_as_exit_code_else_zero\""," on the app instead: commands print what users should see, return data for callers and tests, and only an integer return becomes an exit status. Tests then override it per call with ",[14,1235,1236],{},"result_action=\"return_value\""," to assert on the returned object instead of parsing stdout.",[29,1239,1241],{"id":1240},"grouping-parameters-with-a-dataclass","Grouping parameters with a dataclass",[10,1243,1244],{},"Commands that share a set of options — connection settings, output settings — can declare them once as a dataclass and accept it as a single parameter. Cyclopts flattens the dataclass fields into options, so users see ordinary flags while the function receives one typed object:",[155,1246,1248],{"className":157,"code":1247,"language":159,"meta":160,"style":160},"from dataclasses import dataclass\n\nfrom cyclopts import Parameter\n\n\n@Parameter(name=\"*\")            # flatten: --host, not --conn.host\n@dataclass\nclass Connection:\n    host: str = \"localhost\"\n    \"\"\"Server to connect to.\"\"\"\n    port: int = 5432\n    \"\"\"TCP port.\"\"\"\n\n\n@app.command\ndef ping(*, conn: Connection | None = None) -> str:\n    \"\"\"Check that the server answers.\"\"\"\n    conn = conn or Connection()\n    return f\"{conn.host}:{conn.port}\"\n",[14,1249,1250,1262,1266,1277,1281,1285,1305,1310,1320,1332,1337,1349,1354,1358,1362,1366,1394,1399,1414],{"__ignoreMap":160},[164,1251,1252,1254,1257,1259],{"class":166,"line":167},[164,1253,178],{"class":177},[164,1255,1256],{"class":188}," dataclasses ",[164,1258,207],{"class":177},[164,1260,1261],{"class":188}," dataclass\n",[164,1263,1264],{"class":166,"line":174},[164,1265,196],{"emptyLinePlaceholder":195},[164,1267,1268,1270,1272,1274],{"class":166,"line":192},[164,1269,178],{"class":177},[164,1271,236],{"class":188},[164,1273,207],{"class":177},[164,1275,1276],{"class":188}," Parameter\n",[164,1278,1279],{"class":166,"line":199},[164,1280,196],{"emptyLinePlaceholder":195},[164,1282,1283],{"class":166,"line":213},[164,1284,196],{"emptyLinePlaceholder":195},[164,1286,1287,1290,1292,1294,1296,1299,1302],{"class":166,"line":226},[164,1288,1289],{"class":395},"@Parameter",[164,1291,617],{"class":188},[164,1293,449],{"class":264},[164,1295,255],{"class":177},[164,1297,1298],{"class":270},"\"*\"",[164,1300,1301],{"class":188},")            ",[164,1303,1304],{"class":170},"# flatten: --host, not --conn.host\n",[164,1306,1307],{"class":166,"line":231},[164,1308,1309],{"class":395},"@dataclass\n",[164,1311,1312,1315,1318],{"class":166,"line":244},[164,1313,1314],{"class":177},"class",[164,1316,1317],{"class":395}," Connection",[164,1319,529],{"class":188},[164,1321,1322,1325,1327,1329],{"class":166,"line":249},[164,1323,1324],{"class":188},"    host: ",[164,1326,417],{"class":181},[164,1328,795],{"class":177},[164,1330,1331],{"class":270}," \"localhost\"\n",[164,1333,1334],{"class":166,"line":261},[164,1335,1336],{"class":270},"    \"\"\"Server to connect to.\"\"\"\n",[164,1338,1339,1342,1344,1346],{"class":166,"line":277},[164,1340,1341],{"class":188},"    port: ",[164,1343,480],{"class":181},[164,1345,795],{"class":177},[164,1347,1348],{"class":181}," 5432\n",[164,1350,1351],{"class":166,"line":290},[164,1352,1353],{"class":270},"    \"\"\"TCP port.\"\"\"\n",[164,1355,1356],{"class":166,"line":303},[164,1357,196],{"emptyLinePlaceholder":195},[164,1359,1360],{"class":166,"line":334},[164,1361,196],{"emptyLinePlaceholder":195},[164,1363,1364],{"class":166,"line":351},[164,1365,396],{"class":395},[164,1367,1368,1370,1373,1375,1377,1380,1382,1384,1386,1388,1390,1392],{"class":166,"line":357},[164,1369,402],{"class":177},[164,1371,1372],{"class":395}," ping",[164,1374,617],{"class":188},[164,1376,100],{"class":177},[164,1378,1379],{"class":188},", conn: Connection ",[164,1381,440],{"class":177},[164,1383,443],{"class":181},[164,1385,795],{"class":177},[164,1387,443],{"class":181},[164,1389,524],{"class":188},[164,1391,417],{"class":181},[164,1393,529],{"class":188},[164,1395,1396],{"class":166,"line":362},[164,1397,1398],{"class":270},"    \"\"\"Check that the server answers.\"\"\"\n",[164,1400,1401,1404,1406,1409,1411],{"class":166,"line":382},[164,1402,1403],{"class":188},"    conn ",[164,1405,255],{"class":177},[164,1407,1408],{"class":188}," conn ",[164,1410,645],{"class":177},[164,1412,1413],{"class":188}," Connection()\n",[164,1415,1416,1418,1421,1423,1425,1428,1430,1433,1435,1438,1440],{"class":166,"line":387},[164,1417,703],{"class":177},[164,1419,1420],{"class":177}," f",[164,1422,695],{"class":270},[164,1424,680],{"class":181},[164,1426,1427],{"class":188},"conn.host",[164,1429,692],{"class":181},[164,1431,1432],{"class":270},":",[164,1434,680],{"class":181},[164,1436,1437],{"class":188},"conn.port",[164,1439,692],{"class":181},[164,1441,1442],{"class":270},"\"\n",[10,1444,1445,1448,1449,1452,1453,1457],{},[14,1446,1447],{},"notes ping --host db.internal --port 6543"," now fills the dataclass, and the same ",[14,1450,1451],{},"Connection"," can be reused by every command that talks to the server. It is the Cyclopts counterpart of the reusable option decorators described in ",[23,1454,1456],{"href":1455},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands\u002F","sharing common options across commands",", with the advantage that the grouped values travel together as one validated object. Field docstrings become the help text for each flag.",[29,1459,1461],{"id":1460},"ux-considerations","UX considerations",[34,1463,1464,1478,1491,1499,1505],{},[37,1465,1466,1469,1470,1472,1473,1477],{},[80,1467,1468],{},"Keyword-only by default."," Putting most parameters after ",[14,1471,100],{}," makes them options, which is easier to read in scripts and safer to extend: adding a new positional argument later is a breaking change, adding an option is not. ",[23,1474,1476],{"href":1475},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options\u002F","Positional arguments vs options"," covers the reasoning.",[37,1479,1480,1483,1484,1486,1487,1490],{},[80,1481,1482],{},"Write the docstring once."," Help text lives in one place and stays next to the code it describes; resist adding ",[14,1485,20],{}," to ",[14,1488,1489],{},"Parameter"," as well.",[37,1492,1493,1498],{},[80,1494,1495,1496,54],{},"Name choices with ",[14,1497,16],{}," It shows the allowed values in help and in errors, and type checkers see the same constraint as the parser.",[37,1500,1501,1504],{},[80,1502,1503],{},"Usage errors exit with 2."," Cyclopts prints a Rich error panel to stderr and exits with code 2 on a bad value, matching Click and argparse, so wrapper scripts can tell a typo from a runtime failure.",[37,1506,1507,1510,1511,1514,1515,1518,1519,1522,1523,1526],{},[80,1508,1509],{},"Scope environment defaults deliberately."," With ",[14,1512,1513],{},"config.Env"," on the app, help shows an ",[14,1516,1517],{},"[env var: NOTES_…]"," hint next to ",[1222,1520,1521],{},"every"," parameter — including the required ",[14,1524,1525],{},"TEXT",", which can now come from the environment too. If that is more than you want, attach the config to fewer parameters or use a narrower source.",[71,1528],{"name":1529},"cyclopts-vs-typer",[29,1531,1533],{"id":1532},"common-pitfalls","Common pitfalls",[34,1535,1536,1545,1558,1570],{},[37,1537,1538,1541,1542,1544],{},[80,1539,1540],{},"Positional by accident."," A parameter before ",[14,1543,100],{}," can be passed positionally, so reordering the function signature changes the command line. Make the boundary explicit and keep only true arguments before it.",[37,1546,1547,1550,1551,1554,1555,1557],{},[80,1548,1549],{},"Mutable defaults."," As in any Python function, ",[14,1552,1553],{},"tag: list[str] = []"," shares one list between calls. Use ",[14,1556,808],{}," and normalise inside the function, as the recipe does — it matters in tests that call the app repeatedly.",[37,1559,1560,1563,1564,1566,1567,1569],{},[80,1561,1562],{},"Return values printed twice."," Under the default result action, a command that prints its result ",[1222,1565,1228],{}," returns it shows the output twice. Set an app-wide result action as the recipe does, or return ",[14,1568,808],{}," from commands that print.",[37,1571,1572,1575,1576,1579],{},[80,1573,1574],{},"Over-broad config sources."," An ",[14,1577,1578],{},"Env"," config on the whole app can satisfy required arguments from stray environment variables in CI. Scope it to the options that should be configurable.",[29,1581,1583],{"id":1582},"testing-the-behaviour","Testing the behaviour",[10,1585,1586,1587,1589],{},"A Cyclopts ",[14,1588,1188],{}," is callable with a list of tokens. In tests, turn off the process exit and ask for the return value:",[155,1591,1593],{"className":157,"code":1592,"language":159,"meta":160,"style":160},"# tests\u002Ftest_cli.py\nimport pytest\nfrom cyclopts.exceptions import CycloptsError\n\nfrom notes import cli\n\n\n@pytest.fixture(autouse=True)\ndef empty_store():\n    cli.STORE.clear()\n\n\ndef run(*tokens):\n    return cli.app(list(tokens), result_action=\"return_value\",\n                   exit_on_error=False, print_error=False)\n\n\ndef test_add_converts_and_collects_tags():\n    note = run(\"add\", \"buy milk\", \"-t\", \"home\", \"-t\", \"errand\", \"--priority\", \"5\")\n    assert note == {\"id\": 1, \"text\": \"buy milk\", \"tags\": [\"home\", \"errand\"], \"priority\": 5}\n\n\ndef test_priority_out_of_range_is_rejected():\n    with pytest.raises(CycloptsError):\n        run(\"add\", \"x\", \"--priority\", \"9\")\n\n\ndef test_literal_choices_are_enforced():\n    with pytest.raises(CycloptsError):\n        run(\"export\", \"out.json\", \"--fmt\", \"xml\")\n\n\ndef test_env_var_supplies_default(monkeypatch):\n    monkeypatch.setenv(\"NOTES_PRIORITY\", \"4\")\n    assert run(\"add\", \"x\")[\"priority\"] == 4\n\n\ndef test_sub_app_command(capsys):\n    run(\"add\", \"a\", \"-t\", \"b\")\n    run(\"add\", \"c\", \"-t\", \"a\")\n    assert run(\"tags\", \"list\") == [\"a\", \"b\"]\n    assert capsys.readouterr().out.endswith(\"a\\nb\\n\")\n\n\ndef test_usage_error_exits_with_2():\n    with pytest.raises(SystemExit) as exc:\n        cli.app([\"add\"], print_error=False)\n    assert exc.value.code == 2\n",[14,1594,1595,1600,1607,1619,1623,1635,1639,1643,1660,1670,1680,1684,1688,1702,1725,1745,1749,1753,1762,1809,1860,1864,1868,1877,1885,1908,1912,1916,1925,1931,1955,1959,1963,1973,1988,2012,2016,2020,2030,2053,2074,2102,2124,2128,2132,2141,2159,2176],{"__ignoreMap":160},[164,1596,1597],{"class":166,"line":167},[164,1598,1599],{"class":170},"# tests\u002Ftest_cli.py\n",[164,1601,1602,1604],{"class":166,"line":174},[164,1603,207],{"class":177},[164,1605,1606],{"class":188}," pytest\n",[164,1608,1609,1611,1614,1616],{"class":166,"line":192},[164,1610,178],{"class":177},[164,1612,1613],{"class":188}," cyclopts.exceptions ",[164,1615,207],{"class":177},[164,1617,1618],{"class":188}," CycloptsError\n",[164,1620,1621],{"class":166,"line":199},[164,1622,196],{"emptyLinePlaceholder":195},[164,1624,1625,1627,1630,1632],{"class":166,"line":213},[164,1626,178],{"class":177},[164,1628,1629],{"class":188}," notes ",[164,1631,207],{"class":177},[164,1633,1634],{"class":188}," cli\n",[164,1636,1637],{"class":166,"line":226},[164,1638,196],{"emptyLinePlaceholder":195},[164,1640,1641],{"class":166,"line":231},[164,1642,196],{"emptyLinePlaceholder":195},[164,1644,1645,1648,1650,1653,1655,1658],{"class":166,"line":244},[164,1646,1647],{"class":395},"@pytest.fixture",[164,1649,617],{"class":188},[164,1651,1652],{"class":264},"autouse",[164,1654,255],{"class":177},[164,1656,1657],{"class":181},"True",[164,1659,354],{"class":188},[164,1661,1662,1664,1667],{"class":166,"line":249},[164,1663,402],{"class":177},[164,1665,1666],{"class":395}," empty_store",[164,1668,1669],{"class":188},"():\n",[164,1671,1672,1675,1677],{"class":166,"line":261},[164,1673,1674],{"class":188},"    cli.",[164,1676,365],{"class":181},[164,1678,1679],{"class":188},".clear()\n",[164,1681,1682],{"class":166,"line":277},[164,1683,196],{"emptyLinePlaceholder":195},[164,1685,1686],{"class":166,"line":290},[164,1687,196],{"emptyLinePlaceholder":195},[164,1689,1690,1692,1695,1697,1699],{"class":166,"line":303},[164,1691,402],{"class":177},[164,1693,1694],{"class":395}," run",[164,1696,617],{"class":188},[164,1698,100],{"class":177},[164,1700,1701],{"class":188},"tokens):\n",[164,1703,1704,1706,1709,1712,1715,1718,1720,1723],{"class":166,"line":334},[164,1705,703],{"class":177},[164,1707,1708],{"class":188}," cli.app(",[164,1710,1711],{"class":181},"list",[164,1713,1714],{"class":188},"(tokens), ",[164,1716,1717],{"class":264},"result_action",[164,1719,255],{"class":177},[164,1721,1722],{"class":270},"\"return_value\"",[164,1724,274],{"class":188},[164,1726,1727,1730,1732,1734,1736,1739,1741,1743],{"class":166,"line":351},[164,1728,1729],{"class":264},"                   exit_on_error",[164,1731,255],{"class":177},[164,1733,325],{"class":181},[164,1735,317],{"class":188},[164,1737,1738],{"class":264},"print_error",[164,1740,255],{"class":177},[164,1742,325],{"class":181},[164,1744,354],{"class":188},[164,1746,1747],{"class":166,"line":357},[164,1748,196],{"emptyLinePlaceholder":195},[164,1750,1751],{"class":166,"line":362},[164,1752,196],{"emptyLinePlaceholder":195},[164,1754,1755,1757,1760],{"class":166,"line":382},[164,1756,402],{"class":177},[164,1758,1759],{"class":395}," test_add_converts_and_collects_tags",[164,1761,1669],{"class":188},[164,1763,1764,1766,1768,1771,1774,1776,1779,1781,1783,1785,1788,1790,1792,1794,1797,1799,1802,1804,1807],{"class":166,"line":387},[164,1765,600],{"class":188},[164,1767,255],{"class":177},[164,1769,1770],{"class":188}," run(",[164,1772,1773],{"class":270},"\"add\"",[164,1775,317],{"class":188},[164,1777,1778],{"class":270},"\"buy milk\"",[164,1780,317],{"class":188},[164,1782,462],{"class":270},[164,1784,317],{"class":188},[164,1786,1787],{"class":270},"\"home\"",[164,1789,317],{"class":188},[164,1791,462],{"class":270},[164,1793,317],{"class":188},[164,1795,1796],{"class":270},"\"errand\"",[164,1798,317],{"class":188},[164,1800,1801],{"class":270},"\"--priority\"",[164,1803,317],{"class":188},[164,1805,1806],{"class":270},"\"5\"",[164,1808,354],{"class":188},[164,1810,1811,1814,1816,1819,1821,1823,1825,1827,1829,1831,1833,1835,1837,1839,1842,1844,1846,1848,1851,1853,1855,1857],{"class":166,"line":392},[164,1812,1813],{"class":177},"    assert",[164,1815,1063],{"class":188},[164,1817,1818],{"class":177},"==",[164,1820,605],{"class":188},[164,1822,608],{"class":270},[164,1824,611],{"class":188},[164,1826,498],{"class":181},[164,1828,317],{"class":188},[164,1830,633],{"class":270},[164,1832,611],{"class":188},[164,1834,1778],{"class":270},[164,1836,317],{"class":188},[164,1838,639],{"class":270},[164,1840,1841],{"class":188},": [",[164,1843,1787],{"class":270},[164,1845,317],{"class":188},[164,1847,1796],{"class":270},[164,1849,1850],{"class":188},"], ",[164,1852,651],{"class":270},[164,1854,611],{"class":188},[164,1856,508],{"class":181},[164,1858,1859],{"class":188},"}\n",[164,1861,1862],{"class":166,"line":399},[164,1863,196],{"emptyLinePlaceholder":195},[164,1865,1866],{"class":166,"line":411},[164,1867,196],{"emptyLinePlaceholder":195},[164,1869,1870,1872,1875],{"class":166,"line":422},[164,1871,402],{"class":177},[164,1873,1874],{"class":395}," test_priority_out_of_range_is_rejected",[164,1876,1669],{"class":188},[164,1878,1879,1882],{"class":166,"line":430},[164,1880,1881],{"class":177},"    with",[164,1883,1884],{"class":188}," pytest.raises(CycloptsError):\n",[164,1886,1887,1890,1892,1894,1897,1899,1901,1903,1906],{"class":166,"line":474},[164,1888,1889],{"class":188},"        run(",[164,1891,1773],{"class":270},[164,1893,317],{"class":188},[164,1895,1896],{"class":270},"\"x\"",[164,1898,317],{"class":188},[164,1900,1801],{"class":270},[164,1902,317],{"class":188},[164,1904,1905],{"class":270},"\"9\"",[164,1907,354],{"class":188},[164,1909,1910],{"class":166,"line":521},[164,1911,196],{"emptyLinePlaceholder":195},[164,1913,1914],{"class":166,"line":532},[164,1915,196],{"emptyLinePlaceholder":195},[164,1917,1918,1920,1923],{"class":166,"line":538},[164,1919,402],{"class":177},[164,1921,1922],{"class":395}," test_literal_choices_are_enforced",[164,1924,1669],{"class":188},[164,1926,1927,1929],{"class":166,"line":543},[164,1928,1881],{"class":177},[164,1930,1884],{"class":188},[164,1932,1933,1935,1938,1940,1943,1945,1948,1950,1953],{"class":166,"line":549},[164,1934,1889],{"class":188},[164,1936,1937],{"class":270},"\"export\"",[164,1939,317],{"class":188},[164,1941,1942],{"class":270},"\"out.json\"",[164,1944,317],{"class":188},[164,1946,1947],{"class":270},"\"--fmt\"",[164,1949,317],{"class":188},[164,1951,1952],{"class":270},"\"xml\"",[164,1954,354],{"class":188},[164,1956,1957],{"class":166,"line":555},[164,1958,196],{"emptyLinePlaceholder":195},[164,1960,1961],{"class":166,"line":561},[164,1962,196],{"emptyLinePlaceholder":195},[164,1964,1965,1967,1970],{"class":166,"line":567},[164,1966,402],{"class":177},[164,1968,1969],{"class":395}," test_env_var_supplies_default",[164,1971,1972],{"class":188},"(monkeypatch):\n",[164,1974,1975,1978,1981,1983,1986],{"class":166,"line":573},[164,1976,1977],{"class":188},"    monkeypatch.setenv(",[164,1979,1980],{"class":270},"\"NOTES_PRIORITY\"",[164,1982,317],{"class":188},[164,1984,1985],{"class":270},"\"4\"",[164,1987,354],{"class":188},[164,1989,1990,1992,1994,1996,1998,2000,2003,2005,2007,2009],{"class":166,"line":579},[164,1991,1813],{"class":177},[164,1993,1770],{"class":188},[164,1995,1773],{"class":270},[164,1997,317],{"class":188},[164,1999,1896],{"class":270},[164,2001,2002],{"class":188},")[",[164,2004,651],{"class":270},[164,2006,374],{"class":188},[164,2008,1818],{"class":177},[164,2010,2011],{"class":181}," 4\n",[164,2013,2014],{"class":166,"line":585},[164,2015,196],{"emptyLinePlaceholder":195},[164,2017,2018],{"class":166,"line":591},[164,2019,196],{"emptyLinePlaceholder":195},[164,2021,2022,2024,2027],{"class":166,"line":597},[164,2023,402],{"class":177},[164,2025,2026],{"class":395}," test_sub_app_command",[164,2028,2029],{"class":188},"(capsys):\n",[164,2031,2032,2035,2037,2039,2042,2044,2046,2048,2051],{"class":166,"line":657},[164,2033,2034],{"class":188},"    run(",[164,2036,1773],{"class":270},[164,2038,317],{"class":188},[164,2040,2041],{"class":270},"\"a\"",[164,2043,317],{"class":188},[164,2045,462],{"class":270},[164,2047,317],{"class":188},[164,2049,2050],{"class":270},"\"b\"",[164,2052,354],{"class":188},[164,2054,2055,2057,2059,2061,2064,2066,2068,2070,2072],{"class":166,"line":666},[164,2056,2034],{"class":188},[164,2058,1773],{"class":270},[164,2060,317],{"class":188},[164,2062,2063],{"class":270},"\"c\"",[164,2065,317],{"class":188},[164,2067,462],{"class":270},[164,2069,317],{"class":188},[164,2071,2041],{"class":270},[164,2073,354],{"class":188},[164,2075,2076,2078,2080,2082,2084,2086,2088,2090,2093,2095,2097,2099],{"class":166,"line":700},[164,2077,1813],{"class":177},[164,2079,1770],{"class":188},[164,2081,639],{"class":270},[164,2083,317],{"class":188},[164,2085,1019],{"class":270},[164,2087,622],{"class":188},[164,2089,1818],{"class":177},[164,2091,2092],{"class":188}," [",[164,2094,2041],{"class":270},[164,2096,317],{"class":188},[164,2098,2050],{"class":270},[164,2100,2101],{"class":188},"]\n",[164,2103,2104,2106,2109,2112,2115,2118,2120,2122],{"class":166,"line":709},[164,2105,1813],{"class":177},[164,2107,2108],{"class":188}," capsys.readouterr().out.endswith(",[164,2110,2111],{"class":270},"\"a",[164,2113,2114],{"class":181},"\\n",[164,2116,2117],{"class":270},"b",[164,2119,2114],{"class":181},[164,2121,695],{"class":270},[164,2123,354],{"class":188},[164,2125,2126],{"class":166,"line":714},[164,2127,196],{"emptyLinePlaceholder":195},[164,2129,2130],{"class":166,"line":719},[164,2131,196],{"emptyLinePlaceholder":195},[164,2133,2134,2136,2139],{"class":166,"line":724},[164,2135,402],{"class":177},[164,2137,2138],{"class":395}," test_usage_error_exits_with_2",[164,2140,1669],{"class":188},[164,2142,2143,2145,2148,2151,2153,2156],{"class":166,"line":734},[164,2144,1881],{"class":177},[164,2146,2147],{"class":188}," pytest.raises(",[164,2149,2150],{"class":181},"SystemExit",[164,2152,622],{"class":188},[164,2154,2155],{"class":177},"as",[164,2157,2158],{"class":188}," exc:\n",[164,2160,2161,2164,2166,2168,2170,2172,2174],{"class":166,"line":757},[164,2162,2163],{"class":188},"        cli.app([",[164,2165,1773],{"class":270},[164,2167,1850],{"class":188},[164,2169,1738],{"class":264},[164,2171,255],{"class":177},[164,2173,325],{"class":181},[164,2175,354],{"class":188},[164,2177,2178,2180,2183,2185],{"class":166,"line":764},[164,2179,1813],{"class":177},[164,2181,2182],{"class":188}," exc.value.code ",[164,2184,1818],{"class":177},[164,2186,2187],{"class":181}," 2\n",[10,2189,2190,2191,2195],{},"The last test runs with the normal exit behaviour, pinning the exit code users and scripts see. For an end-to-end check of the installed command, the approach in ",[23,2192,2194],{"href":2193},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli\u002F","end-to-end testing an installed CLI"," works unchanged.",[29,2197,2199],{"id":2198},"conclusion","Conclusion",[10,2201,2202,2203,2205],{},"Cyclopts keeps the best part of Typer — the function signature is the interface — and adds richer types, docstring-driven help and built-in configuration sources. Write commands as plain functions with keyword-only options, let validators reject bad input before your code runs, register sub-apps for nesting, and test by calling the app with tokens and ",[14,2204,1236],{},". If you need Click's plugin ecosystem, Typer is still the better fit; otherwise Cyclopts is a strong default for new type-hinted tools.",[29,2207,2209],{"id":2208},"frequently-asked-questions","Frequently asked questions",[1214,2211,2213,2214,2217],{"id":2212},"how-do-i-add-global-options-such-as-verbose-to-every-command","How do I add global options such as ",[14,2215,2216],{},"--verbose"," to every command?",[10,2219,2220,2221,2224,2225,2228,2229,2232,2233,2236,2237,2240,2241,2244],{},"Use the ",[1222,2222,2223],{},"meta app",": decorate a function with ",[14,2226,2227],{},"@app.meta.default"," that takes the global options plus ",[14,2230,2231],{},"*tokens",", configures logging, and then calls ",[14,2234,2235],{},"app(tokens)",". Run the program through ",[14,2238,2239],{},"app.meta()"," instead of ",[14,2242,2243],{},"app()",". It plays the role of a Typer callback or a Click group function.",[1214,2246,2248],{"id":2247},"can-cyclopts-read-a-toml-config-file","Can Cyclopts read a TOML config file?",[10,2250,2251,2252,2255,2256,54],{},"Yes. ",[14,2253,2254],{},"config.Toml(\"pyproject.toml\", root_keys=[\"tool\", \"notes\"])"," or a dedicated file adds a config layer, and several sources can be combined in a list. Keep the precedence order explicit and documented, as in ",[23,2257,2259],{"href":2258},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence: flags, env, files, defaults",[1214,2261,2263],{"id":2262},"does-cyclopts-support-async-commands","Does Cyclopts support async commands?",[10,2265,2266,2267,2270,2271,2275],{},"Yes — decorate an ",[14,2268,2269],{},"async def"," function and Cyclopts runs it in an event loop (asyncio by default, Trio optionally). For cancellation and Ctrl-C behaviour, the patterns in ",[23,2272,2274],{"href":2273},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click\u002F","running async code in Typer and Click"," still apply.",[1214,2277,2279],{"id":2278},"how-hard-is-it-to-migrate-from-typer","How hard is it to migrate from Typer?",[10,2281,2282,2283,2286,2287,2290,2291,2294],{},"Usually mechanical: replace ",[14,2284,2285],{},"typer.Option(...)"," metadata with ",[14,2288,2289],{},"Parameter(...)",", move help strings into docstrings, and turn ",[14,2292,2293],{},"@app.callback()"," logic into a meta app. Keep business logic outside the command functions and the migration touches only the outer layer.",[29,2296,2298],{"id":2297},"related","Related",[34,2300,2301,2307,2313,2319,2324],{},[37,2302,2303,2304],{},"Up: ",[23,2305,2306],{"href":25},"Alternative Python CLI frameworks",[37,2308,2309],{},[23,2310,2312],{"href":2311},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire\u002F","Quick CLIs from functions with Python Fire",[37,2314,2315],{},[23,2316,2318],{"href":2317},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo\u002F","Building a CLI with Cleo",[37,2320,2321],{},[23,2322,2323],{"href":60},"Using Annotated options in Typer",[37,2325,2326],{},[23,2327,2329],{"href":2328},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison\u002F","argparse vs Click vs Typer comparison",[2331,2332,2333],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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":160,"searchDepth":174,"depth":174,"links":2335},[2336,2337,2338,2341,2342,2343,2344,2345,2346,2353],{"id":31,"depth":174,"text":32},{"id":65,"depth":174,"text":66},{"id":152,"depth":174,"text":153,"children":2339},[2340],{"id":1216,"depth":192,"text":1217},{"id":1240,"depth":174,"text":1241},{"id":1460,"depth":174,"text":1461},{"id":1532,"depth":174,"text":1533},{"id":1582,"depth":174,"text":1583},{"id":2198,"depth":174,"text":2199},{"id":2208,"depth":174,"text":2209,"children":2347},[2348,2350,2351,2352],{"id":2212,"depth":192,"text":2349},"How do I add global options such as --verbose to every command?",{"id":2247,"depth":192,"text":2248},{"id":2262,"depth":192,"text":2263},{"id":2278,"depth":192,"text":2279},{"id":2297,"depth":174,"text":2298},"2026-10-02","Build a multi-command CLI with Cyclopts: signatures as the interface, docstring help, Literal and Path types, validators, env config, sub-apps and tests.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts",{"title":5,"description":2355},"modern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts\u002Findex",[42,2364,2365,2366],"type-hints","frameworks","testing","I6zVGPV7tmwXRdfmbM3UDxyTQvhN-6VXjMVXS4992HI",[2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853,2856,2859,2862,2865,2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2973,2976,2979,2982,2985,2988,2991,2994,2997,3000,3003,3006,3009,3012,3015,3018,3021,3024,3027,3030,3033,3036,3039,3042,3045,3048,3051,3054,3057,3060,3063,3066,3069,3072,3075,3078,3081,3084,3087,3090,3093,3096,3099,3102,3105,3108,3111,3114,3117,3120,3123,3126,3129,3132,3135,3138,3141,3144,3147,3150,3153,3156,3159,3162,3165,3168,3171,3174,3177,3180,3183,3186,3189,3192,3195,3198,3201,3204,3207,3210],{"path":2370,"title":2371},"\u002Fabout","About Python CLI Toolcraft",{"path":2373,"title":2374},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2376,"title":2377},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2379,"title":2380},"\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":2382,"title":2383},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2385,"title":2386},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2388,"title":2389},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2391,"title":2392},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2394,"title":2395},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2397,"title":2398},"\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":2400,"title":2401},"\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":2403,"title":2404},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2406,"title":2407},"\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":2409,"title":2410},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2412,"title":2413},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2415,"title":2416},"\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":2418,"title":2419},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2421,"title":2422},"\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":2424,"title":2425},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2427,"title":2428},"\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":2430,"title":2431},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2433,"title":2434},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2436,"title":2437},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2439,"title":2440},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2442,"title":2443},"\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":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2448,"title":2449},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2451,"title":2452},"\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":2454,"title":2455},"\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":2457,"title":2458},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2460,"title":2461},"\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":2463,"title":2464},"\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":2466,"title":2467},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2469,"title":2470},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2472,"title":2473},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2475,"title":2476},"\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":2478,"title":2479},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2481,"title":2482},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2484,"title":2485},"\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":2487,"title":2488},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2490,"title":2491},"\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":2493,"title":2494},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2496,"title":2497},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2499,"title":2500},"\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":2502,"title":2503},"\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":2505,"title":2506},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2508,"title":2509},"\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":2511,"title":2512},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2514,"title":2515},"\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":2517,"title":2518},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2520,"title":2521},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2523,"title":2524},"\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":2526,"title":2527},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2529,"title":2530},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2532,"title":2533},"\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":2535,"title":2536},"\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":2538,"title":2539},"\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":2541,"title":2542},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2544,"title":2545},"\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":2547,"title":2548},"\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":2550,"title":2551},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2553,"title":2554},"\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":2556,"title":2557},"\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":2559,"title":2560},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2562,"title":2563},"\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":2565,"title":2566},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2568,"title":2569},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2571,"title":2572},"\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":2574,"title":2575},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2577,"title":2578},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2580,"title":2581},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2583,"title":2584},"\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":2586,"title":2587},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2589,"title":2590},"\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":2592,"title":2593},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2595,"title":2596},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2598,"title":2599},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2601,"title":2602},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2604,"title":2605},"\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":2607,"title":2608},"\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":2610,"title":2611},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2613,"title":2614},"\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":2616,"title":2617},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2619,"title":2620},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2622,"title":2623},"\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":2625,"title":2626},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2628,"title":2629},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2631,"title":2632},"\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":2634,"title":2635},"\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":2637,"title":2638},"\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":2640,"title":2641},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2643,"title":2644},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2646,"title":2647},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2649,"title":2650},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2652,"title":2653},"\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":2655,"title":2656},"\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":2658,"title":2659},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2661,"title":2662},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2664,"title":2665},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2667,"title":2668},"\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":2670,"title":2671},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2673,"title":2674},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2676,"title":2677},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2679,"title":2680},"\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":2682,"title":2683},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2685,"title":2686},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2688,"title":2689},"\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":2691,"title":2692},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2694,"title":2695},"\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":2697,"title":2698},"\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":2700,"title":2701},"\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":2703,"title":2704},"\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":2706,"title":2707},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2709,"title":2710},"\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":2712,"title":2713},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2715,"title":2716},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2718,"title":2719},"\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":2721,"title":2722},"\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":2724,"title":2725},"\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":2727,"title":2728},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2730,"title":2731},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2733,"title":2734},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2736,"title":2737},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2739,"title":2740},"\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":2742,"title":2743},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2745,"title":2746},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2748,"title":2749},"\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":2751,"title":2752},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2754,"title":2755},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2757,"title":2758},"\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":2760,"title":2761},"\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":2763,"title":2764},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2766,"title":2767},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2769,"title":2770},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2772,"title":2773},"\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":2775,"title":2776},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2778,"title":2779},"\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":2781,"title":2782},"\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":2784,"title":2785},"\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":2787,"title":2788},"\u002F","Python CLI Toolcraft",{"path":2790,"title":2791},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2360,"title":5},{"path":2794,"title":2795},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2797,"title":2798},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2800,"title":2801},"\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":2803,"title":2804},"\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":2806,"title":2807},"\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":2809,"title":2810},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2812,"title":2813},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2815,"title":2816},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2818,"title":2819},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2821,"title":2822},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2824,"title":2825},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2827,"title":2828},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2830,"title":2831},"\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":2833,"title":2834},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2836,"title":2837},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2839,"title":2840},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2842,"title":2843},"\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":2845,"title":2846},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2848,"title":2849},"\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":2851,"title":2852},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2854,"title":2855},"\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":2857,"title":2858},"\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":2860,"title":2861},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2863,"title":2864},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2866,"title":2867},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2869,"title":2870},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2872,"title":2873},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2875,"title":2876},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2878,"title":2879},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2881,"title":2882},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2884,"title":2885},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2887,"title":2888},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2890,"title":2891},"\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":2893,"title":2894},"\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":2896,"title":2897},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2899,"title":2900},"\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":2902,"title":2903},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2905,"title":2906},"\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":2908,"title":2909},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2911,"title":2912},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2914,"title":2915},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2917,"title":2918},"\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":2920,"title":2921},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2923,"title":2924},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2926,"title":2927},"\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":2929,"title":2930},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2932,"title":2933},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2935,"title":2936},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2938,"title":2939},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2941,"title":2942},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2944,"title":2945},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2947,"title":2948},"\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":2950,"title":2951},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2953,"title":2954},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2956,"title":2957},"\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":2959,"title":2960},"\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":2962,"title":2963},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2965,"title":2966},"\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":2968,"title":2969},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2971,"title":2972},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2974,"title":2975},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2977,"title":2978},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2980,"title":2981},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2983,"title":2984},"\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":2986,"title":2987},"\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":2989,"title":2990},"\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":2992,"title":2993},"\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":2995,"title":2996},"\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":2998,"title":2999},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3001,"title":3002},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3004,"title":3005},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3007,"title":3008},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3010,"title":3011},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3013,"title":3014},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3016,"title":3017},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3019,"title":3020},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3022,"title":3023},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3025,"title":3026},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3028,"title":3029},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3031,"title":3032},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3034,"title":3035},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3037,"title":3038},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3040,"title":3041},"\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":3043,"title":3044},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3046,"title":3047},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3049,"title":3050},"\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":3052,"title":3053},"\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":3055,"title":3056},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3058,"title":3059},"\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":3061,"title":3062},"\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":3064,"title":3065},"\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":3067,"title":3068},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3070,"title":3071},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3073,"title":3074},"\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":3076,"title":3077},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3079,"title":3080},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3082,"title":3083},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3085,"title":3086},"\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":3088,"title":3089},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3091,"title":3092},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3094,"title":3095},"\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":3097,"title":3098},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3100,"title":3101},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3103,"title":3104},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3106,"title":3107},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3109,"title":3110},"\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":3112,"title":3113},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3115,"title":3116},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3118,"title":3119},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3121,"title":3122},"\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":3124,"title":3125},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3127,"title":3128},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3130,"title":3131},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3133,"title":3134},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3136,"title":3137},"\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":3139,"title":3140},"\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":3142,"title":3143},"\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":3145,"title":3146},"\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":3148,"title":3149},"\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":3151,"title":3152},"\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":3154,"title":3155},"\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":3157,"title":3158},"\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":3160,"title":3161},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3163,"title":3164},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3166,"title":3167},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3169,"title":3170},"\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":3172,"title":3173},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3175,"title":3176},"\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":3178,"title":3179},"\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":3181,"title":3182},"\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":3184,"title":3185},"\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":3187,"title":3188},"\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":3190,"title":3191},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3193,"title":3194},"\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":3196,"title":3197},"\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":3199,"title":3200},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3202,"title":3203},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3205,"title":3206},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3208,"title":3209},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3211,"title":3212},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967540163]