[{"data":1,"prerenderedAt":2832},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002F":3,"content-directory":2286},{"id":4,"title":5,"body":6,"date":2271,"description":2272,"difficulty":2273,"draft":2274,"extension":2275,"meta":2276,"navigation":238,"path":2277,"seo":2278,"stem":2279,"tags":2280,"updated":2271,"__hash__":2285},"content\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Findex.md","Concurrency and Async in Python CLIs",{"type":7,"value":8,"toc":2246},"minimark",[9,13,37,41,46,101,105,115,118,139,158,167,170,174,177,180,183,187,194,197,757,760,1240,1270,1274,1285,1579,1594,1598,1612,1616,1624,1645,1669,1673,1676,1708,1712,1719,1722,1770,1779,1783,1791,1795,1798,1822,2074,2078,2104,2108,2113,2119,2126,2133,2139,2154,2158,2164,2168,2175,2179,2197,2201,2242],[10,11,12],"p",{},"A CLI that processes one thing at a time is easy to write, easy to read and, for many jobs, far too slow. Checking the status of 300 repositories, uploading 2,000 files, fetching details for every item in a list, resizing a folder of images — each of these spends most of its time waiting on the network or burning a single CPU core while the other seven sit idle. Adding concurrency can turn a ten-minute command into a thirty-second one. It can also turn a predictable tool into one that hangs on Ctrl+C, floods an API until it is rate-limited, prints interleaved garbage, or loses errors somewhere inside a thread.",[10,14,15,16,20,21,26,27,31,32,36],{},"This topic covers adding concurrency to a Python CLI while keeping control of it: picking between threads, ",[17,18,19],"code",{},"asyncio"," and processes based on the work; keeping all concurrency behind one function so commands stay simple; bounding how much runs at once; cancelling cleanly when the user presses Ctrl+C; and staying within an API's rate limits. It is part of the ",[22,23,25],"a",{"href":24},"\u002Fcli-runtime-systems-integration\u002F","CLI Runtime & Systems Integration"," section and pairs naturally with ",[22,28,30],{"href":29},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002F","calling HTTP APIs"," and ",[22,33,35],{"href":34},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","running subprocesses",", which supply most of the work worth parallelising.",[38,39],"inline-diagram",{"name":40},"cc-topic-map",[42,43,45],"h2",{"id":44},"tldr","TL;DR",[47,48,49,68,74,83,89,95],"ul",{},[50,51,52,56,57,60,61,63,64,67],"li",{},[53,54,55],"strong",{},"Match the model to the work."," Waiting on network or disk: a ",[17,58,59],{},"ThreadPoolExecutor"," or ",[17,62,19],{},". Computing in pure Python: a ",[17,65,66],{},"ProcessPoolExecutor",".",[50,69,70,73],{},[53,71,72],{},"Keep concurrency inside one function"," that takes inputs and returns results. Commands stay synchronous and testable.",[50,75,76,79,80,67],{},[53,77,78],{},"Always bound concurrency"," with a pool size or semaphore, and expose it as ",[17,81,82],{},"--jobs",[50,84,85,88],{},[53,86,87],{},"Collect failures as values",", report them together, and pick the exit code from the whole batch.",[50,90,91,94],{},[53,92,93],{},"Make Ctrl+C stop everything",": cancel pending work, let running work clean up, and exit 130.",[50,96,97,100],{},[53,98,99],{},"Respect rate limits"," with a token bucket when calling APIs; more workers do not beat a quota.",[42,102,104],{"id":103},"choosing-a-concurrency-model","Choosing a concurrency model",[10,106,107,108,60,111,114],{},"Python gives you three practical models, and the choice follows from one question: while a unit of work is running, is it ",[53,109,110],{},"waiting",[53,112,113],{},"computing","?",[38,116],{"name":117},"cc-model-matrix",[10,119,120,123,124,127,128,131,132,131,135,138],{},[53,121,122],{},"Threads"," (",[17,125,126],{},"concurrent.futures.ThreadPoolExecutor",") are the most direct upgrade for an existing synchronous CLI. Every blocking library — ",[17,129,130],{},"httpx.Client",", ",[17,133,134],{},"boto3",[17,136,137],{},"subprocess",", file I\u002FO — releases the global interpreter lock (GIL) while it waits, so eight threads can wait on eight network requests at once. You keep your existing sync code and wrap the loop.",[10,140,141,143,144,147,148,131,151,131,154,157],{},[53,142,19],{}," runs many tasks on one thread, switching between them whenever one ",[17,145,146],{},"await","s. It scales to thousands of concurrent network operations with little overhead and has first-class cancellation, but it requires async-capable libraries (",[17,149,150],{},"httpx.AsyncClient",[17,152,153],{},"asyncpg",[17,155,156],{},"aiofiles",") and async functions all the way down the call chain.",[10,159,160,123,163,166],{},[53,161,162],{},"Processes",[17,164,165],{},"concurrent.futures.ProcessPoolExecutor",") are the answer when the work is CPU-bound Python — parsing, hashing, image manipulation in pure Python, data transformation. Each worker is a separate interpreter with its own GIL, so work runs truly in parallel across cores, at the cost of startup time and copying arguments and results between processes.",[10,168,169],{},"A rough decision rule that holds up well in practice: start with a thread pool; move to asyncio when you need hundreds of concurrent operations or fine-grained cancellation; use processes only when profiling shows the CPU is the bottleneck.",[42,171,173],{"id":172},"how-much-faster-really","How much faster, really?",[10,175,176],{},"For latency-bound work the gains are dramatic and easy to predict. If each of 40 requests takes 250 ms of mostly waiting, running them one after another takes ten seconds; with eight in flight at once, the ideal is a little over one second.",[38,178],{"name":179},"cc-speedup-bars",[10,181,182],{},"The curve flattens for three reasons, and each one is a design constraint rather than a bug: the server has its own limits (and will start returning 429s), connection setup adds cost per worker, and your local resources — file descriptors, bandwidth, memory — are finite. That is why every pattern in this topic includes an explicit bound.",[42,184,186],{"id":185},"keep-concurrency-behind-one-function","Keep concurrency behind one function",[10,188,189,190,193],{},"The single most useful structural rule: ",[53,191,192],{},"commands should not know that anything runs concurrently."," A command parses arguments, calls one function that owns the pool or event loop, receives ordinary results, and renders them. Threads, tasks and futures never escape that function.",[38,195],{"name":196},"cc-boundary",[198,199,204],"pre",{"className":200,"code":201,"language":202,"meta":203,"style":203},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fbatch.py\nfrom __future__ import annotations\n\nfrom collections.abc import Callable, Iterable\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom dataclasses import dataclass\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\")\nR = TypeVar(\"R\")\n\n\n@dataclass(frozen=True)\nclass Outcome(Generic[T, R]):\n    item: T\n    result: R | None = None\n    error: BaseException | None = None\n\n    @property\n    def ok(self) -> bool:\n        return self.error is None\n\n\ndef run_all(fn: Callable[[T], R], items: Iterable[T], *, jobs: int = 8,\n            on_done: Callable[[Outcome[T, R]], None] | None = None) -> list[Outcome[T, R]]:\n    \"\"\"Run fn over items with at most `jobs` in flight; never raises for item failures.\"\"\"\n    outcomes: list[Outcome[T, R]] = []\n    with ThreadPoolExecutor(max_workers=jobs) as pool:\n        futures = {pool.submit(fn, item): item for item in items}\n        try:\n            for future in as_completed(futures):\n                item = futures[future]\n                try:\n                    outcome = Outcome(item, result=future.result())\n                except Exception as exc:          # one bad item must not sink the batch\n                    outcome = Outcome(item, error=exc)\n                outcomes.append(outcome)\n                if on_done:\n                    on_done(outcome)\n        except KeyboardInterrupt:\n            pool.shutdown(wait=True, cancel_futures=True)\n            raise\n    return outcomes\n","python","",[17,205,206,215,233,240,254,267,280,293,298,317,332,337,342,363,375,381,399,417,422,431,449,466,471,476,505,528,534,545,568,591,599,613,624,632,651,669,686,692,701,707,718,742,748],{"__ignoreMap":203},[207,208,211],"span",{"class":209,"line":210},"line",1,[207,212,214],{"class":213},"sJ8bj","# src\u002Fmytool\u002Fbatch.py\n",[207,216,218,222,226,229],{"class":209,"line":217},2,[207,219,221],{"class":220},"szBVR","from",[207,223,225],{"class":224},"sj4cs"," __future__",[207,227,228],{"class":220}," import",[207,230,232],{"class":231},"sVt8B"," annotations\n",[207,234,236],{"class":209,"line":235},3,[207,237,239],{"emptyLinePlaceholder":238},true,"\n",[207,241,243,245,248,251],{"class":209,"line":242},4,[207,244,221],{"class":220},[207,246,247],{"class":231}," collections.abc ",[207,249,250],{"class":220},"import",[207,252,253],{"class":231}," Callable, Iterable\n",[207,255,257,259,262,264],{"class":209,"line":256},5,[207,258,221],{"class":220},[207,260,261],{"class":231}," concurrent.futures ",[207,263,250],{"class":220},[207,265,266],{"class":231}," ThreadPoolExecutor, as_completed\n",[207,268,270,272,275,277],{"class":209,"line":269},6,[207,271,221],{"class":220},[207,273,274],{"class":231}," dataclasses ",[207,276,250],{"class":220},[207,278,279],{"class":231}," dataclass\n",[207,281,283,285,288,290],{"class":209,"line":282},7,[207,284,221],{"class":220},[207,286,287],{"class":231}," typing ",[207,289,250],{"class":220},[207,291,292],{"class":231}," Generic, TypeVar\n",[207,294,296],{"class":209,"line":295},8,[207,297,239],{"emptyLinePlaceholder":238},[207,299,301,304,307,310,314],{"class":209,"line":300},9,[207,302,303],{"class":231},"T ",[207,305,306],{"class":220},"=",[207,308,309],{"class":231}," TypeVar(",[207,311,313],{"class":312},"sZZnC","\"T\"",[207,315,316],{"class":231},")\n",[207,318,320,323,325,327,330],{"class":209,"line":319},10,[207,321,322],{"class":231},"R ",[207,324,306],{"class":220},[207,326,309],{"class":231},[207,328,329],{"class":312},"\"R\"",[207,331,316],{"class":231},[207,333,335],{"class":209,"line":334},11,[207,336,239],{"emptyLinePlaceholder":238},[207,338,340],{"class":209,"line":339},12,[207,341,239],{"emptyLinePlaceholder":238},[207,343,345,349,352,356,358,361],{"class":209,"line":344},13,[207,346,348],{"class":347},"sScJk","@dataclass",[207,350,351],{"class":231},"(",[207,353,355],{"class":354},"s4XuR","frozen",[207,357,306],{"class":220},[207,359,360],{"class":224},"True",[207,362,316],{"class":231},[207,364,366,369,372],{"class":209,"line":365},14,[207,367,368],{"class":220},"class",[207,370,371],{"class":347}," Outcome",[207,373,374],{"class":231},"(Generic[T, R]):\n",[207,376,378],{"class":209,"line":377},15,[207,379,380],{"class":231},"    item: T\n",[207,382,384,387,390,393,396],{"class":209,"line":383},16,[207,385,386],{"class":231},"    result: R ",[207,388,389],{"class":220},"|",[207,391,392],{"class":224}," None",[207,394,395],{"class":220}," =",[207,397,398],{"class":224}," None\n",[207,400,402,405,408,411,413,415],{"class":209,"line":401},17,[207,403,404],{"class":231},"    error: ",[207,406,407],{"class":224},"BaseException",[207,409,410],{"class":220}," |",[207,412,392],{"class":224},[207,414,395],{"class":220},[207,416,398],{"class":224},[207,418,420],{"class":209,"line":419},18,[207,421,239],{"emptyLinePlaceholder":238},[207,423,425,428],{"class":209,"line":424},19,[207,426,427],{"class":347},"    @",[207,429,430],{"class":224},"property\n",[207,432,434,437,440,443,446],{"class":209,"line":433},20,[207,435,436],{"class":220},"    def",[207,438,439],{"class":347}," ok",[207,441,442],{"class":231},"(self) -> ",[207,444,445],{"class":224},"bool",[207,447,448],{"class":231},":\n",[207,450,452,455,458,461,464],{"class":209,"line":451},21,[207,453,454],{"class":220},"        return",[207,456,457],{"class":224}," self",[207,459,460],{"class":231},".error ",[207,462,463],{"class":220},"is",[207,465,398],{"class":224},[207,467,469],{"class":209,"line":468},22,[207,470,239],{"emptyLinePlaceholder":238},[207,472,474],{"class":209,"line":473},23,[207,475,239],{"emptyLinePlaceholder":238},[207,477,479,482,485,488,491,494,497,499,502],{"class":209,"line":478},24,[207,480,481],{"class":220},"def",[207,483,484],{"class":347}," run_all",[207,486,487],{"class":231},"(fn: Callable[[T], R], items: Iterable[T], ",[207,489,490],{"class":220},"*",[207,492,493],{"class":231},", jobs: ",[207,495,496],{"class":224},"int",[207,498,395],{"class":220},[207,500,501],{"class":224}," 8",[207,503,504],{"class":231},",\n",[207,506,508,511,514,517,519,521,523,525],{"class":209,"line":507},25,[207,509,510],{"class":231},"            on_done: Callable[[Outcome[T, R]], ",[207,512,513],{"class":224},"None",[207,515,516],{"class":231},"] ",[207,518,389],{"class":220},[207,520,392],{"class":224},[207,522,395],{"class":220},[207,524,392],{"class":224},[207,526,527],{"class":231},") -> list[Outcome[T, R]]:\n",[207,529,531],{"class":209,"line":530},26,[207,532,533],{"class":312},"    \"\"\"Run fn over items with at most `jobs` in flight; never raises for item failures.\"\"\"\n",[207,535,537,540,542],{"class":209,"line":536},27,[207,538,539],{"class":231},"    outcomes: list[Outcome[T, R]] ",[207,541,306],{"class":220},[207,543,544],{"class":231}," []\n",[207,546,548,551,554,557,559,562,565],{"class":209,"line":547},28,[207,549,550],{"class":220},"    with",[207,552,553],{"class":231}," ThreadPoolExecutor(",[207,555,556],{"class":354},"max_workers",[207,558,306],{"class":220},[207,560,561],{"class":231},"jobs) ",[207,563,564],{"class":220},"as",[207,566,567],{"class":231}," pool:\n",[207,569,571,574,576,579,582,585,588],{"class":209,"line":570},29,[207,572,573],{"class":231},"        futures ",[207,575,306],{"class":220},[207,577,578],{"class":231}," {pool.submit(fn, item): item ",[207,580,581],{"class":220},"for",[207,583,584],{"class":231}," item ",[207,586,587],{"class":220},"in",[207,589,590],{"class":231}," items}\n",[207,592,594,597],{"class":209,"line":593},30,[207,595,596],{"class":220},"        try",[207,598,448],{"class":231},[207,600,602,605,608,610],{"class":209,"line":601},31,[207,603,604],{"class":220},"            for",[207,606,607],{"class":231}," future ",[207,609,587],{"class":220},[207,611,612],{"class":231}," as_completed(futures):\n",[207,614,616,619,621],{"class":209,"line":615},32,[207,617,618],{"class":231},"                item ",[207,620,306],{"class":220},[207,622,623],{"class":231}," futures[future]\n",[207,625,627,630],{"class":209,"line":626},33,[207,628,629],{"class":220},"                try",[207,631,448],{"class":231},[207,633,635,638,640,643,646,648],{"class":209,"line":634},34,[207,636,637],{"class":231},"                    outcome ",[207,639,306],{"class":220},[207,641,642],{"class":231}," Outcome(item, ",[207,644,645],{"class":354},"result",[207,647,306],{"class":220},[207,649,650],{"class":231},"future.result())\n",[207,652,654,657,660,663,666],{"class":209,"line":653},35,[207,655,656],{"class":220},"                except",[207,658,659],{"class":224}," Exception",[207,661,662],{"class":220}," as",[207,664,665],{"class":231}," exc:          ",[207,667,668],{"class":213},"# one bad item must not sink the batch\n",[207,670,672,674,676,678,681,683],{"class":209,"line":671},36,[207,673,637],{"class":231},[207,675,306],{"class":220},[207,677,642],{"class":231},[207,679,680],{"class":354},"error",[207,682,306],{"class":220},[207,684,685],{"class":231},"exc)\n",[207,687,689],{"class":209,"line":688},37,[207,690,691],{"class":231},"                outcomes.append(outcome)\n",[207,693,695,698],{"class":209,"line":694},38,[207,696,697],{"class":220},"                if",[207,699,700],{"class":231}," on_done:\n",[207,702,704],{"class":209,"line":703},39,[207,705,706],{"class":231},"                    on_done(outcome)\n",[207,708,710,713,716],{"class":209,"line":709},40,[207,711,712],{"class":220},"        except",[207,714,715],{"class":224}," KeyboardInterrupt",[207,717,448],{"class":231},[207,719,721,724,727,729,731,733,736,738,740],{"class":209,"line":720},41,[207,722,723],{"class":231},"            pool.shutdown(",[207,725,726],{"class":354},"wait",[207,728,306],{"class":220},[207,730,360],{"class":224},[207,732,131],{"class":231},[207,734,735],{"class":354},"cancel_futures",[207,737,306],{"class":220},[207,739,360],{"class":224},[207,741,316],{"class":231},[207,743,745],{"class":209,"line":744},42,[207,746,747],{"class":220},"            raise\n",[207,749,751,754],{"class":209,"line":750},43,[207,752,753],{"class":220},"    return",[207,755,756],{"class":231}," outcomes\n",[10,758,759],{},"The command using it is ordinary synchronous Typer code:",[198,761,763],{"className":200,"code":762,"language":202,"meta":203,"style":203},"# src\u002Fmytool\u002Fcli.py\nimport httpx\nimport typer\n\nfrom mytool.batch import run_all\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Link checker.\"\"\"\n\n\n@app.command()\ndef check(urls_file: typer.FileText, jobs: int = typer.Option(8, \"--jobs\", \"-j\", min=1, max=64)) -> None:\n    \"\"\"Check that every URL in URLS_FILE responds.\"\"\"\n    urls = [line.strip() for line in urls_file if line.strip()]\n    with httpx.Client(timeout=10.0, follow_redirects=True) as client:\n        def probe(url: str) -> int:\n            return client.head(url).status_code\n\n        outcomes = run_all(probe, urls, jobs=jobs)\n    failed = [o for o in outcomes if not o.ok or o.result >= 400]\n    for o in failed:\n        typer.echo(f\"✗ {o.item}: {o.error or o.result}\", err=True)\n    typer.echo(f\"{len(urls) - len(failed)}\u002F{len(urls)} ok\", err=True)\n    raise typer.Exit(1 if failed else 0)\n\n\nif __name__ == \"__main__\":\n    app()\n",[17,764,765,770,777,784,788,800,804,814,818,822,830,844,849,853,857,864,921,926,952,984,1005,1013,1017,1035,1078,1090,1139,1188,1212,1216,1220,1235],{"__ignoreMap":203},[207,766,767],{"class":209,"line":210},[207,768,769],{"class":213},"# src\u002Fmytool\u002Fcli.py\n",[207,771,772,774],{"class":209,"line":217},[207,773,250],{"class":220},[207,775,776],{"class":231}," httpx\n",[207,778,779,781],{"class":209,"line":235},[207,780,250],{"class":220},[207,782,783],{"class":231}," typer\n",[207,785,786],{"class":209,"line":242},[207,787,239],{"emptyLinePlaceholder":238},[207,789,790,792,795,797],{"class":209,"line":256},[207,791,221],{"class":220},[207,793,794],{"class":231}," mytool.batch ",[207,796,250],{"class":220},[207,798,799],{"class":231}," run_all\n",[207,801,802],{"class":209,"line":269},[207,803,239],{"emptyLinePlaceholder":238},[207,805,806,809,811],{"class":209,"line":282},[207,807,808],{"class":231},"app ",[207,810,306],{"class":220},[207,812,813],{"class":231}," typer.Typer()\n",[207,815,816],{"class":209,"line":295},[207,817,239],{"emptyLinePlaceholder":238},[207,819,820],{"class":209,"line":300},[207,821,239],{"emptyLinePlaceholder":238},[207,823,824,827],{"class":209,"line":319},[207,825,826],{"class":347},"@app.callback",[207,828,829],{"class":231},"()\n",[207,831,832,834,837,840,842],{"class":209,"line":334},[207,833,481],{"class":220},[207,835,836],{"class":347}," main",[207,838,839],{"class":231},"() -> ",[207,841,513],{"class":224},[207,843,448],{"class":231},[207,845,846],{"class":209,"line":339},[207,847,848],{"class":312},"    \"\"\"Link checker.\"\"\"\n",[207,850,851],{"class":209,"line":344},[207,852,239],{"emptyLinePlaceholder":238},[207,854,855],{"class":209,"line":365},[207,856,239],{"emptyLinePlaceholder":238},[207,858,859,862],{"class":209,"line":377},[207,860,861],{"class":347},"@app.command",[207,863,829],{"class":231},[207,865,866,868,871,874,876,878,881,884,886,889,891,894,896,899,901,904,906,909,911,914,917,919],{"class":209,"line":383},[207,867,481],{"class":220},[207,869,870],{"class":347}," check",[207,872,873],{"class":231},"(urls_file: typer.FileText, jobs: ",[207,875,496],{"class":224},[207,877,395],{"class":220},[207,879,880],{"class":231}," typer.Option(",[207,882,883],{"class":224},"8",[207,885,131],{"class":231},[207,887,888],{"class":312},"\"--jobs\"",[207,890,131],{"class":231},[207,892,893],{"class":312},"\"-j\"",[207,895,131],{"class":231},[207,897,898],{"class":354},"min",[207,900,306],{"class":220},[207,902,903],{"class":224},"1",[207,905,131],{"class":231},[207,907,908],{"class":354},"max",[207,910,306],{"class":220},[207,912,913],{"class":224},"64",[207,915,916],{"class":231},")) -> ",[207,918,513],{"class":224},[207,920,448],{"class":231},[207,922,923],{"class":209,"line":401},[207,924,925],{"class":312},"    \"\"\"Check that every URL in URLS_FILE responds.\"\"\"\n",[207,927,928,931,933,936,938,941,943,946,949],{"class":209,"line":419},[207,929,930],{"class":231},"    urls ",[207,932,306],{"class":220},[207,934,935],{"class":231}," [line.strip() ",[207,937,581],{"class":220},[207,939,940],{"class":231}," line ",[207,942,587],{"class":220},[207,944,945],{"class":231}," urls_file ",[207,947,948],{"class":220},"if",[207,950,951],{"class":231}," line.strip()]\n",[207,953,954,956,959,962,964,967,969,972,974,976,979,981],{"class":209,"line":424},[207,955,550],{"class":220},[207,957,958],{"class":231}," httpx.Client(",[207,960,961],{"class":354},"timeout",[207,963,306],{"class":220},[207,965,966],{"class":224},"10.0",[207,968,131],{"class":231},[207,970,971],{"class":354},"follow_redirects",[207,973,306],{"class":220},[207,975,360],{"class":224},[207,977,978],{"class":231},") ",[207,980,564],{"class":220},[207,982,983],{"class":231}," client:\n",[207,985,986,989,992,995,998,1001,1003],{"class":209,"line":433},[207,987,988],{"class":220},"        def",[207,990,991],{"class":347}," probe",[207,993,994],{"class":231},"(url: ",[207,996,997],{"class":224},"str",[207,999,1000],{"class":231},") -> ",[207,1002,496],{"class":224},[207,1004,448],{"class":231},[207,1006,1007,1010],{"class":209,"line":451},[207,1008,1009],{"class":220},"            return",[207,1011,1012],{"class":231}," client.head(url).status_code\n",[207,1014,1015],{"class":209,"line":468},[207,1016,239],{"emptyLinePlaceholder":238},[207,1018,1019,1022,1024,1027,1030,1032],{"class":209,"line":473},[207,1020,1021],{"class":231},"        outcomes ",[207,1023,306],{"class":220},[207,1025,1026],{"class":231}," run_all(probe, urls, ",[207,1028,1029],{"class":354},"jobs",[207,1031,306],{"class":220},[207,1033,1034],{"class":231},"jobs)\n",[207,1036,1037,1040,1042,1045,1047,1050,1052,1055,1057,1060,1063,1066,1069,1072,1075],{"class":209,"line":478},[207,1038,1039],{"class":231},"    failed ",[207,1041,306],{"class":220},[207,1043,1044],{"class":231}," [o ",[207,1046,581],{"class":220},[207,1048,1049],{"class":231}," o ",[207,1051,587],{"class":220},[207,1053,1054],{"class":231}," outcomes ",[207,1056,948],{"class":220},[207,1058,1059],{"class":220}," not",[207,1061,1062],{"class":231}," o.ok ",[207,1064,1065],{"class":220},"or",[207,1067,1068],{"class":231}," o.result ",[207,1070,1071],{"class":220},">=",[207,1073,1074],{"class":224}," 400",[207,1076,1077],{"class":231},"]\n",[207,1079,1080,1083,1085,1087],{"class":209,"line":507},[207,1081,1082],{"class":220},"    for",[207,1084,1049],{"class":231},[207,1086,587],{"class":220},[207,1088,1089],{"class":231}," failed:\n",[207,1091,1092,1095,1098,1101,1104,1107,1110,1113,1115,1118,1120,1123,1125,1128,1130,1133,1135,1137],{"class":209,"line":530},[207,1093,1094],{"class":231},"        typer.echo(",[207,1096,1097],{"class":220},"f",[207,1099,1100],{"class":312},"\"✗ ",[207,1102,1103],{"class":224},"{",[207,1105,1106],{"class":231},"o.item",[207,1108,1109],{"class":224},"}",[207,1111,1112],{"class":312},": ",[207,1114,1103],{"class":224},[207,1116,1117],{"class":231},"o.error ",[207,1119,1065],{"class":220},[207,1121,1122],{"class":231}," o.result",[207,1124,1109],{"class":224},[207,1126,1127],{"class":312},"\"",[207,1129,131],{"class":231},[207,1131,1132],{"class":354},"err",[207,1134,306],{"class":220},[207,1136,360],{"class":224},[207,1138,316],{"class":231},[207,1140,1141,1144,1146,1148,1151,1154,1157,1160,1163,1165,1168,1170,1173,1175,1178,1180,1182,1184,1186],{"class":209,"line":536},[207,1142,1143],{"class":231},"    typer.echo(",[207,1145,1097],{"class":220},[207,1147,1127],{"class":312},[207,1149,1150],{"class":224},"{len",[207,1152,1153],{"class":231},"(urls) ",[207,1155,1156],{"class":220},"-",[207,1158,1159],{"class":224}," len",[207,1161,1162],{"class":231},"(failed)",[207,1164,1109],{"class":224},[207,1166,1167],{"class":312},"\u002F",[207,1169,1150],{"class":224},[207,1171,1172],{"class":231},"(urls)",[207,1174,1109],{"class":224},[207,1176,1177],{"class":312}," ok\"",[207,1179,131],{"class":231},[207,1181,1132],{"class":354},[207,1183,306],{"class":220},[207,1185,360],{"class":224},[207,1187,316],{"class":231},[207,1189,1190,1193,1196,1198,1201,1204,1207,1210],{"class":209,"line":547},[207,1191,1192],{"class":220},"    raise",[207,1194,1195],{"class":231}," typer.Exit(",[207,1197,903],{"class":224},[207,1199,1200],{"class":220}," if",[207,1202,1203],{"class":231}," failed ",[207,1205,1206],{"class":220},"else",[207,1208,1209],{"class":224}," 0",[207,1211,316],{"class":231},[207,1213,1214],{"class":209,"line":570},[207,1215,239],{"emptyLinePlaceholder":238},[207,1217,1218],{"class":209,"line":593},[207,1219,239],{"emptyLinePlaceholder":238},[207,1221,1222,1224,1227,1230,1233],{"class":209,"line":601},[207,1223,948],{"class":220},[207,1225,1226],{"class":224}," __name__",[207,1228,1229],{"class":220}," ==",[207,1231,1232],{"class":312}," \"__main__\"",[207,1234,448],{"class":231},[207,1236,1237],{"class":209,"line":615},[207,1238,1239],{"class":231},"    app()\n",[10,1241,1242,1243,1246,1247,1250,1251,1254,1255,1257,1258,1264,1265,1269],{},"Three habits are embedded here. ",[53,1244,1245],{},"Failures are values",": each item's exception is captured in its ",[17,1248,1249],{},"Outcome",", so one bad URL does not abort the other 299 and the command can report every failure at the end. ",[53,1252,1253],{},"The pool is bounded"," by ",[17,1256,82],{},", with a sensible default and a maximum. ",[53,1259,1260,1261,1263],{},"One ",[17,1262,130],{}," is shared"," across threads — httpx clients are thread-safe and share a connection pool, which is exactly what you want. ",[22,1266,1268],{"href":1267},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","Parallelising CLI work with thread pools"," develops this into a full pattern with a Rich progress bar.",[42,1271,1273],{"id":1272},"async-commands","Async commands",[10,1275,1276,1277,1280,1281,1284],{},"Typer and Click call your command functions synchronously, so an ",[17,1278,1279],{},"async def"," command does not run on its own. The bridge is ",[17,1282,1283],{},"asyncio.run()",", called once at the edge:",[198,1286,1288],{"className":200,"code":1287,"language":202,"meta":203,"style":203},"import asyncio\n\nimport httpx\nimport typer\n\napp = typer.Typer()\n\n\nasync def fetch_all(urls: list[str], limit: int) -> list[int]:\n    sem = asyncio.Semaphore(limit)\n    async with httpx.AsyncClient(timeout=10.0) as client:\n        async def one(url: str) -> int:\n            async with sem:\n                return (await client.get(url)).status_code\n\n        async with asyncio.TaskGroup() as tg:\n            tasks = [tg.create_task(one(u)) for u in urls]\n    return [t.result() for t in tasks]\n\n\n@app.command()\ndef statuses(urls: list[str], jobs: int = 16) -> None:\n    \"\"\"Print the status code of each URL.\"\"\"\n    for url, code in zip(urls, asyncio.run(fetch_all(urls, jobs))):\n        typer.echo(f\"{code}  {url}\")\n",[17,1289,1290,1297,1301,1307,1313,1317,1325,1329,1333,1362,1372,1395,1415,1425,1437,1441,1455,1475,1492,1496,1500,1506,1533,1538,1553],{"__ignoreMap":203},[207,1291,1292,1294],{"class":209,"line":210},[207,1293,250],{"class":220},[207,1295,1296],{"class":231}," asyncio\n",[207,1298,1299],{"class":209,"line":217},[207,1300,239],{"emptyLinePlaceholder":238},[207,1302,1303,1305],{"class":209,"line":235},[207,1304,250],{"class":220},[207,1306,776],{"class":231},[207,1308,1309,1311],{"class":209,"line":242},[207,1310,250],{"class":220},[207,1312,783],{"class":231},[207,1314,1315],{"class":209,"line":256},[207,1316,239],{"emptyLinePlaceholder":238},[207,1318,1319,1321,1323],{"class":209,"line":269},[207,1320,808],{"class":231},[207,1322,306],{"class":220},[207,1324,813],{"class":231},[207,1326,1327],{"class":209,"line":282},[207,1328,239],{"emptyLinePlaceholder":238},[207,1330,1331],{"class":209,"line":295},[207,1332,239],{"emptyLinePlaceholder":238},[207,1334,1335,1338,1341,1344,1347,1349,1352,1354,1357,1359],{"class":209,"line":300},[207,1336,1337],{"class":220},"async",[207,1339,1340],{"class":220}," def",[207,1342,1343],{"class":347}," fetch_all",[207,1345,1346],{"class":231},"(urls: list[",[207,1348,997],{"class":224},[207,1350,1351],{"class":231},"], limit: ",[207,1353,496],{"class":224},[207,1355,1356],{"class":231},") -> list[",[207,1358,496],{"class":224},[207,1360,1361],{"class":231},"]:\n",[207,1363,1364,1367,1369],{"class":209,"line":319},[207,1365,1366],{"class":231},"    sem ",[207,1368,306],{"class":220},[207,1370,1371],{"class":231}," asyncio.Semaphore(limit)\n",[207,1373,1374,1377,1380,1383,1385,1387,1389,1391,1393],{"class":209,"line":334},[207,1375,1376],{"class":220},"    async",[207,1378,1379],{"class":220}," with",[207,1381,1382],{"class":231}," httpx.AsyncClient(",[207,1384,961],{"class":354},[207,1386,306],{"class":220},[207,1388,966],{"class":224},[207,1390,978],{"class":231},[207,1392,564],{"class":220},[207,1394,983],{"class":231},[207,1396,1397,1400,1402,1405,1407,1409,1411,1413],{"class":209,"line":339},[207,1398,1399],{"class":220},"        async",[207,1401,1340],{"class":220},[207,1403,1404],{"class":347}," one",[207,1406,994],{"class":231},[207,1408,997],{"class":224},[207,1410,1000],{"class":231},[207,1412,496],{"class":224},[207,1414,448],{"class":231},[207,1416,1417,1420,1422],{"class":209,"line":344},[207,1418,1419],{"class":220},"            async",[207,1421,1379],{"class":220},[207,1423,1424],{"class":231}," sem:\n",[207,1426,1427,1430,1432,1434],{"class":209,"line":365},[207,1428,1429],{"class":220},"                return",[207,1431,123],{"class":231},[207,1433,146],{"class":220},[207,1435,1436],{"class":231}," client.get(url)).status_code\n",[207,1438,1439],{"class":209,"line":377},[207,1440,239],{"emptyLinePlaceholder":238},[207,1442,1443,1445,1447,1450,1452],{"class":209,"line":383},[207,1444,1399],{"class":220},[207,1446,1379],{"class":220},[207,1448,1449],{"class":231}," asyncio.TaskGroup() ",[207,1451,564],{"class":220},[207,1453,1454],{"class":231}," tg:\n",[207,1456,1457,1460,1462,1465,1467,1470,1472],{"class":209,"line":401},[207,1458,1459],{"class":231},"            tasks ",[207,1461,306],{"class":220},[207,1463,1464],{"class":231}," [tg.create_task(one(u)) ",[207,1466,581],{"class":220},[207,1468,1469],{"class":231}," u ",[207,1471,587],{"class":220},[207,1473,1474],{"class":231}," urls]\n",[207,1476,1477,1479,1482,1484,1487,1489],{"class":209,"line":419},[207,1478,753],{"class":220},[207,1480,1481],{"class":231}," [t.result() ",[207,1483,581],{"class":220},[207,1485,1486],{"class":231}," t ",[207,1488,587],{"class":220},[207,1490,1491],{"class":231}," tasks]\n",[207,1493,1494],{"class":209,"line":424},[207,1495,239],{"emptyLinePlaceholder":238},[207,1497,1498],{"class":209,"line":433},[207,1499,239],{"emptyLinePlaceholder":238},[207,1501,1502,1504],{"class":209,"line":451},[207,1503,861],{"class":347},[207,1505,829],{"class":231},[207,1507,1508,1510,1513,1515,1517,1520,1522,1524,1527,1529,1531],{"class":209,"line":468},[207,1509,481],{"class":220},[207,1511,1512],{"class":347}," statuses",[207,1514,1346],{"class":231},[207,1516,997],{"class":224},[207,1518,1519],{"class":231},"], jobs: ",[207,1521,496],{"class":224},[207,1523,395],{"class":220},[207,1525,1526],{"class":224}," 16",[207,1528,1000],{"class":231},[207,1530,513],{"class":224},[207,1532,448],{"class":231},[207,1534,1535],{"class":209,"line":473},[207,1536,1537],{"class":312},"    \"\"\"Print the status code of each URL.\"\"\"\n",[207,1539,1540,1542,1545,1547,1550],{"class":209,"line":478},[207,1541,1082],{"class":220},[207,1543,1544],{"class":231}," url, code ",[207,1546,587],{"class":220},[207,1548,1549],{"class":224}," zip",[207,1551,1552],{"class":231},"(urls, asyncio.run(fetch_all(urls, jobs))):\n",[207,1554,1555,1557,1559,1561,1563,1565,1567,1570,1573,1575,1577],{"class":209,"line":507},[207,1556,1094],{"class":231},[207,1558,1097],{"class":220},[207,1560,1127],{"class":312},[207,1562,1103],{"class":224},[207,1564,17],{"class":231},[207,1566,1109],{"class":224},[207,1568,1569],{"class":224},"  {",[207,1571,1572],{"class":231},"url",[207,1574,1109],{"class":224},[207,1576,1127],{"class":312},[207,1578,316],{"class":231},[10,1580,1581,1584,1585,1588,1589,1593],{},[17,1582,1583],{},"asyncio.TaskGroup"," (Python 3.11+) is the structured way to run tasks: if one fails, the others are cancelled, and no task can outlive the ",[17,1586,1587],{},"async with"," block. The semaphore bounds concurrency exactly as a pool size does. ",[22,1590,1592],{"href":1591},"\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"," covers a reusable decorator, async callbacks, and mixing sync and async libraries.",[42,1595,1597],{"id":1596},"cpu-bound-work-and-the-gil","CPU-bound work and the GIL",[10,1599,1600,1601,1603,1604,1606,1607,1611],{},"Threads do not speed up pure-Python computation in standard CPython builds, because only one thread executes Python bytecode at a time. For CPU-bound work, use a process pool. The API is nearly identical — ",[17,1602,66],{}," instead of ",[17,1605,59],{}," — but the constraints differ: functions and arguments must be picklable, worker functions must be defined at module level, and the CLI's entry point must be guarded so child processes do not re-run it. Free-threaded Python builds (3.13t and later) remove the GIL, and are worth watching, but for tools distributed to other people today, processes remain the portable answer. ",[22,1608,1610],{"href":1609},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks\u002F","Multiprocessing for CPU-bound CLI tasks"," covers the start-method differences between Linux, macOS and Windows, and chunking work so pickling costs do not eat the gains.",[42,1613,1615],{"id":1614},"stopping-ctrlc-and-failures","Stopping: Ctrl+C and failures",[10,1617,1618,1619,1623],{},"Starting concurrent work is easy; stopping it is where CLIs misbehave. When the user presses Ctrl+C they expect the tool to stop ",[1620,1621,1622],"em",{},"now"," — not after the remaining 280 queued items, and not by leaving half-written files and orphaned threads. The requirements are the same across all three models:",[1625,1626,1627,1630,1633,1636,1639],"ol",{},[50,1628,1629],{},"Stop scheduling new work immediately.",[50,1631,1632],{},"Cancel queued work that has not started.",[50,1634,1635],{},"Let running work finish or clean up, within a short bound.",[50,1637,1638],{},"Report what was done, what was cancelled, and what never started.",[50,1640,1641,1642,67],{},"Exit with 130, the conventional code for termination by ",[17,1643,1644],{},"SIGINT",[10,1646,1647,1648,1651,1652,1655,1656,1659,1660,1664,1665,67],{},"For thread pools, ",[17,1649,1650],{},"shutdown(cancel_futures=True)"," handles steps 1 and 2; running threads cannot be killed and must check a flag or finish their current item. For asyncio, ",[17,1653,1654],{},"asyncio.run"," (since 3.11) converts the first Ctrl+C into cancellation of the main task, which propagates to every child in a ",[17,1657,1658],{},"TaskGroup",". The details — shielding critical writes, bounding cleanup time, handling the second Ctrl+C — are in ",[22,1661,1663],{"href":1662},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c\u002F","cancelling async tasks on Ctrl+C",", building on the synchronous basics in ",[22,1666,1668],{"href":1667},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","handling KeyboardInterrupt cleanly",[42,1670,1672],{"id":1671},"output-from-concurrent-work","Output from concurrent work",[10,1674,1675],{},"Concurrent work produces output concurrently, and a terminal is a single shared resource. Three rules keep it readable:",[47,1677,1678,1688,1694],{},[50,1679,1680,1683,1684,1687],{},[53,1681,1682],{},"Print from one place."," Let workers return results and let the thread that collects them do the printing. The ",[17,1685,1686],{},"on_done"," callback above runs on the main thread, which is why it is safe to print or update a progress bar there.",[50,1689,1690,1693],{},[53,1691,1692],{},"Prefer a progress bar to a log of every item."," For hundreds of items, a single Rich progress bar with a count and an error tally is far more informative than hundreds of lines. Print individual lines only for failures.",[50,1695,1696,1699,1700,1703,1704,67],{},[53,1697,1698],{},"Keep results in a deterministic order"," when they go to stdout. ",[17,1701,1702],{},"as_completed"," yields in completion order, which differs every run; sort before printing so output is diffable, as described in ",[22,1705,1707],{"href":1706},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","emitting JSON output for scripting",[42,1709,1711],{"id":1710},"shared-state-between-workers","Shared state between workers",[10,1713,1714,1715,1718],{},"The fastest way to introduce a bug that appears once a week is to let workers mutate shared state. A dictionary of results updated from eight threads, a counter incremented without a lock, a list appended to from callbacks on different threads — each works in testing and fails under load, because individual Python operations are atomic but sequences of them are not. ",[17,1716,1717],{},"counts[key] = counts.get(key, 0) + 1"," is a read, a compute and a write, and two threads can interleave between them.",[10,1720,1721],{},"The patterns that avoid it, in order of preference:",[47,1723,1724,1733,1747,1757],{},[50,1725,1726,1729,1730,1732],{},[53,1727,1728],{},"Return, do not mutate."," Workers compute a value and return it; the collecting loop — on a single thread — builds whatever structure it needs. The ",[17,1731,1249],{}," list above is built this way, and it needs no locks at all.",[50,1734,1735,1738,1739,1742,1743,1746],{},[53,1736,1737],{},"Pass messages."," When workers must report progress while running, put events onto a ",[17,1740,1741],{},"queue.Queue"," (or an ",[17,1744,1745],{},"asyncio.Queue",") and drain it from one place.",[50,1748,1749,1752,1753,1756],{},[53,1750,1751],{},"Lock narrowly."," If shared mutable state is unavoidable — a cache that workers read and fill — protect it with a ",[17,1754,1755],{},"threading.Lock"," held only for the few lines that touch it, never around I\u002FO.",[50,1758,1759,1762,1763,1766,1767,67],{},[53,1760,1761],{},"Share only thread-safe objects."," HTTP clients, loggers and Rich consoles are designed to be shared. Database connections, most SDK clients created with mutable session state, and open files generally are not; create one per worker with ",[17,1764,1765],{},"threading.local()"," or an executor ",[17,1768,1769],{},"initializer",[10,1771,1772,1773,1775,1776,1778],{},"In asyncio the risk is smaller — tasks only switch at ",[17,1774,146],{}," points — but not zero: any ",[17,1777,146],{}," between reading and writing shared state is a place another task can change it. The same \"return, do not mutate\" rule removes the question entirely.",[42,1780,1782],{"id":1781},"being-a-good-api-citizen","Being a good API citizen",[10,1784,1785,1786,1790],{},"Concurrency multiplies your request rate, and most APIs enforce limits: a number of concurrent requests, a number per second, or both. A semaphore caps the first; a token bucket caps the second. Without a rate limiter, a fast CLI with sixteen workers can exhaust a quota in seconds and spend the rest of its run retrying 429s. ",[22,1787,1789],{"href":1788},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis\u002F","Rate-limiting concurrent requests in CLIs"," implements a token bucket for threads and for asyncio and shows how to derive sensible settings from an API's documented limits.",[42,1792,1794],{"id":1793},"testing-concurrent-code","Testing concurrent code",[10,1796,1797],{},"Concurrency adds non-determinism, and tests must not depend on timing luck. Three techniques keep them reliable:",[47,1799,1800,1810,1816],{},[50,1801,1802,1805,1806,1809],{},[53,1803,1804],{},"Test the batch function with trivial work."," ",[17,1807,1808],{},"run_all(lambda x: x * 2, range(100), jobs=8)"," exercises the pool without I\u002FO; assert on the set of results, not their order.",[50,1811,1812,1815],{},[53,1813,1814],{},"Inject failures deliberately."," A worker that raises for item 13 proves that other items still complete and that the failure is reported.",[50,1817,1818,1821],{},[53,1819,1820],{},"Replace time."," Rate limiters and retry loops take a clock and a sleep function as parameters, so tests advance a fake clock instead of waiting.",[198,1823,1825],{"className":200,"code":1824,"language":202,"meta":203,"style":203},"from mytool.batch import run_all\n\n\ndef test_failures_do_not_stop_the_batch():\n    def work(n: int) -> int:\n        if n == 13:\n            raise ValueError(\"unlucky\")\n        return n * 2\n\n    outcomes = run_all(work, range(50), jobs=8)\n    assert len(outcomes) == 50\n    assert sorted(o.result for o in outcomes if o.ok) == sorted(n * 2 for n in range(50) if n != 13)\n    [bad] = [o for o in outcomes if not o.ok]\n    assert bad.item == 13 and isinstance(bad.error, ValueError)\n",[17,1826,1827,1837,1841,1845,1855,1873,1889,1904,1915,1919,1948,1963,2025,2049],{"__ignoreMap":203},[207,1828,1829,1831,1833,1835],{"class":209,"line":210},[207,1830,221],{"class":220},[207,1832,794],{"class":231},[207,1834,250],{"class":220},[207,1836,799],{"class":231},[207,1838,1839],{"class":209,"line":217},[207,1840,239],{"emptyLinePlaceholder":238},[207,1842,1843],{"class":209,"line":235},[207,1844,239],{"emptyLinePlaceholder":238},[207,1846,1847,1849,1852],{"class":209,"line":242},[207,1848,481],{"class":220},[207,1850,1851],{"class":347}," test_failures_do_not_stop_the_batch",[207,1853,1854],{"class":231},"():\n",[207,1856,1857,1859,1862,1865,1867,1869,1871],{"class":209,"line":256},[207,1858,436],{"class":220},[207,1860,1861],{"class":347}," work",[207,1863,1864],{"class":231},"(n: ",[207,1866,496],{"class":224},[207,1868,1000],{"class":231},[207,1870,496],{"class":224},[207,1872,448],{"class":231},[207,1874,1875,1878,1881,1884,1887],{"class":209,"line":269},[207,1876,1877],{"class":220},"        if",[207,1879,1880],{"class":231}," n ",[207,1882,1883],{"class":220},"==",[207,1885,1886],{"class":224}," 13",[207,1888,448],{"class":231},[207,1890,1891,1894,1897,1899,1902],{"class":209,"line":282},[207,1892,1893],{"class":220},"            raise",[207,1895,1896],{"class":224}," ValueError",[207,1898,351],{"class":231},[207,1900,1901],{"class":312},"\"unlucky\"",[207,1903,316],{"class":231},[207,1905,1906,1908,1910,1912],{"class":209,"line":295},[207,1907,454],{"class":220},[207,1909,1880],{"class":231},[207,1911,490],{"class":220},[207,1913,1914],{"class":224}," 2\n",[207,1916,1917],{"class":209,"line":300},[207,1918,239],{"emptyLinePlaceholder":238},[207,1920,1921,1924,1926,1929,1932,1934,1937,1940,1942,1944,1946],{"class":209,"line":319},[207,1922,1923],{"class":231},"    outcomes ",[207,1925,306],{"class":220},[207,1927,1928],{"class":231}," run_all(work, ",[207,1930,1931],{"class":224},"range",[207,1933,351],{"class":231},[207,1935,1936],{"class":224},"50",[207,1938,1939],{"class":231},"), ",[207,1941,1029],{"class":354},[207,1943,306],{"class":220},[207,1945,883],{"class":224},[207,1947,316],{"class":231},[207,1949,1950,1953,1955,1958,1960],{"class":209,"line":334},[207,1951,1952],{"class":220},"    assert",[207,1954,1159],{"class":224},[207,1956,1957],{"class":231},"(outcomes) ",[207,1959,1883],{"class":220},[207,1961,1962],{"class":224}," 50\n",[207,1964,1965,1967,1970,1973,1975,1977,1979,1981,1983,1986,1988,1990,1993,1995,1998,2001,2003,2005,2008,2010,2012,2014,2016,2018,2021,2023],{"class":209,"line":339},[207,1966,1952],{"class":220},[207,1968,1969],{"class":224}," sorted",[207,1971,1972],{"class":231},"(o.result ",[207,1974,581],{"class":220},[207,1976,1049],{"class":231},[207,1978,587],{"class":220},[207,1980,1054],{"class":231},[207,1982,948],{"class":220},[207,1984,1985],{"class":231}," o.ok) ",[207,1987,1883],{"class":220},[207,1989,1969],{"class":224},[207,1991,1992],{"class":231},"(n ",[207,1994,490],{"class":220},[207,1996,1997],{"class":224}," 2",[207,1999,2000],{"class":220}," for",[207,2002,1880],{"class":231},[207,2004,587],{"class":220},[207,2006,2007],{"class":224}," range",[207,2009,351],{"class":231},[207,2011,1936],{"class":224},[207,2013,978],{"class":231},[207,2015,948],{"class":220},[207,2017,1880],{"class":231},[207,2019,2020],{"class":220},"!=",[207,2022,1886],{"class":224},[207,2024,316],{"class":231},[207,2026,2027,2030,2032,2034,2036,2038,2040,2042,2044,2046],{"class":209,"line":344},[207,2028,2029],{"class":231},"    [bad] ",[207,2031,306],{"class":220},[207,2033,1044],{"class":231},[207,2035,581],{"class":220},[207,2037,1049],{"class":231},[207,2039,587],{"class":220},[207,2041,1054],{"class":231},[207,2043,948],{"class":220},[207,2045,1059],{"class":220},[207,2047,2048],{"class":231}," o.ok]\n",[207,2050,2051,2053,2056,2058,2060,2063,2066,2069,2072],{"class":209,"line":365},[207,2052,1952],{"class":220},[207,2054,2055],{"class":231}," bad.item ",[207,2057,1883],{"class":220},[207,2059,1886],{"class":224},[207,2061,2062],{"class":220}," and",[207,2064,2065],{"class":224}," isinstance",[207,2067,2068],{"class":231},"(bad.error, ",[207,2070,2071],{"class":224},"ValueError",[207,2073,316],{"class":231},[42,2075,2077],{"id":2076},"key-takeaways","Key takeaways",[47,2079,2080,2083,2086,2092,2095,2098,2101],{},[50,2081,2082],{},"Pick threads or asyncio for waiting, processes for computing; start with a thread pool.",[50,2084,2085],{},"Put all concurrency inside one function that returns plain results; commands stay synchronous.",[50,2087,2088,2089,2091],{},"Bound concurrency, expose it as ",[17,2090,82],{},", and cap it at something sensible.",[50,2093,2094],{},"Capture per-item failures as values and report them together at the end.",[50,2096,2097],{},"Design for Ctrl+C from the start: cancel pending work, clean up running work, report, exit 130.",[50,2099,2100],{},"Print from one thread, prefer progress bars, and sort anything that goes to stdout.",[50,2102,2103],{},"Add a rate limiter before an API adds one for you.",[42,2105,2107],{"id":2106},"frequently-asked-questions","Frequently asked questions",[2109,2110,2112],"h3",{"id":2111},"should-i-make-my-whole-cli-async","Should I make my whole CLI async?",[10,2114,2115,2116,2118],{},"Usually not. Most commands are simpler as synchronous code, and async spreads: every function that calls an async one must itself be async. Use ",[17,2117,1654],{}," inside the few commands that benefit, and keep the rest synchronous.",[2109,2120,2122,2123,2125],{"id":2121},"is-httpxclient-safe-to-share-between-threads","Is ",[17,2124,130],{}," safe to share between threads?",[10,2127,2128,2129,2132],{},"Yes. httpx clients are thread-safe and share a connection pool, which is more efficient than one client per thread. Size the pool (",[17,2130,2131],{},"httpx.Limits(max_connections=...)",") to at least your worker count so threads do not queue for connections.",[2109,2134,2136,2137,114],{"id":2135},"how-do-i-choose-a-default-for-jobs","How do I choose a default for ",[17,2138,82],{},[10,2140,2141,2142,2145,2146,2149,2150,2153],{},"For network work, eight is a conservative, widely safe default. For CPU work, ",[17,2143,2144],{},"os.cpu_count()"," (or ",[17,2147,2148],{},"len(os.sched_getaffinity(0))"," on Linux containers, which respects CPU limits). Always allow users to override it, and consider ",[17,2151,2152],{},"--jobs 1"," as a debugging aid that makes output sequential.",[2109,2155,2157],{"id":2156},"why-is-my-concurrent-version-slower","Why is my concurrent version slower?",[10,2159,2160,2161,2163],{},"Common causes: CPU-bound work in threads (the GIL), a pool much larger than the server can handle, a shared lock held for too long, or per-task setup — a new HTTP client per item, for example — that outweighs the parallelism. Profile with ",[17,2162,2152],{}," and a few different sizes before assuming concurrency itself is at fault.",[2109,2165,2167],{"id":2166},"what-exit-code-should-a-batch-command-use-when-some-items-fail","What exit code should a batch command use when some items fail?",[10,2169,2170,2171,2174],{},"Decide it from the whole batch, after everything has finished: 0 when every item succeeded, 1 (or a documented code of your own) when any failed, and a separate code if nothing could run at all — for example because authentication failed before the first item. Print a one-line summary with the counts, list the failures on stderr, and offer ",[17,2172,2173],{},"--json"," output with per-item status so scripts can retry only what failed rather than parsing messages.",[2109,2176,2178],{"id":2177},"does-concurrency-affect-startup-time","Does concurrency affect startup time?",[10,2180,2181,2182,2185,2186,2188,2189,2192,2193,67],{},"Importing ",[17,2183,2184],{},"concurrent.futures"," is cheap; importing ",[17,2187,19],{}," adds a few milliseconds and large async libraries add more. Import them inside the commands that use them to keep ",[17,2190,2191],{},"--help"," fast, as in ",[22,2194,2196],{"href":2195},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","reducing CLI dependency weight",[42,2198,2200],{"id":2199},"related","Related",[47,2202,2203,2208,2213,2217,2221,2226,2230,2236],{},[50,2204,2205,2206],{},"Up: ",[22,2207,25],{"href":24},[50,2209,2210,2211],{},"Down: ",[22,2212,1592],{"href":1591},[50,2214,2210,2215],{},[22,2216,1268],{"href":1267},[50,2218,2210,2219],{},[22,2220,1610],{"href":1609},[50,2222,2210,2223],{},[22,2224,2225],{"href":1662},"Cancelling async tasks on Ctrl+C",[50,2227,2210,2228],{},[22,2229,1789],{"href":1788},[50,2231,2232,2233],{},"Sideways: ",[22,2234,2235],{"href":29},"Calling HTTP APIs from Python CLIs",[50,2237,2232,2238],{},[22,2239,2241],{"href":2240},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[2243,2244,2245],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html 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":203,"searchDepth":217,"depth":217,"links":2247},[2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2270],{"id":44,"depth":217,"text":45},{"id":103,"depth":217,"text":104},{"id":172,"depth":217,"text":173},{"id":185,"depth":217,"text":186},{"id":1272,"depth":217,"text":1273},{"id":1596,"depth":217,"text":1597},{"id":1614,"depth":217,"text":1615},{"id":1671,"depth":217,"text":1672},{"id":1710,"depth":217,"text":1711},{"id":1781,"depth":217,"text":1782},{"id":1793,"depth":217,"text":1794},{"id":2076,"depth":217,"text":2077},{"id":2106,"depth":217,"text":2107,"children":2261},[2262,2263,2265,2267,2268,2269],{"id":2111,"depth":235,"text":2112},{"id":2121,"depth":235,"text":2264},"Is httpx.Client safe to share between threads?",{"id":2135,"depth":235,"text":2266},"How do I choose a default for --jobs?",{"id":2156,"depth":235,"text":2157},{"id":2166,"depth":235,"text":2167},{"id":2177,"depth":235,"text":2178},{"id":2199,"depth":217,"text":2200},"2026-09-18","Make Python CLIs faster with threads, asyncio and process pools without losing control: choosing a model, bounding work, cancelling on Ctrl+C and rate limits.","advanced",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis",{"title":5,"description":2272},"cli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Findex",[2281,19,2282,2283,2284],"concurrency","threads","multiprocessing","performance","Dbj8UbX53oa2KCdGJpj-mKqB831L5-Nkp_gO2_4ZtPU",[2287,2290,2293,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2460,2463,2466,2469,2472,2473,2476,2479,2482,2485,2488,2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2559,2562,2565,2568,2571,2574,2577,2580,2583,2586,2589,2592,2595,2598,2601,2604,2607,2610,2613,2616,2619,2622,2625,2628,2631,2634,2637,2640,2643,2646,2649,2652,2655,2658,2661,2664,2667,2670,2673,2676,2679,2682,2685,2688,2691,2694,2697,2700,2703,2706,2709,2712,2715,2718,2721,2724,2727,2730,2733,2736,2739,2742,2745,2748,2751,2754,2757,2760,2763,2766,2769,2772,2775,2778,2781,2784,2787,2790,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829],{"path":2288,"title":2289},"\u002Fabout","About Python CLI Toolcraft",{"path":2291,"title":2292},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2294,"title":2295},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2297,"title":2298},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2300,"title":2301},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2303,"title":2304},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2306,"title":2307},"\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":2309,"title":2310},"\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":2312,"title":2313},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2315,"title":2316},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2318,"title":2319},"\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":2321,"title":2322},"\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":2324,"title":2325},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2327,"title":2328},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2330,"title":2331},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2333,"title":2334},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2336,"title":2337},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2339,"title":2340},"\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":2342,"title":2343},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2345,"title":2346},"\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":2348,"title":2349},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2351,"title":2352},"\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":2354,"title":2355},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2357,"title":2358},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2360,"title":2361},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2363,"title":2364},"\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":2366,"title":2367},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2369,"title":2370},"\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":2372,"title":2373},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2375,"title":2376},"\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":2378,"title":2379},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2381,"title":2382},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2384,"title":2385},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2387,"title":2388},"\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":2390,"title":2391},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2393,"title":2394},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2396,"title":2397},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2399,"title":2400},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2402,"title":2403},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2405,"title":2406},"\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":2408,"title":2409},"\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":2411,"title":2412},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2414,"title":2415},"\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":2417,"title":2418},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2420,"title":2421},"\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":2423,"title":2424},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2426,"title":2427},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2429,"title":2430},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2432,"title":2433},"\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":2435,"title":2436},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2438,"title":2439},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2441,"title":2442},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2444,"title":2445},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2447,"title":2448},"\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":2450,"title":2451},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2453,"title":2454},"\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":2456,"title":2457},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2459,"title":2235},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":2461,"title":2462},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2464,"title":2465},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2467,"title":2468},"\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":2470,"title":2471},"\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":2277,"title":5},{"path":2474,"title":2475},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2477,"title":2478},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2480,"title":2481},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2483,"title":2484},"\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":2486,"title":2487},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2489,"title":2490},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2492,"title":2493},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2495,"title":2496},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2498,"title":2499},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2501,"title":2502},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2504,"title":2505},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2507,"title":2508},"\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":2510,"title":2511},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2513,"title":2514},"\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":2516,"title":2517},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2519,"title":2520},"\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":2522,"title":2523},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2525,"title":2526},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2528,"title":2529},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2531,"title":2532},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2534,"title":2535},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2537,"title":2538},"\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":2540,"title":2541},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2543,"title":2544},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2546,"title":2547},"\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":2549,"title":2550},"\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":2552,"title":2553},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2555,"title":2556},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1167,"title":2558},"Python CLI Toolcraft",{"path":2560,"title":2561},"\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":2563,"title":2564},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2566,"title":2567},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2569,"title":2570},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2572,"title":2573},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2575,"title":2576},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2578,"title":2579},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2581,"title":2582},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2584,"title":2585},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2587,"title":2588},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2590,"title":2591},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2593,"title":2594},"\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":2596,"title":2597},"\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":2599,"title":2600},"\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":2602,"title":2603},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2605,"title":2606},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2608,"title":2609},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2611,"title":2612},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2614,"title":2615},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2617,"title":2618},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2620,"title":2621},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2623,"title":2624},"\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":2626,"title":2627},"\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":2629,"title":2630},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2632,"title":2633},"\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":2635,"title":2636},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2638,"title":2639},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2641,"title":2642},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2644,"title":2645},"\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":2647,"title":2648},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2650,"title":2651},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2653,"title":2654},"\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":2656,"title":2657},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2659,"title":2660},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2662,"title":2663},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2665,"title":2666},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2668,"title":2669},"\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":2671,"title":2672},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2674,"title":2675},"\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":2677,"title":2678},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2680,"title":2681},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2683,"title":2684},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2686,"title":2687},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2689,"title":2690},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2692,"title":2693},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2695,"title":2696},"\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":2698,"title":2699},"\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":2701,"title":2702},"\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":2704,"title":2705},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2707,"title":2708},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2710,"title":2711},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2713,"title":2714},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2716,"title":2717},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2719,"title":2720},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2722,"title":2723},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2725,"title":2726},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2728,"title":2729},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2731,"title":2732},"\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":2734,"title":2735},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2737,"title":2738},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2740,"title":2741},"\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":2743,"title":2744},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2746,"title":2747},"\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":2749,"title":2750},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2752,"title":2753},"\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":2755,"title":2756},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2758,"title":2759},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2761,"title":2762},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2764,"title":2765},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2767,"title":2768},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2770,"title":2771},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2773,"title":2774},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2776,"title":2777},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2779,"title":2780},"\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":2782,"title":2783},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2785,"title":2786},"\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":2788,"title":2789},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2791,"title":2792},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2794,"title":2795},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2797,"title":2798},"\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":2800,"title":2801},"\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":2803,"title":2804},"\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":2806,"title":2807},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2809,"title":2810},"\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":2812,"title":2813},"\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":2815,"title":2816},"\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":2818,"title":2819},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2821,"title":2822},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2824,"title":2825},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2827,"title":2828},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2830,"title":2831},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905048]