[{"data":1,"prerenderedAt":3719},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis\u002F":3,"content-directory":2874},{"id":4,"title":5,"body":6,"date":2858,"description":2859,"difficulty":2860,"draft":2861,"extension":2862,"meta":2863,"navigation":166,"path":2864,"seo":2865,"stem":2866,"tags":2867,"updated":2858,"__hash__":2873},"content\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis\u002Findex.md","Structured Concurrency with TaskGroups in Python CLIs",{"type":7,"value":8,"toc":2836},"minimark",[9,67,72,96,100,104,121,125,953,958,979,983,997,1012,1016,1614,1636,1639,1643,1653,1657,1732,1735,1739,1742,2699,2713,2717,2731,2735,2742,2751,2755,2766,2770,2777,2781,2788,2792,2801,2805,2832],[10,11,12,13,17,18,21,22,28,29,33,34,37,38,41,42,45,46,49,50,45,53,56,57,60,61,66],"p",{},"A health-check command probes twenty hosts; a deploy command uploads forty files; a sync command fetches a hundred pages. Doing these concurrently with asyncio is easy to start — ",[14,15,16],"code",{},"asyncio.gather"," or a handful of ",[14,19,20],{},"create_task"," calls — and surprisingly easy to get wrong at the edges. What happens to the other nineteen probes when one raises? Who notices an exception in a task nobody awaited? What does the user see when three things fail at once? ",[23,24,25],"strong",{},[14,26,27],{},"asyncio.TaskGroup"," (Python 3.11+) answers these with ",[30,31,32],"em",{},"structured concurrency",": tasks live inside an ",[14,35,36],{},"async with"," block, the block does not exit until all of them have finished, a failure cancels the siblings, and every failure is reported together in an ",[14,39,40],{},"ExceptionGroup",". This guide uses TaskGroups for the two modes a CLI usually needs — ",[23,43,44],{},"fail fast"," (",[14,47,48],{},"make","-style, stop at the first error) and ",[23,51,52],{},"keep going",[14,54,55],{},"make -k",", run everything and report all failures) — and adds concurrency limits, timeouts, ",[14,58,59],{},"except*"," handling and tests. It belongs to the ",[62,63,65],"a",{"href":64},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002F","concurrency and async topic",".",[68,69,71],"h2",{"id":70},"prerequisites","Prerequisites",[73,74,75,89],"ul",{},[76,77,78,79,82,83,86,87,66],"li",{},"Python 3.11 or newer for ",[14,80,81],{},"TaskGroup",", ",[14,84,85],{},"asyncio.timeout"," and ",[14,88,59],{},[76,90,91,95],{},[62,92,94],{"href":93},"\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"," for calling async code from commands.",[68,97,99],{"id":98},"what-structure-buys","What structure buys",[101,102],"inline-diagram",{"name":103},"tg-structure",[10,105,106,107,110,111,114,115,117,118,120],{},"With unstructured tasks, a task can outlive the function that created it, and its exception surfaces — if at all — as a “Task exception was never retrieved” warning at interpreter exit. With ",[14,108,109],{},"gather",", the first exception is returned to the caller while the other tasks ",[30,112,113],{},"keep running"," in the background unless you cancel them yourself. A TaskGroup removes both problems by construction: the ",[14,116,36],{}," block is a boundary that no task crosses. When any task fails with an ordinary exception, the group cancels the others, waits for them to finish cancelling, and raises an ",[14,119,40],{}," containing every failure. Ctrl+C and outer cancellation propagate into the group the same way.",[68,122,124],{"id":123},"the-recipe","The recipe",[126,127,132],"pre",{"className":128,"code":129,"language":130,"meta":131,"style":131},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fchecks.py\nfrom __future__ import annotations\n\nimport asyncio\nfrom collections.abc import Awaitable, Callable, Iterable\nfrom dataclasses import dataclass\n\n\nclass CheckFailed(Exception):\n    \"\"\"An expected, reportable failure of one check.\"\"\"\n\n\n@dataclass\nclass Outcome:\n    name: str\n    ok: bool\n    detail: str\n\n\nasync def run_fail_fast(names: Iterable[str], check: Callable[[str], Awaitable[str]],\n                        *, limit: int = 8, timeout: float = 30) -> dict[str, str]:\n    \"\"\"All-or-nothing: the first failure cancels every other check.\"\"\"\n    gate = asyncio.Semaphore(limit)\n    results: dict[str, str] = {}\n\n    async def one(name: str) -> None:\n        async with gate:\n            results[name] = await check(name)\n\n    async with asyncio.timeout(timeout):\n        async with asyncio.TaskGroup() as tg:\n            for name in names:\n                tg.create_task(one(name), name=f\"check:{name}\")\n    return results\n\n\nasync def run_keep_going(names: Iterable[str], check: Callable[[str], Awaitable[str]],\n                         *, limit: int = 8, per_check_timeout: float = 10) -> list[Outcome]:\n    \"\"\"Best effort: every check runs; expected failures become outcomes, bugs still propagate.\"\"\"\n    gate = asyncio.Semaphore(limit)\n\n    async def one(name: str) -> Outcome:\n        async with gate:\n            try:\n                async with asyncio.timeout(per_check_timeout):\n                    return Outcome(name, True, await check(name))\n            except CheckFailed as exc:\n                return Outcome(name, False, str(exc))\n            except TimeoutError:\n                return Outcome(name, False, f\"timed out after {per_check_timeout:g}s\")\n\n    async with asyncio.TaskGroup() as tg:\n        tasks = [tg.create_task(one(name)) for name in names]\n    return [task.result() for task in tasks]\n\n\ndef describe(group: BaseExceptionGroup) -> list[str]:\n    \"\"\"Flatten a (possibly nested) exception group into one line per leaf exception.\"\"\"\n    lines = []\n    for exc in group.exceptions:\n        if isinstance(exc, BaseExceptionGroup):\n            lines.extend(describe(exc))\n        else:\n            lines.append(f\"{type(exc).__name__}: {exc}\")\n    return lines\n","python","",[14,133,134,143,161,168,177,190,203,208,213,232,239,244,249,255,266,275,284,292,297,302,333,374,380,392,412,417,441,453,467,472,482,498,513,545,554,559,564,588,615,621,630,635,651,660,668,679,699,713,731,741,772,777,790,811,829,834,839,855,861,872,886,898,904,912,945],{"__ignoreMap":131},[135,136,139],"span",{"class":137,"line":138},"line",1,[135,140,142],{"class":141},"sJ8bj","# src\u002Fmytool\u002Fchecks.py\n",[135,144,146,150,154,157],{"class":137,"line":145},2,[135,147,149],{"class":148},"szBVR","from",[135,151,153],{"class":152},"sj4cs"," __future__",[135,155,156],{"class":148}," import",[135,158,160],{"class":159},"sVt8B"," annotations\n",[135,162,164],{"class":137,"line":163},3,[135,165,167],{"emptyLinePlaceholder":166},true,"\n",[135,169,171,174],{"class":137,"line":170},4,[135,172,173],{"class":148},"import",[135,175,176],{"class":159}," asyncio\n",[135,178,180,182,185,187],{"class":137,"line":179},5,[135,181,149],{"class":148},[135,183,184],{"class":159}," collections.abc ",[135,186,173],{"class":148},[135,188,189],{"class":159}," Awaitable, Callable, Iterable\n",[135,191,193,195,198,200],{"class":137,"line":192},6,[135,194,149],{"class":148},[135,196,197],{"class":159}," dataclasses ",[135,199,173],{"class":148},[135,201,202],{"class":159}," dataclass\n",[135,204,206],{"class":137,"line":205},7,[135,207,167],{"emptyLinePlaceholder":166},[135,209,211],{"class":137,"line":210},8,[135,212,167],{"emptyLinePlaceholder":166},[135,214,216,219,223,226,229],{"class":137,"line":215},9,[135,217,218],{"class":148},"class",[135,220,222],{"class":221},"sScJk"," CheckFailed",[135,224,225],{"class":159},"(",[135,227,228],{"class":152},"Exception",[135,230,231],{"class":159},"):\n",[135,233,235],{"class":137,"line":234},10,[135,236,238],{"class":237},"sZZnC","    \"\"\"An expected, reportable failure of one check.\"\"\"\n",[135,240,242],{"class":137,"line":241},11,[135,243,167],{"emptyLinePlaceholder":166},[135,245,247],{"class":137,"line":246},12,[135,248,167],{"emptyLinePlaceholder":166},[135,250,252],{"class":137,"line":251},13,[135,253,254],{"class":221},"@dataclass\n",[135,256,258,260,263],{"class":137,"line":257},14,[135,259,218],{"class":148},[135,261,262],{"class":221}," Outcome",[135,264,265],{"class":159},":\n",[135,267,269,272],{"class":137,"line":268},15,[135,270,271],{"class":159},"    name: ",[135,273,274],{"class":152},"str\n",[135,276,278,281],{"class":137,"line":277},16,[135,279,280],{"class":159},"    ok: ",[135,282,283],{"class":152},"bool\n",[135,285,287,290],{"class":137,"line":286},17,[135,288,289],{"class":159},"    detail: ",[135,291,274],{"class":152},[135,293,295],{"class":137,"line":294},18,[135,296,167],{"emptyLinePlaceholder":166},[135,298,300],{"class":137,"line":299},19,[135,301,167],{"emptyLinePlaceholder":166},[135,303,305,308,311,314,317,320,323,325,328,330],{"class":137,"line":304},20,[135,306,307],{"class":148},"async",[135,309,310],{"class":148}," def",[135,312,313],{"class":221}," run_fail_fast",[135,315,316],{"class":159},"(names: Iterable[",[135,318,319],{"class":152},"str",[135,321,322],{"class":159},"], check: Callable[[",[135,324,319],{"class":152},[135,326,327],{"class":159},"], Awaitable[",[135,329,319],{"class":152},[135,331,332],{"class":159},"]],\n",[135,334,336,339,342,345,348,351,354,357,359,362,365,367,369,371],{"class":137,"line":335},21,[135,337,338],{"class":148},"                        *",[135,340,341],{"class":159},", limit: ",[135,343,344],{"class":152},"int",[135,346,347],{"class":148}," =",[135,349,350],{"class":152}," 8",[135,352,353],{"class":159},", timeout: ",[135,355,356],{"class":152},"float",[135,358,347],{"class":148},[135,360,361],{"class":152}," 30",[135,363,364],{"class":159},") -> dict[",[135,366,319],{"class":152},[135,368,82],{"class":159},[135,370,319],{"class":152},[135,372,373],{"class":159},"]:\n",[135,375,377],{"class":137,"line":376},22,[135,378,379],{"class":237},"    \"\"\"All-or-nothing: the first failure cancels every other check.\"\"\"\n",[135,381,383,386,389],{"class":137,"line":382},23,[135,384,385],{"class":159},"    gate ",[135,387,388],{"class":148},"=",[135,390,391],{"class":159}," asyncio.Semaphore(limit)\n",[135,393,395,398,400,402,404,407,409],{"class":137,"line":394},24,[135,396,397],{"class":159},"    results: dict[",[135,399,319],{"class":152},[135,401,82],{"class":159},[135,403,319],{"class":152},[135,405,406],{"class":159},"] ",[135,408,388],{"class":148},[135,410,411],{"class":159}," {}\n",[135,413,415],{"class":137,"line":414},25,[135,416,167],{"emptyLinePlaceholder":166},[135,418,420,423,425,428,431,433,436,439],{"class":137,"line":419},26,[135,421,422],{"class":148},"    async",[135,424,310],{"class":148},[135,426,427],{"class":221}," one",[135,429,430],{"class":159},"(name: ",[135,432,319],{"class":152},[135,434,435],{"class":159},") -> ",[135,437,438],{"class":152},"None",[135,440,265],{"class":159},[135,442,444,447,450],{"class":137,"line":443},27,[135,445,446],{"class":148},"        async",[135,448,449],{"class":148}," with",[135,451,452],{"class":159}," gate:\n",[135,454,456,459,461,464],{"class":137,"line":455},28,[135,457,458],{"class":159},"            results[name] ",[135,460,388],{"class":148},[135,462,463],{"class":148}," await",[135,465,466],{"class":159}," check(name)\n",[135,468,470],{"class":137,"line":469},29,[135,471,167],{"emptyLinePlaceholder":166},[135,473,475,477,479],{"class":137,"line":474},30,[135,476,422],{"class":148},[135,478,449],{"class":148},[135,480,481],{"class":159}," asyncio.timeout(timeout):\n",[135,483,485,487,489,492,495],{"class":137,"line":484},31,[135,486,446],{"class":148},[135,488,449],{"class":148},[135,490,491],{"class":159}," asyncio.TaskGroup() ",[135,493,494],{"class":148},"as",[135,496,497],{"class":159}," tg:\n",[135,499,501,504,507,510],{"class":137,"line":500},32,[135,502,503],{"class":148},"            for",[135,505,506],{"class":159}," name ",[135,508,509],{"class":148},"in",[135,511,512],{"class":159}," names:\n",[135,514,516,519,523,525,528,531,534,536,539,542],{"class":137,"line":515},33,[135,517,518],{"class":159},"                tg.create_task(one(name), ",[135,520,522],{"class":521},"s4XuR","name",[135,524,388],{"class":148},[135,526,527],{"class":148},"f",[135,529,530],{"class":237},"\"check:",[135,532,533],{"class":152},"{",[135,535,522],{"class":159},[135,537,538],{"class":152},"}",[135,540,541],{"class":237},"\"",[135,543,544],{"class":159},")\n",[135,546,548,551],{"class":137,"line":547},34,[135,549,550],{"class":148},"    return",[135,552,553],{"class":159}," results\n",[135,555,557],{"class":137,"line":556},35,[135,558,167],{"emptyLinePlaceholder":166},[135,560,562],{"class":137,"line":561},36,[135,563,167],{"emptyLinePlaceholder":166},[135,565,567,569,571,574,576,578,580,582,584,586],{"class":137,"line":566},37,[135,568,307],{"class":148},[135,570,310],{"class":148},[135,572,573],{"class":221}," run_keep_going",[135,575,316],{"class":159},[135,577,319],{"class":152},[135,579,322],{"class":159},[135,581,319],{"class":152},[135,583,327],{"class":159},[135,585,319],{"class":152},[135,587,332],{"class":159},[135,589,591,594,596,598,600,602,605,607,609,612],{"class":137,"line":590},38,[135,592,593],{"class":148},"                         *",[135,595,341],{"class":159},[135,597,344],{"class":152},[135,599,347],{"class":148},[135,601,350],{"class":152},[135,603,604],{"class":159},", per_check_timeout: ",[135,606,356],{"class":152},[135,608,347],{"class":148},[135,610,611],{"class":152}," 10",[135,613,614],{"class":159},") -> list[Outcome]:\n",[135,616,618],{"class":137,"line":617},39,[135,619,620],{"class":237},"    \"\"\"Best effort: every check runs; expected failures become outcomes, bugs still propagate.\"\"\"\n",[135,622,624,626,628],{"class":137,"line":623},40,[135,625,385],{"class":159},[135,627,388],{"class":148},[135,629,391],{"class":159},[135,631,633],{"class":137,"line":632},41,[135,634,167],{"emptyLinePlaceholder":166},[135,636,638,640,642,644,646,648],{"class":137,"line":637},42,[135,639,422],{"class":148},[135,641,310],{"class":148},[135,643,427],{"class":221},[135,645,430],{"class":159},[135,647,319],{"class":152},[135,649,650],{"class":159},") -> Outcome:\n",[135,652,654,656,658],{"class":137,"line":653},43,[135,655,446],{"class":148},[135,657,449],{"class":148},[135,659,452],{"class":159},[135,661,663,666],{"class":137,"line":662},44,[135,664,665],{"class":148},"            try",[135,667,265],{"class":159},[135,669,671,674,676],{"class":137,"line":670},45,[135,672,673],{"class":148},"                async",[135,675,449],{"class":148},[135,677,678],{"class":159}," asyncio.timeout(per_check_timeout):\n",[135,680,682,685,688,691,693,696],{"class":137,"line":681},46,[135,683,684],{"class":148},"                    return",[135,686,687],{"class":159}," Outcome(name, ",[135,689,690],{"class":152},"True",[135,692,82],{"class":159},[135,694,695],{"class":148},"await",[135,697,698],{"class":159}," check(name))\n",[135,700,702,705,708,710],{"class":137,"line":701},47,[135,703,704],{"class":148},"            except",[135,706,707],{"class":159}," CheckFailed ",[135,709,494],{"class":148},[135,711,712],{"class":159}," exc:\n",[135,714,716,719,721,724,726,728],{"class":137,"line":715},48,[135,717,718],{"class":148},"                return",[135,720,687],{"class":159},[135,722,723],{"class":152},"False",[135,725,82],{"class":159},[135,727,319],{"class":152},[135,729,730],{"class":159},"(exc))\n",[135,732,734,736,739],{"class":137,"line":733},49,[135,735,704],{"class":148},[135,737,738],{"class":152}," TimeoutError",[135,740,265],{"class":159},[135,742,744,746,748,750,752,754,757,759,762,765,767,770],{"class":137,"line":743},50,[135,745,718],{"class":148},[135,747,687],{"class":159},[135,749,723],{"class":152},[135,751,82],{"class":159},[135,753,527],{"class":148},[135,755,756],{"class":237},"\"timed out after ",[135,758,533],{"class":152},[135,760,761],{"class":159},"per_check_timeout",[135,763,764],{"class":148},":g",[135,766,538],{"class":152},[135,768,769],{"class":237},"s\"",[135,771,544],{"class":159},[135,773,775],{"class":137,"line":774},51,[135,776,167],{"emptyLinePlaceholder":166},[135,778,780,782,784,786,788],{"class":137,"line":779},52,[135,781,422],{"class":148},[135,783,449],{"class":148},[135,785,491],{"class":159},[135,787,494],{"class":148},[135,789,497],{"class":159},[135,791,793,796,798,801,804,806,808],{"class":137,"line":792},53,[135,794,795],{"class":159},"        tasks ",[135,797,388],{"class":148},[135,799,800],{"class":159}," [tg.create_task(one(name)) ",[135,802,803],{"class":148},"for",[135,805,506],{"class":159},[135,807,509],{"class":148},[135,809,810],{"class":159}," names]\n",[135,812,814,816,819,821,824,826],{"class":137,"line":813},54,[135,815,550],{"class":148},[135,817,818],{"class":159}," [task.result() ",[135,820,803],{"class":148},[135,822,823],{"class":159}," task ",[135,825,509],{"class":148},[135,827,828],{"class":159}," tasks]\n",[135,830,832],{"class":137,"line":831},55,[135,833,167],{"emptyLinePlaceholder":166},[135,835,837],{"class":137,"line":836},56,[135,838,167],{"emptyLinePlaceholder":166},[135,840,842,845,848,851,853],{"class":137,"line":841},57,[135,843,844],{"class":148},"def",[135,846,847],{"class":221}," describe",[135,849,850],{"class":159},"(group: BaseExceptionGroup) -> list[",[135,852,319],{"class":152},[135,854,373],{"class":159},[135,856,858],{"class":137,"line":857},58,[135,859,860],{"class":237},"    \"\"\"Flatten a (possibly nested) exception group into one line per leaf exception.\"\"\"\n",[135,862,864,867,869],{"class":137,"line":863},59,[135,865,866],{"class":159},"    lines ",[135,868,388],{"class":148},[135,870,871],{"class":159}," []\n",[135,873,875,878,881,883],{"class":137,"line":874},60,[135,876,877],{"class":148},"    for",[135,879,880],{"class":159}," exc ",[135,882,509],{"class":148},[135,884,885],{"class":159}," group.exceptions:\n",[135,887,889,892,895],{"class":137,"line":888},61,[135,890,891],{"class":148},"        if",[135,893,894],{"class":152}," isinstance",[135,896,897],{"class":159},"(exc, BaseExceptionGroup):\n",[135,899,901],{"class":137,"line":900},62,[135,902,903],{"class":159},"            lines.extend(describe(exc))\n",[135,905,907,910],{"class":137,"line":906},63,[135,908,909],{"class":148},"        else",[135,911,265],{"class":159},[135,913,915,918,920,922,925,928,931,934,936,939,941,943],{"class":137,"line":914},64,[135,916,917],{"class":159},"            lines.append(",[135,919,527],{"class":148},[135,921,541],{"class":237},[135,923,924],{"class":152},"{type",[135,926,927],{"class":159},"(exc).",[135,929,930],{"class":152},"__name__}",[135,932,933],{"class":237},": ",[135,935,533],{"class":152},[135,937,938],{"class":159},"exc",[135,940,538],{"class":152},[135,942,541],{"class":237},[135,944,544],{"class":159},[135,946,948,950],{"class":137,"line":947},65,[135,949,550],{"class":148},[135,951,952],{"class":159}," lines\n",[954,955,957],"h3",{"id":956},"fail-fast","Fail fast",[10,959,960,963,964,967,968,970,971,974,975,978],{},[14,961,962],{},"run_fail_fast"," is the TaskGroup in its natural form. Each task acquires a semaphore before working, which caps concurrency at ",[14,965,966],{},"limit"," without changing the structure — twenty hosts, at most eight probes in flight. The whole group sits inside ",[14,969,85],{},", so a hung probe cannot hold the command forever: when the deadline passes, the group is cancelled and the timeout surfaces as ",[14,972,973],{},"TimeoutError",". When a check raises ",[14,976,977],{},"CheckFailed",", its siblings are cancelled immediately, which is what the user wants from a deploy precondition check: no point testing the cache once the database is known to be down.",[954,980,982],{"id":981},"keep-going","Keep going",[10,984,985,988,989,992,993,996],{},[14,986,987],{},"run_keep_going"," changes one thing: each task catches the ",[30,990,991],{},"expected"," failure types itself and turns them into an ",[14,994,995],{},"Outcome",". Expected failures therefore never reach the group, so nothing is cancelled and every check runs to completion. Each check also gets its own timeout, so one slow host becomes a “timed out” outcome instead of a cancelled run. Results are read from the tasks in creation order, so the report lists hosts in the order the user gave them, not the order they finished.",[10,998,999,1000,1003,1004,1007,1008,66],{},"Unexpected exceptions — a ",[14,1001,1002],{},"KeyError"," from a bug — are deliberately ",[30,1005,1006],{},"not"," caught. They still propagate, cancel the siblings and fail the command, because continuing past a bug produces results nobody should trust. The distinction between expected and unexpected errors is the same one made in ",[62,1009,1011],{"href":1010},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli\u002F","designing an exception hierarchy for a CLI",[954,1013,1015],{"id":1014},"reporting-exception-groups","Reporting exception groups",[126,1017,1019],{"className":128,"code":1018,"language":130,"meta":131,"style":131},"# src\u002Fmytool\u002Fcli.py\nimport asyncio\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.checks import CheckFailed, describe, run_fail_fast, run_keep_going\n\napp = typer.Typer()\nHOSTS = [\"api\", \"db\", \"cache\", \"search\"]\n\n\nasync def probe(name: str) -> str:\n    await asyncio.sleep(0.01)\n    if name == \"search\":\n        raise CheckFailed(\"connection refused\")\n    return \"ok\"\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Health tool.\"\"\"\n\n\n@app.command()\ndef check(keep_going: Annotated[bool, typer.Option(\"--keep-going\", \"-k\")] = False) -> None:\n    \"\"\"Check every host; stop at the first failure unless --keep-going.\"\"\"\n    if keep_going:\n        outcomes = asyncio.run(run_keep_going(HOSTS, probe))\n        for o in outcomes:\n            typer.echo(f\"{o.name:\u003C8} {'ok' if o.ok else 'FAIL'}  {o.detail if not o.ok else ''}\".rstrip())\n        failed = sum(not o.ok for o in outcomes)\n        if failed:\n            typer.echo(f\"{failed} of {len(outcomes)} checks failed\", err=True)\n            raise typer.Exit(1)\n        return\n    try:\n        results = asyncio.run(run_fail_fast(HOSTS, probe))\n    except* CheckFailed as group:\n        for line in describe(group):\n            typer.echo(f\"error: {line}\", err=True)\n        raise typer.Exit(1)\n    except* TimeoutError:\n        typer.echo(\"error: checks did not finish in time\", err=True)\n        raise typer.Exit(1)\n    typer.echo(f\"all {len(results)} checks passed\")\n",[14,1020,1021,1026,1032,1044,1048,1055,1059,1071,1075,1085,1116,1120,1124,1143,1156,1171,1184,1191,1195,1199,1207,1221,1226,1230,1234,1241,1279,1284,1291,1306,1319,1384,1409,1416,1456,1469,1474,1481,1495,1507,1519,1546,1556,1564,1582,1592],{"__ignoreMap":131},[135,1022,1023],{"class":137,"line":138},[135,1024,1025],{"class":141},"# src\u002Fmytool\u002Fcli.py\n",[135,1027,1028,1030],{"class":137,"line":145},[135,1029,173],{"class":148},[135,1031,176],{"class":159},[135,1033,1034,1036,1039,1041],{"class":137,"line":163},[135,1035,149],{"class":148},[135,1037,1038],{"class":159}," typing ",[135,1040,173],{"class":148},[135,1042,1043],{"class":159}," Annotated\n",[135,1045,1046],{"class":137,"line":170},[135,1047,167],{"emptyLinePlaceholder":166},[135,1049,1050,1052],{"class":137,"line":179},[135,1051,173],{"class":148},[135,1053,1054],{"class":159}," typer\n",[135,1056,1057],{"class":137,"line":192},[135,1058,167],{"emptyLinePlaceholder":166},[135,1060,1061,1063,1066,1068],{"class":137,"line":205},[135,1062,149],{"class":148},[135,1064,1065],{"class":159}," mytool.checks ",[135,1067,173],{"class":148},[135,1069,1070],{"class":159}," CheckFailed, describe, run_fail_fast, run_keep_going\n",[135,1072,1073],{"class":137,"line":210},[135,1074,167],{"emptyLinePlaceholder":166},[135,1076,1077,1080,1082],{"class":137,"line":215},[135,1078,1079],{"class":159},"app ",[135,1081,388],{"class":148},[135,1083,1084],{"class":159}," typer.Typer()\n",[135,1086,1087,1090,1092,1095,1098,1100,1103,1105,1108,1110,1113],{"class":137,"line":234},[135,1088,1089],{"class":152},"HOSTS",[135,1091,347],{"class":148},[135,1093,1094],{"class":159}," [",[135,1096,1097],{"class":237},"\"api\"",[135,1099,82],{"class":159},[135,1101,1102],{"class":237},"\"db\"",[135,1104,82],{"class":159},[135,1106,1107],{"class":237},"\"cache\"",[135,1109,82],{"class":159},[135,1111,1112],{"class":237},"\"search\"",[135,1114,1115],{"class":159},"]\n",[135,1117,1118],{"class":137,"line":241},[135,1119,167],{"emptyLinePlaceholder":166},[135,1121,1122],{"class":137,"line":246},[135,1123,167],{"emptyLinePlaceholder":166},[135,1125,1126,1128,1130,1133,1135,1137,1139,1141],{"class":137,"line":251},[135,1127,307],{"class":148},[135,1129,310],{"class":148},[135,1131,1132],{"class":221}," probe",[135,1134,430],{"class":159},[135,1136,319],{"class":152},[135,1138,435],{"class":159},[135,1140,319],{"class":152},[135,1142,265],{"class":159},[135,1144,1145,1148,1151,1154],{"class":137,"line":257},[135,1146,1147],{"class":148},"    await",[135,1149,1150],{"class":159}," asyncio.sleep(",[135,1152,1153],{"class":152},"0.01",[135,1155,544],{"class":159},[135,1157,1158,1161,1163,1166,1169],{"class":137,"line":268},[135,1159,1160],{"class":148},"    if",[135,1162,506],{"class":159},[135,1164,1165],{"class":148},"==",[135,1167,1168],{"class":237}," \"search\"",[135,1170,265],{"class":159},[135,1172,1173,1176,1179,1182],{"class":137,"line":277},[135,1174,1175],{"class":148},"        raise",[135,1177,1178],{"class":159}," CheckFailed(",[135,1180,1181],{"class":237},"\"connection refused\"",[135,1183,544],{"class":159},[135,1185,1186,1188],{"class":137,"line":286},[135,1187,550],{"class":148},[135,1189,1190],{"class":237}," \"ok\"\n",[135,1192,1193],{"class":137,"line":294},[135,1194,167],{"emptyLinePlaceholder":166},[135,1196,1197],{"class":137,"line":299},[135,1198,167],{"emptyLinePlaceholder":166},[135,1200,1201,1204],{"class":137,"line":304},[135,1202,1203],{"class":221},"@app.callback",[135,1205,1206],{"class":159},"()\n",[135,1208,1209,1211,1214,1217,1219],{"class":137,"line":335},[135,1210,844],{"class":148},[135,1212,1213],{"class":221}," main",[135,1215,1216],{"class":159},"() -> ",[135,1218,438],{"class":152},[135,1220,265],{"class":159},[135,1222,1223],{"class":137,"line":376},[135,1224,1225],{"class":237},"    \"\"\"Health tool.\"\"\"\n",[135,1227,1228],{"class":137,"line":382},[135,1229,167],{"emptyLinePlaceholder":166},[135,1231,1232],{"class":137,"line":394},[135,1233,167],{"emptyLinePlaceholder":166},[135,1235,1236,1239],{"class":137,"line":414},[135,1237,1238],{"class":221},"@app.command",[135,1240,1206],{"class":159},[135,1242,1243,1245,1248,1251,1254,1257,1260,1262,1265,1268,1270,1273,1275,1277],{"class":137,"line":419},[135,1244,844],{"class":148},[135,1246,1247],{"class":221}," check",[135,1249,1250],{"class":159},"(keep_going: Annotated[",[135,1252,1253],{"class":152},"bool",[135,1255,1256],{"class":159},", typer.Option(",[135,1258,1259],{"class":237},"\"--keep-going\"",[135,1261,82],{"class":159},[135,1263,1264],{"class":237},"\"-k\"",[135,1266,1267],{"class":159},")] ",[135,1269,388],{"class":148},[135,1271,1272],{"class":152}," False",[135,1274,435],{"class":159},[135,1276,438],{"class":152},[135,1278,265],{"class":159},[135,1280,1281],{"class":137,"line":443},[135,1282,1283],{"class":237},"    \"\"\"Check every host; stop at the first failure unless --keep-going.\"\"\"\n",[135,1285,1286,1288],{"class":137,"line":455},[135,1287,1160],{"class":148},[135,1289,1290],{"class":159}," keep_going:\n",[135,1292,1293,1296,1298,1301,1303],{"class":137,"line":469},[135,1294,1295],{"class":159},"        outcomes ",[135,1297,388],{"class":148},[135,1299,1300],{"class":159}," asyncio.run(run_keep_going(",[135,1302,1089],{"class":152},[135,1304,1305],{"class":159},", probe))\n",[135,1307,1308,1311,1314,1316],{"class":137,"line":474},[135,1309,1310],{"class":148},"        for",[135,1312,1313],{"class":159}," o ",[135,1315,509],{"class":148},[135,1317,1318],{"class":159}," outcomes:\n",[135,1320,1321,1324,1326,1328,1330,1333,1336,1338,1341,1344,1347,1350,1353,1356,1358,1361,1364,1367,1370,1372,1374,1377,1379,1381],{"class":137,"line":484},[135,1322,1323],{"class":159},"            typer.echo(",[135,1325,527],{"class":148},[135,1327,541],{"class":237},[135,1329,533],{"class":152},[135,1331,1332],{"class":159},"o.name",[135,1334,1335],{"class":148},":\u003C8",[135,1337,538],{"class":152},[135,1339,1340],{"class":152}," {",[135,1342,1343],{"class":237},"'ok'",[135,1345,1346],{"class":148}," if",[135,1348,1349],{"class":159}," o.ok ",[135,1351,1352],{"class":148},"else",[135,1354,1355],{"class":237}," 'FAIL'",[135,1357,538],{"class":152},[135,1359,1360],{"class":152},"  {",[135,1362,1363],{"class":159},"o.detail ",[135,1365,1366],{"class":148},"if",[135,1368,1369],{"class":148}," not",[135,1371,1349],{"class":159},[135,1373,1352],{"class":148},[135,1375,1376],{"class":237}," ''",[135,1378,538],{"class":152},[135,1380,541],{"class":237},[135,1382,1383],{"class":159},".rstrip())\n",[135,1385,1386,1389,1391,1394,1396,1398,1400,1402,1404,1406],{"class":137,"line":500},[135,1387,1388],{"class":159},"        failed ",[135,1390,388],{"class":148},[135,1392,1393],{"class":152}," sum",[135,1395,225],{"class":159},[135,1397,1006],{"class":148},[135,1399,1349],{"class":159},[135,1401,803],{"class":148},[135,1403,1313],{"class":159},[135,1405,509],{"class":148},[135,1407,1408],{"class":159}," outcomes)\n",[135,1410,1411,1413],{"class":137,"line":515},[135,1412,891],{"class":148},[135,1414,1415],{"class":159}," failed:\n",[135,1417,1418,1420,1422,1424,1426,1429,1431,1434,1437,1440,1442,1445,1447,1450,1452,1454],{"class":137,"line":547},[135,1419,1323],{"class":159},[135,1421,527],{"class":148},[135,1423,541],{"class":237},[135,1425,533],{"class":152},[135,1427,1428],{"class":159},"failed",[135,1430,538],{"class":152},[135,1432,1433],{"class":237}," of ",[135,1435,1436],{"class":152},"{len",[135,1438,1439],{"class":159},"(outcomes)",[135,1441,538],{"class":152},[135,1443,1444],{"class":237}," checks failed\"",[135,1446,82],{"class":159},[135,1448,1449],{"class":521},"err",[135,1451,388],{"class":148},[135,1453,690],{"class":152},[135,1455,544],{"class":159},[135,1457,1458,1461,1464,1467],{"class":137,"line":556},[135,1459,1460],{"class":148},"            raise",[135,1462,1463],{"class":159}," typer.Exit(",[135,1465,1466],{"class":152},"1",[135,1468,544],{"class":159},[135,1470,1471],{"class":137,"line":561},[135,1472,1473],{"class":148},"        return\n",[135,1475,1476,1479],{"class":137,"line":566},[135,1477,1478],{"class":148},"    try",[135,1480,265],{"class":159},[135,1482,1483,1486,1488,1491,1493],{"class":137,"line":590},[135,1484,1485],{"class":159},"        results ",[135,1487,388],{"class":148},[135,1489,1490],{"class":159}," asyncio.run(run_fail_fast(",[135,1492,1089],{"class":152},[135,1494,1305],{"class":159},[135,1496,1497,1500,1502,1504],{"class":137,"line":617},[135,1498,1499],{"class":148},"    except*",[135,1501,707],{"class":159},[135,1503,494],{"class":148},[135,1505,1506],{"class":159}," group:\n",[135,1508,1509,1511,1514,1516],{"class":137,"line":623},[135,1510,1310],{"class":148},[135,1512,1513],{"class":159}," line ",[135,1515,509],{"class":148},[135,1517,1518],{"class":159}," describe(group):\n",[135,1520,1521,1523,1525,1528,1530,1532,1534,1536,1538,1540,1542,1544],{"class":137,"line":632},[135,1522,1323],{"class":159},[135,1524,527],{"class":148},[135,1526,1527],{"class":237},"\"error: ",[135,1529,533],{"class":152},[135,1531,137],{"class":159},[135,1533,538],{"class":152},[135,1535,541],{"class":237},[135,1537,82],{"class":159},[135,1539,1449],{"class":521},[135,1541,388],{"class":148},[135,1543,690],{"class":152},[135,1545,544],{"class":159},[135,1547,1548,1550,1552,1554],{"class":137,"line":637},[135,1549,1175],{"class":148},[135,1551,1463],{"class":159},[135,1553,1466],{"class":152},[135,1555,544],{"class":159},[135,1557,1558,1560,1562],{"class":137,"line":653},[135,1559,1499],{"class":148},[135,1561,738],{"class":152},[135,1563,265],{"class":159},[135,1565,1566,1569,1572,1574,1576,1578,1580],{"class":137,"line":662},[135,1567,1568],{"class":159},"        typer.echo(",[135,1570,1571],{"class":237},"\"error: checks did not finish in time\"",[135,1573,82],{"class":159},[135,1575,1449],{"class":521},[135,1577,388],{"class":148},[135,1579,690],{"class":152},[135,1581,544],{"class":159},[135,1583,1584,1586,1588,1590],{"class":137,"line":670},[135,1585,1175],{"class":148},[135,1587,1463],{"class":159},[135,1589,1466],{"class":152},[135,1591,544],{"class":159},[135,1593,1594,1597,1599,1602,1604,1607,1609,1612],{"class":137,"line":681},[135,1595,1596],{"class":159},"    typer.echo(",[135,1598,527],{"class":148},[135,1600,1601],{"class":237},"\"all ",[135,1603,1436],{"class":152},[135,1605,1606],{"class":159},"(results)",[135,1608,538],{"class":152},[135,1610,1611],{"class":237}," checks passed\"",[135,1613,544],{"class":159},[10,1615,1616,1618,1619,1622,1623,1625,1626,1628,1629,1631,1632,1635],{},[14,1617,59],{}," matches exception types ",[30,1620,1621],{},"inside"," a group: the ",[14,1624,977],{}," branch receives a sub-group containing only those, and the ",[14,1627,973],{}," branch handles the timeout (",[14,1630,59],{}," also matches a bare exception that is not in a group). Anything not matched — a bug — propagates as before and reaches the crash handler. ",[14,1633,1634],{},"describe"," flattens nested groups into one line per failure, because users want “error: CheckFailed: connection refused”, not a nested traceback tree. When several checks fail at nearly the same moment, a fail-fast group can contain more than one failure; the loop reports them all.",[101,1637],{"name":1638},"tg-terminal",[68,1640,1642],{"id":1641},"choosing-the-mode","Choosing the mode",[10,1644,1645,1646,1649,1650,1652],{},"Offer both and pick a default by asking what a partial run is worth. Preconditions, deploy steps and anything that changes state default to fail fast: once something is wrong, continuing wastes time or makes things worse. Read-only surveys — health checks, linting many files, downloading a batch where each file is independent — default to keep going, or at least offer ",[14,1647,1648],{},"-k\u002F--keep-going",", the flag ",[14,1651,48],{}," users know. Exit non-zero in both modes when anything failed, so scripts notice; print the summary (“1 of 4 checks failed”) on stderr.",[68,1654,1656],{"id":1655},"ux-considerations","UX considerations",[73,1658,1659,1673,1679,1689,1712],{},[76,1660,1661,1664,1665,1668,1669,66],{},[23,1662,1663],{},"Bound concurrency."," A semaphore around the work keeps the CLI from opening hundreds of connections; make the limit a ",[14,1666,1667],{},"--jobs"," option, as in ",[62,1670,1672],{"href":1671},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis\u002F","rate limiting concurrent requests in CLIs",[76,1674,1675,1678],{},[23,1676,1677],{},"Report in input order."," Users scan results against the list they supplied; completion order is noise.",[76,1680,1681,1684,1685,66],{},[23,1682,1683],{},"Show progress for long groups."," A live count of finished, failed and running tasks is covered in ",[62,1686,1688],{"href":1687},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks\u002F","showing progress for concurrent tasks",[76,1690,1691,1697,1698,1701,1702,1704,1705,1707,1708,66],{},[23,1692,1693,1694,66],{},"Clean up in ",[14,1695,1696],{},"finally"," Cancelled tasks receive ",[14,1699,1700],{},"CancelledError"," at their current ",[14,1703,695],{},"; partial files and open connections are closed in ",[14,1706,1696],{}," blocks, as in ",[62,1709,1711],{"href":1710},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c\u002F","cancelling async tasks on Ctrl+C",[76,1713,1714,1719,1720,1723,1724,1727,1728,1731],{},[23,1715,1716,1717,66],{},"Never swallow ",[14,1718,1700],{}," An ",[14,1721,1722],{},"except Exception"," does not catch it (it derives from ",[14,1725,1726],{},"BaseException","), but a bare ",[14,1729,1730],{},"except:"," does and breaks cancellation for the whole group.",[101,1733],{"name":1734},"tg-modes",[68,1736,1738],{"id":1737},"testing-the-behaviour","Testing the behaviour",[10,1740,1741],{},"Fake checks that sleep, fail or raise bugs on demand make every property testable in milliseconds — sibling cancellation, the concurrency cap, outcome order, per-check and overall timeouts, and the CLI’s messages:",[126,1743,1745],{"className":128,"code":1744,"language":130,"meta":131,"style":131},"# tests\u002Ftest_checks.py\nimport asyncio\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom mytool.checks import CheckFailed, describe, run_fail_fast, run_keep_going\nfrom mytool.cli import app\n\n\ndef make_check(fail=(), slow=(), bug=()):\n    started, cancelled = [], []\n\n    async def check(name):\n        started.append(name)\n        try:\n            await asyncio.sleep(1 if name in slow else 0.01)\n        except asyncio.CancelledError:\n            cancelled.append(name)\n            raise\n        if name in bug:\n            raise KeyError(name)\n        if name in fail:\n            raise CheckFailed(f\"{name} is down\")\n        return \"ok\"\n\n    return check, started, cancelled\n\n\ndef test_fail_fast_cancels_siblings():\n    check, started, cancelled = make_check(fail={\"b\"}, slow={\"a\", \"c\"})\n    with pytest.raises(ExceptionGroup) as info:\n        asyncio.run(run_fail_fast([\"a\", \"b\", \"c\"], check))\n    assert describe(info.value) == [\"CheckFailed: b is down\"]\n    assert sorted(cancelled) == [\"a\", \"c\"]\n\n\ndef test_concurrency_limit_is_respected():\n    running = peak = 0\n\n    async def check(name):\n        nonlocal running, peak\n        running += 1\n        peak = max(peak, running)\n        await asyncio.sleep(0.01)\n        running -= 1\n        return \"ok\"\n\n    asyncio.run(run_fail_fast([str(n) for n in range(20)], check, limit=3))\n    assert peak == 3\n\n\ndef test_keep_going_reports_every_outcome_in_order():\n    check, *_ = make_check(fail={\"b\"}, slow={\"c\"})\n    outcomes = asyncio.run(run_keep_going([\"a\", \"b\", \"c\"], check, per_check_timeout=0.2))\n    assert [(o.name, o.ok) for o in outcomes] == [(\"a\", True), (\"b\", False), (\"c\", False)]\n    assert outcomes[2].detail == \"timed out after 0.2s\"\n\n\ndef test_keep_going_still_surfaces_bugs():\n    check, *_ = make_check(bug={\"b\"})\n    with pytest.raises(ExceptionGroup) as info:\n        asyncio.run(run_keep_going([\"a\", \"b\"], check))\n    assert info.group_contains(KeyError)\n\n\ndef test_overall_timeout():\n    check, *_ = make_check(slow={\"a\"})\n    with pytest.raises(TimeoutError):\n        asyncio.run(run_fail_fast([\"a\"], check, timeout=0.05))\n\n\ndef test_cli_modes():\n    runner = CliRunner()\n    fast = runner.invoke(app, [\"check\"])\n    assert fast.exit_code == 1 and \"error: CheckFailed: connection refused\" in fast.stderr\n    full = runner.invoke(app, [\"check\", \"-k\"])\n    assert full.exit_code == 1 and \"search   FAIL  connection refused\" in full.stdout\n    assert \"1 of 4 checks failed\" in full.stderr\n",[14,1746,1747,1752,1758,1762,1769,1781,1785,1795,1807,1811,1815,1840,1850,1854,1865,1870,1877,1902,1910,1915,1920,1931,1941,1952,1973,1980,1984,1991,1995,1999,2009,2050,2063,2081,2098,2120,2124,2128,2137,2152,2156,2166,2174,2185,2198,2209,2218,2224,2228,2266,2277,2281,2285,2294,2329,2361,2408,2426,2430,2434,2443,2466,2476,2489,2500,2504,2509,2519,2542,2554,2573,2578,2583,2593,2604,2621,2646,2664,2686],{"__ignoreMap":131},[135,1748,1749],{"class":137,"line":138},[135,1750,1751],{"class":141},"# tests\u002Ftest_checks.py\n",[135,1753,1754,1756],{"class":137,"line":145},[135,1755,173],{"class":148},[135,1757,176],{"class":159},[135,1759,1760],{"class":137,"line":163},[135,1761,167],{"emptyLinePlaceholder":166},[135,1763,1764,1766],{"class":137,"line":170},[135,1765,173],{"class":148},[135,1767,1768],{"class":159}," pytest\n",[135,1770,1771,1773,1776,1778],{"class":137,"line":179},[135,1772,149],{"class":148},[135,1774,1775],{"class":159}," typer.testing ",[135,1777,173],{"class":148},[135,1779,1780],{"class":159}," CliRunner\n",[135,1782,1783],{"class":137,"line":192},[135,1784,167],{"emptyLinePlaceholder":166},[135,1786,1787,1789,1791,1793],{"class":137,"line":205},[135,1788,149],{"class":148},[135,1790,1065],{"class":159},[135,1792,173],{"class":148},[135,1794,1070],{"class":159},[135,1796,1797,1799,1802,1804],{"class":137,"line":210},[135,1798,149],{"class":148},[135,1800,1801],{"class":159}," mytool.cli ",[135,1803,173],{"class":148},[135,1805,1806],{"class":159}," app\n",[135,1808,1809],{"class":137,"line":215},[135,1810,167],{"emptyLinePlaceholder":166},[135,1812,1813],{"class":137,"line":234},[135,1814,167],{"emptyLinePlaceholder":166},[135,1816,1817,1819,1822,1825,1827,1830,1832,1835,1837],{"class":137,"line":241},[135,1818,844],{"class":148},[135,1820,1821],{"class":221}," make_check",[135,1823,1824],{"class":159},"(fail",[135,1826,388],{"class":148},[135,1828,1829],{"class":159},"(), slow",[135,1831,388],{"class":148},[135,1833,1834],{"class":159},"(), bug",[135,1836,388],{"class":148},[135,1838,1839],{"class":159},"()):\n",[135,1841,1842,1845,1847],{"class":137,"line":246},[135,1843,1844],{"class":159},"    started, cancelled ",[135,1846,388],{"class":148},[135,1848,1849],{"class":159}," [], []\n",[135,1851,1852],{"class":137,"line":251},[135,1853,167],{"emptyLinePlaceholder":166},[135,1855,1856,1858,1860,1862],{"class":137,"line":257},[135,1857,422],{"class":148},[135,1859,310],{"class":148},[135,1861,1247],{"class":221},[135,1863,1864],{"class":159},"(name):\n",[135,1866,1867],{"class":137,"line":268},[135,1868,1869],{"class":159},"        started.append(name)\n",[135,1871,1872,1875],{"class":137,"line":277},[135,1873,1874],{"class":148},"        try",[135,1876,265],{"class":159},[135,1878,1879,1882,1884,1886,1888,1890,1892,1895,1897,1900],{"class":137,"line":286},[135,1880,1881],{"class":148},"            await",[135,1883,1150],{"class":159},[135,1885,1466],{"class":152},[135,1887,1346],{"class":148},[135,1889,506],{"class":159},[135,1891,509],{"class":148},[135,1893,1894],{"class":159}," slow ",[135,1896,1352],{"class":148},[135,1898,1899],{"class":152}," 0.01",[135,1901,544],{"class":159},[135,1903,1904,1907],{"class":137,"line":294},[135,1905,1906],{"class":148},"        except",[135,1908,1909],{"class":159}," asyncio.CancelledError:\n",[135,1911,1912],{"class":137,"line":299},[135,1913,1914],{"class":159},"            cancelled.append(name)\n",[135,1916,1917],{"class":137,"line":304},[135,1918,1919],{"class":148},"            raise\n",[135,1921,1922,1924,1926,1928],{"class":137,"line":335},[135,1923,891],{"class":148},[135,1925,506],{"class":159},[135,1927,509],{"class":148},[135,1929,1930],{"class":159}," bug:\n",[135,1932,1933,1935,1938],{"class":137,"line":376},[135,1934,1460],{"class":148},[135,1936,1937],{"class":152}," KeyError",[135,1939,1940],{"class":159},"(name)\n",[135,1942,1943,1945,1947,1949],{"class":137,"line":382},[135,1944,891],{"class":148},[135,1946,506],{"class":159},[135,1948,509],{"class":148},[135,1950,1951],{"class":159}," fail:\n",[135,1953,1954,1956,1958,1960,1962,1964,1966,1968,1971],{"class":137,"line":394},[135,1955,1460],{"class":148},[135,1957,1178],{"class":159},[135,1959,527],{"class":148},[135,1961,541],{"class":237},[135,1963,533],{"class":152},[135,1965,522],{"class":159},[135,1967,538],{"class":152},[135,1969,1970],{"class":237}," is down\"",[135,1972,544],{"class":159},[135,1974,1975,1978],{"class":137,"line":414},[135,1976,1977],{"class":148},"        return",[135,1979,1190],{"class":237},[135,1981,1982],{"class":137,"line":419},[135,1983,167],{"emptyLinePlaceholder":166},[135,1985,1986,1988],{"class":137,"line":443},[135,1987,550],{"class":148},[135,1989,1990],{"class":159}," check, started, cancelled\n",[135,1992,1993],{"class":137,"line":455},[135,1994,167],{"emptyLinePlaceholder":166},[135,1996,1997],{"class":137,"line":469},[135,1998,167],{"emptyLinePlaceholder":166},[135,2000,2001,2003,2006],{"class":137,"line":474},[135,2002,844],{"class":148},[135,2004,2005],{"class":221}," test_fail_fast_cancels_siblings",[135,2007,2008],{"class":159},"():\n",[135,2010,2011,2014,2016,2019,2022,2024,2026,2029,2032,2035,2037,2039,2042,2044,2047],{"class":137,"line":484},[135,2012,2013],{"class":159},"    check, started, cancelled ",[135,2015,388],{"class":148},[135,2017,2018],{"class":159}," make_check(",[135,2020,2021],{"class":521},"fail",[135,2023,388],{"class":148},[135,2025,533],{"class":159},[135,2027,2028],{"class":237},"\"b\"",[135,2030,2031],{"class":159},"}, ",[135,2033,2034],{"class":521},"slow",[135,2036,388],{"class":148},[135,2038,533],{"class":159},[135,2040,2041],{"class":237},"\"a\"",[135,2043,82],{"class":159},[135,2045,2046],{"class":237},"\"c\"",[135,2048,2049],{"class":159},"})\n",[135,2051,2052,2055,2058,2060],{"class":137,"line":500},[135,2053,2054],{"class":148},"    with",[135,2056,2057],{"class":159}," pytest.raises(ExceptionGroup) ",[135,2059,494],{"class":148},[135,2061,2062],{"class":159}," info:\n",[135,2064,2065,2068,2070,2072,2074,2076,2078],{"class":137,"line":515},[135,2066,2067],{"class":159},"        asyncio.run(run_fail_fast([",[135,2069,2041],{"class":237},[135,2071,82],{"class":159},[135,2073,2028],{"class":237},[135,2075,82],{"class":159},[135,2077,2046],{"class":237},[135,2079,2080],{"class":159},"], check))\n",[135,2082,2083,2086,2089,2091,2093,2096],{"class":137,"line":547},[135,2084,2085],{"class":148},"    assert",[135,2087,2088],{"class":159}," describe(info.value) ",[135,2090,1165],{"class":148},[135,2092,1094],{"class":159},[135,2094,2095],{"class":237},"\"CheckFailed: b is down\"",[135,2097,1115],{"class":159},[135,2099,2100,2102,2105,2108,2110,2112,2114,2116,2118],{"class":137,"line":556},[135,2101,2085],{"class":148},[135,2103,2104],{"class":152}," sorted",[135,2106,2107],{"class":159},"(cancelled) ",[135,2109,1165],{"class":148},[135,2111,1094],{"class":159},[135,2113,2041],{"class":237},[135,2115,82],{"class":159},[135,2117,2046],{"class":237},[135,2119,1115],{"class":159},[135,2121,2122],{"class":137,"line":561},[135,2123,167],{"emptyLinePlaceholder":166},[135,2125,2126],{"class":137,"line":566},[135,2127,167],{"emptyLinePlaceholder":166},[135,2129,2130,2132,2135],{"class":137,"line":590},[135,2131,844],{"class":148},[135,2133,2134],{"class":221}," test_concurrency_limit_is_respected",[135,2136,2008],{"class":159},[135,2138,2139,2142,2144,2147,2149],{"class":137,"line":617},[135,2140,2141],{"class":159},"    running ",[135,2143,388],{"class":148},[135,2145,2146],{"class":159}," peak ",[135,2148,388],{"class":148},[135,2150,2151],{"class":152}," 0\n",[135,2153,2154],{"class":137,"line":623},[135,2155,167],{"emptyLinePlaceholder":166},[135,2157,2158,2160,2162,2164],{"class":137,"line":632},[135,2159,422],{"class":148},[135,2161,310],{"class":148},[135,2163,1247],{"class":221},[135,2165,1864],{"class":159},[135,2167,2168,2171],{"class":137,"line":637},[135,2169,2170],{"class":148},"        nonlocal",[135,2172,2173],{"class":159}," running, peak\n",[135,2175,2176,2179,2182],{"class":137,"line":653},[135,2177,2178],{"class":159},"        running ",[135,2180,2181],{"class":148},"+=",[135,2183,2184],{"class":152}," 1\n",[135,2186,2187,2190,2192,2195],{"class":137,"line":662},[135,2188,2189],{"class":159},"        peak ",[135,2191,388],{"class":148},[135,2193,2194],{"class":152}," max",[135,2196,2197],{"class":159},"(peak, running)\n",[135,2199,2200,2203,2205,2207],{"class":137,"line":670},[135,2201,2202],{"class":148},"        await",[135,2204,1150],{"class":159},[135,2206,1153],{"class":152},[135,2208,544],{"class":159},[135,2210,2211,2213,2216],{"class":137,"line":681},[135,2212,2178],{"class":159},[135,2214,2215],{"class":148},"-=",[135,2217,2184],{"class":152},[135,2219,2220,2222],{"class":137,"line":701},[135,2221,1977],{"class":148},[135,2223,1190],{"class":237},[135,2225,2226],{"class":137,"line":715},[135,2227,167],{"emptyLinePlaceholder":166},[135,2229,2230,2233,2235,2238,2240,2243,2245,2248,2250,2253,2256,2258,2260,2263],{"class":137,"line":733},[135,2231,2232],{"class":159},"    asyncio.run(run_fail_fast([",[135,2234,319],{"class":152},[135,2236,2237],{"class":159},"(n) ",[135,2239,803],{"class":148},[135,2241,2242],{"class":159}," n ",[135,2244,509],{"class":148},[135,2246,2247],{"class":152}," range",[135,2249,225],{"class":159},[135,2251,2252],{"class":152},"20",[135,2254,2255],{"class":159},")], check, ",[135,2257,966],{"class":521},[135,2259,388],{"class":148},[135,2261,2262],{"class":152},"3",[135,2264,2265],{"class":159},"))\n",[135,2267,2268,2270,2272,2274],{"class":137,"line":743},[135,2269,2085],{"class":148},[135,2271,2146],{"class":159},[135,2273,1165],{"class":148},[135,2275,2276],{"class":152}," 3\n",[135,2278,2279],{"class":137,"line":774},[135,2280,167],{"emptyLinePlaceholder":166},[135,2282,2283],{"class":137,"line":779},[135,2284,167],{"emptyLinePlaceholder":166},[135,2286,2287,2289,2292],{"class":137,"line":792},[135,2288,844],{"class":148},[135,2290,2291],{"class":221}," test_keep_going_reports_every_outcome_in_order",[135,2293,2008],{"class":159},[135,2295,2296,2299,2302,2305,2307,2309,2311,2313,2315,2317,2319,2321,2323,2325,2327],{"class":137,"line":813},[135,2297,2298],{"class":159},"    check, ",[135,2300,2301],{"class":148},"*",[135,2303,2304],{"class":159},"_ ",[135,2306,388],{"class":148},[135,2308,2018],{"class":159},[135,2310,2021],{"class":521},[135,2312,388],{"class":148},[135,2314,533],{"class":159},[135,2316,2028],{"class":237},[135,2318,2031],{"class":159},[135,2320,2034],{"class":521},[135,2322,388],{"class":148},[135,2324,533],{"class":159},[135,2326,2046],{"class":237},[135,2328,2049],{"class":159},[135,2330,2331,2334,2336,2339,2341,2343,2345,2347,2349,2352,2354,2356,2359],{"class":137,"line":831},[135,2332,2333],{"class":159},"    outcomes ",[135,2335,388],{"class":148},[135,2337,2338],{"class":159}," asyncio.run(run_keep_going([",[135,2340,2041],{"class":237},[135,2342,82],{"class":159},[135,2344,2028],{"class":237},[135,2346,82],{"class":159},[135,2348,2046],{"class":237},[135,2350,2351],{"class":159},"], check, ",[135,2353,761],{"class":521},[135,2355,388],{"class":148},[135,2357,2358],{"class":152},"0.2",[135,2360,2265],{"class":159},[135,2362,2363,2365,2368,2370,2372,2374,2377,2379,2382,2384,2386,2388,2391,2393,2395,2397,2399,2401,2403,2405],{"class":137,"line":836},[135,2364,2085],{"class":148},[135,2366,2367],{"class":159}," [(o.name, o.ok) ",[135,2369,803],{"class":148},[135,2371,1313],{"class":159},[135,2373,509],{"class":148},[135,2375,2376],{"class":159}," outcomes] ",[135,2378,1165],{"class":148},[135,2380,2381],{"class":159}," [(",[135,2383,2041],{"class":237},[135,2385,82],{"class":159},[135,2387,690],{"class":152},[135,2389,2390],{"class":159},"), (",[135,2392,2028],{"class":237},[135,2394,82],{"class":159},[135,2396,723],{"class":152},[135,2398,2390],{"class":159},[135,2400,2046],{"class":237},[135,2402,82],{"class":159},[135,2404,723],{"class":152},[135,2406,2407],{"class":159},")]\n",[135,2409,2410,2412,2415,2418,2421,2423],{"class":137,"line":841},[135,2411,2085],{"class":148},[135,2413,2414],{"class":159}," outcomes[",[135,2416,2417],{"class":152},"2",[135,2419,2420],{"class":159},"].detail ",[135,2422,1165],{"class":148},[135,2424,2425],{"class":237}," \"timed out after 0.2s\"\n",[135,2427,2428],{"class":137,"line":857},[135,2429,167],{"emptyLinePlaceholder":166},[135,2431,2432],{"class":137,"line":863},[135,2433,167],{"emptyLinePlaceholder":166},[135,2435,2436,2438,2441],{"class":137,"line":874},[135,2437,844],{"class":148},[135,2439,2440],{"class":221}," test_keep_going_still_surfaces_bugs",[135,2442,2008],{"class":159},[135,2444,2445,2447,2449,2451,2453,2455,2458,2460,2462,2464],{"class":137,"line":888},[135,2446,2298],{"class":159},[135,2448,2301],{"class":148},[135,2450,2304],{"class":159},[135,2452,388],{"class":148},[135,2454,2018],{"class":159},[135,2456,2457],{"class":521},"bug",[135,2459,388],{"class":148},[135,2461,533],{"class":159},[135,2463,2028],{"class":237},[135,2465,2049],{"class":159},[135,2467,2468,2470,2472,2474],{"class":137,"line":900},[135,2469,2054],{"class":148},[135,2471,2057],{"class":159},[135,2473,494],{"class":148},[135,2475,2062],{"class":159},[135,2477,2478,2481,2483,2485,2487],{"class":137,"line":906},[135,2479,2480],{"class":159},"        asyncio.run(run_keep_going([",[135,2482,2041],{"class":237},[135,2484,82],{"class":159},[135,2486,2028],{"class":237},[135,2488,2080],{"class":159},[135,2490,2491,2493,2496,2498],{"class":137,"line":914},[135,2492,2085],{"class":148},[135,2494,2495],{"class":159}," info.group_contains(",[135,2497,1002],{"class":152},[135,2499,544],{"class":159},[135,2501,2502],{"class":137,"line":947},[135,2503,167],{"emptyLinePlaceholder":166},[135,2505,2507],{"class":137,"line":2506},66,[135,2508,167],{"emptyLinePlaceholder":166},[135,2510,2512,2514,2517],{"class":137,"line":2511},67,[135,2513,844],{"class":148},[135,2515,2516],{"class":221}," test_overall_timeout",[135,2518,2008],{"class":159},[135,2520,2522,2524,2526,2528,2530,2532,2534,2536,2538,2540],{"class":137,"line":2521},68,[135,2523,2298],{"class":159},[135,2525,2301],{"class":148},[135,2527,2304],{"class":159},[135,2529,388],{"class":148},[135,2531,2018],{"class":159},[135,2533,2034],{"class":521},[135,2535,388],{"class":148},[135,2537,533],{"class":159},[135,2539,2041],{"class":237},[135,2541,2049],{"class":159},[135,2543,2545,2547,2550,2552],{"class":137,"line":2544},69,[135,2546,2054],{"class":148},[135,2548,2549],{"class":159}," pytest.raises(",[135,2551,973],{"class":152},[135,2553,231],{"class":159},[135,2555,2557,2559,2561,2563,2566,2568,2571],{"class":137,"line":2556},70,[135,2558,2067],{"class":159},[135,2560,2041],{"class":237},[135,2562,2351],{"class":159},[135,2564,2565],{"class":521},"timeout",[135,2567,388],{"class":148},[135,2569,2570],{"class":152},"0.05",[135,2572,2265],{"class":159},[135,2574,2576],{"class":137,"line":2575},71,[135,2577,167],{"emptyLinePlaceholder":166},[135,2579,2581],{"class":137,"line":2580},72,[135,2582,167],{"emptyLinePlaceholder":166},[135,2584,2586,2588,2591],{"class":137,"line":2585},73,[135,2587,844],{"class":148},[135,2589,2590],{"class":221}," test_cli_modes",[135,2592,2008],{"class":159},[135,2594,2596,2599,2601],{"class":137,"line":2595},74,[135,2597,2598],{"class":159},"    runner ",[135,2600,388],{"class":148},[135,2602,2603],{"class":159}," CliRunner()\n",[135,2605,2607,2610,2612,2615,2618],{"class":137,"line":2606},75,[135,2608,2609],{"class":159},"    fast ",[135,2611,388],{"class":148},[135,2613,2614],{"class":159}," runner.invoke(app, [",[135,2616,2617],{"class":237},"\"check\"",[135,2619,2620],{"class":159},"])\n",[135,2622,2624,2626,2629,2631,2634,2637,2640,2643],{"class":137,"line":2623},76,[135,2625,2085],{"class":148},[135,2627,2628],{"class":159}," fast.exit_code ",[135,2630,1165],{"class":148},[135,2632,2633],{"class":152}," 1",[135,2635,2636],{"class":148}," and",[135,2638,2639],{"class":237}," \"error: CheckFailed: connection refused\"",[135,2641,2642],{"class":148}," in",[135,2644,2645],{"class":159}," fast.stderr\n",[135,2647,2649,2652,2654,2656,2658,2660,2662],{"class":137,"line":2648},77,[135,2650,2651],{"class":159},"    full ",[135,2653,388],{"class":148},[135,2655,2614],{"class":159},[135,2657,2617],{"class":237},[135,2659,82],{"class":159},[135,2661,1264],{"class":237},[135,2663,2620],{"class":159},[135,2665,2667,2669,2672,2674,2676,2678,2681,2683],{"class":137,"line":2666},78,[135,2668,2085],{"class":148},[135,2670,2671],{"class":159}," full.exit_code ",[135,2673,1165],{"class":148},[135,2675,2633],{"class":152},[135,2677,2636],{"class":148},[135,2679,2680],{"class":237}," \"search   FAIL  connection refused\"",[135,2682,2642],{"class":148},[135,2684,2685],{"class":159}," full.stdout\n",[135,2687,2689,2691,2694,2696],{"class":137,"line":2688},79,[135,2690,2085],{"class":148},[135,2692,2693],{"class":237}," \"1 of 4 checks failed\"",[135,2695,2642],{"class":148},[135,2697,2698],{"class":159}," full.stderr\n",[10,2700,2701,2702,2704,2705,2708,2709,2712],{},"The cancellation test records which checks saw ",[14,2703,1700],{},", proving that the fail-fast group really stopped the slow siblings rather than waiting for them. ",[14,2706,2707],{},"ExceptionInfo.group_contains"," (pytest 8+) checks for an exception type anywhere inside a group without depending on nesting. Every test uses ",[14,2710,2711],{},"asyncio.run",", so no plugin is needed; pytest-asyncio or anyio’s pytest plugin are fine alternatives if your suite already uses them.",[68,2714,2716],{"id":2715},"conclusion","Conclusion",[10,2718,2719,2721,2722,2724,2725,2727,2728,2730],{},[14,2720,27],{}," gives concurrent CLI work a clear shape: tasks start and finish inside one block, failures cancel siblings and arrive together in an ",[14,2723,40],{},". Use the group as-is for fail-fast commands, catch expected failures inside each task for keep-going mode while letting bugs propagate, cap concurrency with a semaphore, put deadlines on the group or each task with ",[14,2726,85],{},", handle groups with ",[14,2729,59],{}," and flatten them into one line per failure, report in input order, and test cancellation and limits with fake checks.",[68,2732,2734],{"id":2733},"frequently-asked-questions","Frequently asked questions",[954,2736,2738,2739,2741],{"id":2737},"should-i-still-use-asynciogather","Should I still use ",[14,2740,16],{},"?",[10,2743,2744,2747,2748,2750],{},[14,2745,2746],{},"gather(..., return_exceptions=True)"," is a compact way to collect results and exceptions together. Without that flag, though, ",[14,2749,109],{}," raises the first exception while the remaining tasks keep running unsupervised, and you have to cancel them yourself. A TaskGroup handles that automatically, which makes it the safer default for new code.",[954,2752,2754],{"id":2753},"do-taskgroups-work-with-threads-or-processes","Do TaskGroups work with threads or processes?",[10,2756,2757,2758,2761,2762,66],{},"Not directly; they manage asyncio tasks. Blocking work can join a group through ",[14,2759,2760],{},"asyncio.to_thread(fn)",", which runs in a thread but is awaited like any coroutine. For thread-only code, see ",[62,2763,2765],{"href":2764},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","parallelising CLI work with thread pools",[954,2767,2769],{"id":2768},"what-about-trio-and-anyio","What about Trio and AnyIO?",[10,2771,2772,2773,2776],{},"Trio introduced nurseries, the idea TaskGroup adopts, and AnyIO offers a ",[14,2774,2775],{},"create_task_group()"," with the same semantics on top of asyncio or Trio. The recipe translates almost line for line.",[954,2778,2780],{"id":2779},"how-do-i-add-tasks-while-the-group-is-running","How do I add tasks while the group is running?",[10,2782,2783,2784,2787],{},"Pass the group to a task and call ",[14,2785,2786],{},"tg.create_task"," from inside it — a crawler that discovers new pages, for example. The group waits for those tasks too.",[954,2789,2791],{"id":2790},"can-i-limit-how-long-cancellation-takes","Can I limit how long cancellation takes?",[10,2793,2794,2795,2797,2798,2800],{},"Cancellation waits for each task’s ",[14,2796,1696],{}," blocks. Keep cleanup short and bounded, and use ",[14,2799,85],{}," around slow cleanup steps so a stuck task cannot block exit indefinitely.",[68,2802,2804],{"id":2803},"related","Related",[73,2806,2807,2813,2818,2823,2828],{},[76,2808,2809,2810],{},"Up: ",[62,2811,2812],{"href":64},"Concurrency and async in Python CLIs",[76,2814,2815],{},[62,2816,2817],{"href":1710},"Cancelling async tasks on Ctrl+C",[76,2819,2820],{},[62,2821,2822],{"href":1671},"Rate limiting concurrent requests in CLIs",[76,2824,2825],{},[62,2826,2827],{"href":1687},"Showing progress for concurrent tasks",[76,2829,2830],{},[62,2831,94],{"href":93},[2833,2834,2835],"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":131,"searchDepth":145,"depth":145,"links":2837},[2838,2839,2840,2845,2846,2847,2848,2849,2857],{"id":70,"depth":145,"text":71},{"id":98,"depth":145,"text":99},{"id":123,"depth":145,"text":124,"children":2841},[2842,2843,2844],{"id":956,"depth":163,"text":957},{"id":981,"depth":163,"text":982},{"id":1014,"depth":163,"text":1015},{"id":1641,"depth":145,"text":1642},{"id":1655,"depth":145,"text":1656},{"id":1737,"depth":145,"text":1738},{"id":2715,"depth":145,"text":2716},{"id":2733,"depth":145,"text":2734,"children":2850},[2851,2853,2854,2855,2856],{"id":2737,"depth":163,"text":2852},"Should I still use asyncio.gather?",{"id":2753,"depth":163,"text":2754},{"id":2768,"depth":163,"text":2769},{"id":2779,"depth":163,"text":2780},{"id":2790,"depth":163,"text":2791},{"id":2803,"depth":145,"text":2804},"2026-10-02","Run concurrent work in an async CLI with asyncio.TaskGroup: fail-fast and keep-going modes, except* for exception groups, concurrency limits, timeouts, tests.","advanced",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis",{"title":5,"description":2859},"cli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis\u002Findex",[2868,2869,2870,2871,2872],"asyncio","taskgroup","concurrency","exceptions","timeouts","7shijjoIiuLHFxhz-zS7lt6JnQcEujv4APaeJEbxVzU",[2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3164,3167,3170,3173,3176,3179,3182,3185,3188,3191,3194,3197,3200,3203,3206,3209,3212,3215,3218,3221,3224,3227,3230,3233,3236,3239,3242,3245,3248,3251,3254,3257,3260,3263,3266,3269,3272,3275,3278,3281,3284,3287,3290,3293,3296,3299,3302,3305,3308,3311,3314,3317,3320,3323,3326,3329,3332,3335,3338,3341,3344,3347,3350,3353,3356,3359,3362,3365,3368,3371,3374,3377,3380,3383,3386,3389,3392,3395,3398,3401,3404,3407,3410,3413,3416,3419,3422,3425,3428,3431,3434,3437,3440,3443,3446,3449,3452,3455,3458,3461,3464,3467,3470,3473,3476,3479,3482,3485,3488,3491,3494,3497,3500,3503,3506,3509,3512,3515,3518,3521,3524,3527,3530,3533,3536,3539,3542,3545,3548,3551,3554,3557,3560,3563,3566,3569,3572,3575,3578,3581,3584,3587,3590,3593,3596,3599,3602,3605,3608,3611,3614,3617,3620,3623,3626,3629,3632,3635,3638,3641,3644,3647,3650,3653,3656,3659,3662,3665,3668,3671,3674,3677,3680,3683,3686,3689,3692,3695,3698,3701,3704,3707,3710,3713,3716],{"path":2876,"title":2877},"\u002Fabout","About Python CLI Toolcraft",{"path":2879,"title":2880},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2882,"title":2883},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2885,"title":2886},"\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":2888,"title":2889},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2891,"title":2892},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2894,"title":2895},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2897,"title":2898},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2900,"title":2901},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2903,"title":2904},"\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":2906,"title":2907},"\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":2909,"title":2910},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2912,"title":2913},"\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":2915,"title":2916},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2918,"title":2919},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2921,"title":2922},"\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":2924,"title":2925},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2927,"title":2928},"\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":2930,"title":2931},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2933,"title":2934},"\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":2936,"title":2937},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2939,"title":2940},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2942,"title":2943},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2945,"title":2946},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2948,"title":2949},"\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":2951,"title":2952},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2954,"title":2955},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2957,"title":2958},"\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":2960,"title":2961},"\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":2963,"title":2964},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2966,"title":2967},"\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":2969,"title":2970},"\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":2972,"title":2973},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2975,"title":2976},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2978,"title":2979},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2981,"title":2982},"\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":2984,"title":2985},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2987,"title":2988},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2990,"title":2991},"\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":2993,"title":2994},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2996,"title":2997},"\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":2999,"title":3000},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":3002,"title":3003},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":3005,"title":3006},"\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":3008,"title":3009},"\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":3011,"title":3012},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3014,"title":3015},"\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":3017,"title":3018},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":3020,"title":3021},"\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":3023,"title":3024},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3026,"title":3027},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3029,"title":3030},"\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":3032,"title":3033},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3035,"title":3036},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":3038,"title":3039},"\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":3041,"title":3042},"\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":3044,"title":3045},"\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":3047,"title":3048},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3050,"title":3051},"\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":3053,"title":3054},"\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":3056,"title":3057},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3059,"title":3060},"\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":3062,"title":3063},"\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":3065,"title":3066},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3068,"title":3069},"\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":3071,"title":3072},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3074,"title":3075},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3077,"title":3078},"\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":3080,"title":3081},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3083,"title":3084},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3086,"title":3087},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3089,"title":3090},"\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":3092,"title":3093},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3095,"title":3096},"\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":3098,"title":3099},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3101,"title":3102},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3104,"title":3105},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3107,"title":3108},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3110,"title":3111},"\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":3113,"title":3114},"\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":3116,"title":3117},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3119,"title":3120},"\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":3122,"title":3123},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3125,"title":3126},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3128,"title":3129},"\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":3131,"title":3132},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3134,"title":3135},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3137,"title":3138},"\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":3140,"title":3141},"\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":3143,"title":3144},"\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":3146,"title":3147},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3149,"title":3150},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3152,"title":3153},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3155,"title":3156},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3158,"title":3159},"\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":3161,"title":3162},"\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":2864,"title":5},{"path":3165,"title":3166},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3168,"title":3169},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3171,"title":3172},"\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":3174,"title":3175},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3177,"title":3178},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3180,"title":3181},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3183,"title":3184},"\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":3186,"title":3187},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3189,"title":3190},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3192,"title":3193},"\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":3195,"title":3196},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3198,"title":3199},"\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":3201,"title":3202},"\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":3204,"title":3205},"\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":3207,"title":3208},"\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":3210,"title":3211},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3213,"title":3214},"\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":3216,"title":3217},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3219,"title":3220},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3222,"title":3223},"\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":3225,"title":3226},"\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":3228,"title":3229},"\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":3231,"title":3232},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3234,"title":3235},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3237,"title":3238},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3240,"title":3241},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3243,"title":3244},"\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":3246,"title":3247},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3249,"title":3250},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3252,"title":3253},"\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":3255,"title":3256},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3258,"title":3259},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3261,"title":3262},"\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":3264,"title":3265},"\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":3267,"title":3268},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3270,"title":3271},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3273,"title":3274},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3276,"title":3277},"\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":3279,"title":3280},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3282,"title":3283},"\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":3285,"title":3286},"\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":3288,"title":3289},"\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":3291,"title":3292},"\u002F","Python CLI Toolcraft",{"path":3294,"title":3295},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3297,"title":3298},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3300,"title":3301},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3303,"title":3304},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3306,"title":3307},"\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":3309,"title":3310},"\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":3312,"title":3313},"\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":3315,"title":3316},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3318,"title":3319},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3321,"title":3322},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3324,"title":3325},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3327,"title":3328},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3330,"title":3331},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3333,"title":3334},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3336,"title":3337},"\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":3339,"title":3340},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3342,"title":3343},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3345,"title":3346},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3348,"title":3349},"\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":3351,"title":3352},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3354,"title":3355},"\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":3357,"title":3358},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3360,"title":3361},"\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":3363,"title":3364},"\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":3366,"title":3367},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3369,"title":3370},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3372,"title":3373},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3375,"title":3376},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3378,"title":3379},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3381,"title":3382},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3384,"title":3385},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3387,"title":3388},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3390,"title":3391},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3393,"title":3394},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3396,"title":3397},"\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":3399,"title":3400},"\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":3402,"title":3403},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3405,"title":3406},"\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":3408,"title":3409},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3411,"title":3412},"\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":3414,"title":3415},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3417,"title":3418},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3420,"title":3421},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3423,"title":3424},"\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":3426,"title":3427},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3429,"title":3430},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3432,"title":3433},"\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":3435,"title":3436},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3438,"title":3439},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3441,"title":3442},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3444,"title":3445},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3447,"title":3448},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3450,"title":3451},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3453,"title":3454},"\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":3456,"title":3457},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3459,"title":3460},"\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":3462,"title":3463},"\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":3465,"title":3466},"\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":3468,"title":3469},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3471,"title":3472},"\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":3474,"title":3475},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3477,"title":3478},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3480,"title":3481},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3483,"title":3484},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3486,"title":3487},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3489,"title":3490},"\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":3492,"title":3493},"\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":3495,"title":3496},"\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":3498,"title":3499},"\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":3501,"title":3502},"\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":3504,"title":3505},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3507,"title":3508},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3510,"title":3511},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3513,"title":3514},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3516,"title":3517},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3519,"title":3520},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3522,"title":3523},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3525,"title":3526},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3528,"title":3529},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3531,"title":3532},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3534,"title":3535},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3537,"title":3538},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3540,"title":3541},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3543,"title":3544},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3546,"title":3547},"\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":3549,"title":3550},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3552,"title":3553},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3555,"title":3556},"\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":3558,"title":3559},"\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":3561,"title":3562},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3564,"title":3565},"\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":3567,"title":3568},"\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":3570,"title":3571},"\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":3573,"title":3574},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3576,"title":3577},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3579,"title":3580},"\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":3582,"title":3583},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3585,"title":3586},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3588,"title":3589},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3591,"title":3592},"\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":3594,"title":3595},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3597,"title":3598},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3600,"title":3601},"\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":3603,"title":3604},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3606,"title":3607},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3609,"title":3610},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3612,"title":3613},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3615,"title":3616},"\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":3618,"title":3619},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3621,"title":3622},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3624,"title":3625},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3627,"title":3628},"\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":3630,"title":3631},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3633,"title":3634},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3636,"title":3637},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3639,"title":3640},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3642,"title":3643},"\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":3645,"title":3646},"\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":3648,"title":3649},"\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":3651,"title":3652},"\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":3654,"title":3655},"\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":3657,"title":3658},"\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":3660,"title":3661},"\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":3663,"title":3664},"\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":3666,"title":3667},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3669,"title":3670},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3672,"title":3673},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3675,"title":3676},"\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":3678,"title":3679},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3681,"title":3682},"\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":3684,"title":3685},"\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":3687,"title":3688},"\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":3690,"title":3691},"\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":3693,"title":3694},"\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":3696,"title":3697},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3699,"title":3700},"\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":3702,"title":3703},"\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":3705,"title":3706},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3708,"title":3709},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3711,"title":3712},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3714,"title":3715},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3717,"title":3718},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967539745]