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