[{"data":1,"prerenderedAt":2035},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002F":3,"content-directory":1489},{"id":4,"title":5,"body":6,"date":1473,"description":1474,"difficulty":1475,"draft":1476,"extension":1477,"meta":1478,"navigation":278,"path":1479,"seo":1480,"stem":1481,"tags":1482,"updated":1473,"__hash__":1488},"content\u002Fcli-runtime-systems-integration\u002Findex.md","CLI Runtime & Systems Integration for Python",{"type":7,"value":8,"toc":1451},"minimark",[9,13,16,20,25,28,101,105,108,111,149,155,178,181,197,201,224,251,415,451,455,470,665,699,703,722,741,764,768,775,794,825,829,832,847,870,874,887,914,918,921,960,1189,1204,1211,1215,1218,1280,1284,1287,1290,1293,1297,1332,1336,1341,1344,1348,1357,1361,1374,1378,1389,1393,1403,1407,1447],[10,11,12],"p",{},"The other tracks on this site are mostly about the front of a command-line tool: how it is packaged and installed, how its commands are structured, how it parses input and presents output. This track is about the back — everything that happens after the arguments have been parsed and the tool starts doing its job. A real CLI runs other programs, reads and writes files it must not corrupt, calls web APIs that are sometimes slow or down, does several things at once, handles credentials that must never leak, and sometimes runs for hours under a supervisor or on a schedule. Each of those is a boundary between your Python code and the rest of the system, and each has its own failure modes that a quick manual test will never reveal.",[10,14,15],{},"It is written for the people who build internal tools, DevOps automation and data tooling in Python: tools that deploy things, sync things, migrate things and check things. Those tools tend to start as a script, gain users, and then fail in production in ways that come down to the same handful of mistakes — a command string handed to a shell, a file overwritten in place, a request with no timeout, a thread pool that ignores Ctrl+C, a token printed in a debug log, a cron job that silently stopped running. The six topics below take each boundary in turn and show the patterns that make it dependable.",[17,18],"inline-diagram",{"name":19},"rt-section-map",[21,22,24],"h2",{"id":23},"what-you-will-learn","What you will learn",[10,26,27],{},"This section is organised into six topics, each with a detailed overview and four or five in-depth guides with runnable, tested code:",[29,30,31,43,57,70,79,88],"ul",{},[32,33,34,42],"li",{},[35,36,37],"strong",{},[38,39,41],"a",{"href":40},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","Running subprocesses from Python CLIs"," — calling external programs safely with argument lists, streaming their output, enforcing timeouts that kill whole process trees, translating their failures into your own exit codes, wrapping tools like git in typed modules, and closing the shell-injection hole.",[32,44,45,51,52,56],{},[35,46,47],{},[38,48,50],{"href":49},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","Filesystem paths and atomic writes"," — writing files that cannot be left half-written, using ",[53,54,55],"code",{},"pathlib"," so paths work on every platform, putting your tool's config, cache and state where the operating system expects them, creating temporary files safely, and locking against concurrent runs.",[32,58,59,65,66,69],{},[35,60,61],{},[38,62,64],{"href":63},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002F","Calling HTTP APIs from Python CLIs"," — building an API client module over ",[53,67,68],{},"httpx",", choosing timeouts, retrying with backoff only when it is safe, paginating without silently truncating, logging in with the OAuth device flow, and downloading large files with progress and resume.",[32,71,72,78],{},[35,73,74],{},[38,75,77],{"href":76},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002F","Concurrency and async in Python CLIs"," — choosing between threads, asyncio and processes, bounding concurrency, running async code from Typer and Click, cancelling everything cleanly on Ctrl+C, and staying inside an API's rate limits.",[32,80,81,87],{},[35,82,83],{},[38,84,86],{"href":85},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","Secrets and credentials in Python CLIs"," — storing tokens in the system keychain, accepting secrets from environment variables and mounted files, prompting securely, redacting secrets from logs and tracebacks, and supporting several accounts through profiles.",[32,89,90,96,97,100],{},[35,91,92],{},[38,93,95],{"href":94},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002F","Long-running and watch-mode CLIs"," — shutting down gracefully on ",[53,98,99],{},"SIGTERM",", building a debounced watch mode, running reliably from cron and systemd timers, and exposing health so that stalled processes and missed runs get noticed.",[21,102,104],{"id":103},"one-pattern-six-boundaries","One pattern, six boundaries",[10,106,107],{},"Before the topics, one idea that runs through all of them. Every boundary in this section — a child process, the filesystem, the network, the credential store, the operating system's signals — deserves the same treatment, and applying it consistently is what keeps a growing CLI understandable.",[17,109],{"name":110},"rt-boundaries",[10,112,113,116,117,120,121,124,125,128,129,132,133,136,137,140,141,144,145,148],{},[35,114,115],{},"Wrap each boundary in one module."," All calls to git go through ",[53,118,119],{},"git.py","; all HTTP calls go through ",[53,122,123],{},"api.py","; all writes of important files go through ",[53,126,127],{},"files.py","; all credential lookups go through ",[53,130,131],{},"credentials.py",". Commands never call ",[53,134,135],{},"subprocess.run",", ",[53,138,139],{},"httpx.get"," or ",[53,142,143],{},"open(path, \"w\")"," directly. That one rule has three payoffs. Fixes apply everywhere at once — when you discover that a child needs ",[53,146,147],{},"LC_ALL=C"," or that a request needs a longer read timeout, you change one line. Tests have exactly one seam to replace per boundary, so command tests run offline and in milliseconds. And the command layer reads like the requirements rather than like plumbing.",[10,150,151,154],{},[35,152,153],{},"Give every boundary a limit."," Subprocesses get timeouts; HTTP requests get connect and read timeouts; concurrent work gets a pool size and a rate limit; long-running loops get a grace period for shutdown. An operation without a limit is a potential hang, and a CLI that hangs is worse than one that fails, because a failure at least says something.",[10,156,157,160,161,165,166,169,170,173,174,177],{},[35,158,159],{},"Translate failures into one error type per module, then into exit codes."," The user asked to run ",[162,163,164],"em",{},"your"," command. A ",[53,167,168],{},"CalledProcessError",", an ",[53,171,172],{},"httpx.ConnectError"," or a ",[53,175,176],{},"FileNotFoundError"," from deep inside is an implementation detail; what they need is a sentence explaining what went wrong and what to do, and scripts need an exit code that distinguishes \"try again later\" from \"fix your configuration\".",[17,179],{"name":180},"rt-failure-budget",[10,182,183,184,187,188,191,192,196],{},"The codes in that table are conventions other tools already use: 124 for timeouts (from ",[53,185,186],{},"timeout(1)","), 127 for missing programs (from shells), 69 and 75 from ",[53,189,190],{},"sysexits.h"," for \"service unavailable\" and \"temporary failure\", and 128 plus the signal number for signals. Using them means a script wrapping your tool can react sensibly without reading your documentation. The general theory is in ",[38,193,195],{"href":194},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",".",[21,198,200],{"id":199},"running-other-programs","Running other programs",[10,202,203,204,136,207,136,210,136,213,136,216,219,220,223],{},"Almost every automation CLI eventually shells out: to ",[53,205,206],{},"git",[53,208,209],{},"rsync",[53,211,212],{},"docker",[53,214,215],{},"terraform",[53,217,218],{},"kubectl",", a compiler or a linter. The ",[53,221,222],{},"subprocess"," module makes it easy to start a program and surprisingly easy to get the details wrong.",[10,225,226,227,230,231,234,235,238,239,242,243,246,247,250],{},"The most important rule is to pass ",[35,228,229],{},"argument lists, not command strings",". ",[53,232,233],{},"subprocess.run([\"git\", \"log\", \"-n\", str(n)])"," executes git directly with exactly those arguments; ",[53,236,237],{},"subprocess.run(f\"git log -n {n}\", shell=True)"," hands a string to ",[53,240,241],{},"\u002Fbin\u002Fsh",", which will interpret any semicolon, backtick or ",[53,244,245],{},"$(...)"," that ends up in it. Argument lists fix the security problem and the everyday bug — paths with spaces — at the same time. The second rule is to decide every behaviour on purpose: check the exit code (",[53,248,249],{},"check=True","), decode output with an explicit encoding, set a timeout, and close stdin for children that should not prompt.",[252,253,258],"pre",{"className":254,"code":255,"language":256,"meta":257,"style":257},"language-python shiki shiki-themes github-light github-dark","import subprocess\n\n\ndef git(*args: str) -> str:\n    return subprocess.run(\n        [\"git\", *args], check=True, capture_output=True, text=True,\n        encoding=\"utf-8\", timeout=60, stdin=subprocess.DEVNULL,\n    ).stdout.strip()\n","python","",[53,259,260,273,280,285,316,325,373,409],{"__ignoreMap":257},[261,262,265,269],"span",{"class":263,"line":264},"line",1,[261,266,268],{"class":267},"szBVR","import",[261,270,272],{"class":271},"sVt8B"," subprocess\n",[261,274,276],{"class":263,"line":275},2,[261,277,279],{"emptyLinePlaceholder":278},true,"\n",[261,281,283],{"class":263,"line":282},3,[261,284,279],{"emptyLinePlaceholder":278},[261,286,288,291,295,298,301,304,308,311,313],{"class":263,"line":287},4,[261,289,290],{"class":267},"def",[261,292,294],{"class":293},"sScJk"," git",[261,296,297],{"class":271},"(",[261,299,300],{"class":267},"*",[261,302,303],{"class":271},"args: ",[261,305,307],{"class":306},"sj4cs","str",[261,309,310],{"class":271},") -> ",[261,312,307],{"class":306},[261,314,315],{"class":271},":\n",[261,317,319,322],{"class":263,"line":318},5,[261,320,321],{"class":267},"    return",[261,323,324],{"class":271}," subprocess.run(\n",[261,326,328,331,335,337,339,342,346,349,352,354,357,359,361,363,366,368,370],{"class":263,"line":327},6,[261,329,330],{"class":271},"        [",[261,332,334],{"class":333},"sZZnC","\"git\"",[261,336,136],{"class":271},[261,338,300],{"class":267},[261,340,341],{"class":271},"args], ",[261,343,345],{"class":344},"s4XuR","check",[261,347,348],{"class":267},"=",[261,350,351],{"class":306},"True",[261,353,136],{"class":271},[261,355,356],{"class":344},"capture_output",[261,358,348],{"class":267},[261,360,351],{"class":306},[261,362,136],{"class":271},[261,364,365],{"class":344},"text",[261,367,348],{"class":267},[261,369,351],{"class":306},[261,371,372],{"class":271},",\n",[261,374,376,379,381,384,386,389,391,394,396,399,401,404,407],{"class":263,"line":375},7,[261,377,378],{"class":344},"        encoding",[261,380,348],{"class":267},[261,382,383],{"class":333},"\"utf-8\"",[261,385,136],{"class":271},[261,387,388],{"class":344},"timeout",[261,390,348],{"class":267},[261,392,393],{"class":306},"60",[261,395,136],{"class":271},[261,397,398],{"class":344},"stdin",[261,400,348],{"class":267},[261,402,403],{"class":271},"subprocess.",[261,405,406],{"class":306},"DEVNULL",[261,408,372],{"class":271},[261,410,412],{"class":263,"line":411},8,[261,413,414],{"class":271},"    ).stdout.strip()\n",[10,416,417,418,421,422,425,426,430,431,435,436,440,441,445,446,450],{},"Beyond the basics, the subprocess topic covers the problems that appear as tools mature: output that arrives in bursts because the child buffers when writing to a pipe, deadlocks when reading two pipes naively, timeouts that kill ",[53,419,420],{},"npm"," but leave its ",[53,423,424],{},"node"," children running, and signal deaths reported as negative return codes. Start with ",[38,427,429],{"href":428},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","calling external commands safely with subprocess",", then ",[38,432,434],{"href":433},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time\u002F","streaming subprocess output in real time"," and ",[38,437,439],{"href":438},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes\u002F","handling subprocess timeouts and exit codes",". If your tool calls one program heavily, ",[38,442,444],{"href":443},"\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"," shows how to turn it into a small typed library, and ",[38,447,449],{"href":448},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis\u002F","avoiding shell injection in Python CLIs"," covers the security layer in depth.",[21,452,454],{"id":453},"files-that-survive-crashes-and-platforms","Files that survive crashes and platforms",[10,456,457,458,461,462,465,466,469],{},"A CLI's worst bugs are the ones that damage data, and file handling is where that happens. Opening a file with ",[53,459,460],{},"\"w\""," truncates it immediately; a crash, a Ctrl+C or a full disk before the write completes leaves it empty. The fix is the atomic write: write to a temporary file in the same directory, flush and ",[53,463,464],{},"fsync"," it, then ",[53,467,468],{},"os.replace"," it over the original, so readers only ever see the complete old version or the complete new one.",[252,471,473],{"className":254,"code":472,"language":256,"meta":257,"style":257},"import os\nimport tempfile\nfrom pathlib import Path\n\n\ndef write_atomic(path: Path, text: str) -> None:\n    fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f\".{path.name}.\")\n    try:\n        with os.fdopen(fd, \"w\", encoding=\"utf-8\") as fh:\n            fh.write(text)\n            fh.flush()\n            os.fsync(fh.fileno())\n        os.replace(tmp, path)\n    except BaseException:\n        Path(tmp).unlink(missing_ok=True)\n        raise\n",[53,474,475,482,489,502,506,510,529,573,580,609,615,621,627,633,644,659],{"__ignoreMap":257},[261,476,477,479],{"class":263,"line":264},[261,478,268],{"class":267},[261,480,481],{"class":271}," os\n",[261,483,484,486],{"class":263,"line":275},[261,485,268],{"class":267},[261,487,488],{"class":271}," tempfile\n",[261,490,491,494,497,499],{"class":263,"line":282},[261,492,493],{"class":267},"from",[261,495,496],{"class":271}," pathlib ",[261,498,268],{"class":267},[261,500,501],{"class":271}," Path\n",[261,503,504],{"class":263,"line":287},[261,505,279],{"emptyLinePlaceholder":278},[261,507,508],{"class":263,"line":318},[261,509,279],{"emptyLinePlaceholder":278},[261,511,512,514,517,520,522,524,527],{"class":263,"line":327},[261,513,290],{"class":267},[261,515,516],{"class":293}," write_atomic",[261,518,519],{"class":271},"(path: Path, text: ",[261,521,307],{"class":306},[261,523,310],{"class":271},[261,525,526],{"class":306},"None",[261,528,315],{"class":271},[261,530,531,534,536,539,542,544,547,550,552,555,558,561,564,567,570],{"class":263,"line":375},[261,532,533],{"class":271},"    fd, tmp ",[261,535,348],{"class":267},[261,537,538],{"class":271}," tempfile.mkstemp(",[261,540,541],{"class":344},"dir",[261,543,348],{"class":267},[261,545,546],{"class":271},"path.parent, ",[261,548,549],{"class":344},"prefix",[261,551,348],{"class":267},[261,553,554],{"class":267},"f",[261,556,557],{"class":333},"\".",[261,559,560],{"class":306},"{",[261,562,563],{"class":271},"path.name",[261,565,566],{"class":306},"}",[261,568,569],{"class":333},".\"",[261,571,572],{"class":271},")\n",[261,574,575,578],{"class":263,"line":411},[261,576,577],{"class":267},"    try",[261,579,315],{"class":271},[261,581,583,586,589,591,593,596,598,600,603,606],{"class":263,"line":582},9,[261,584,585],{"class":267},"        with",[261,587,588],{"class":271}," os.fdopen(fd, ",[261,590,460],{"class":333},[261,592,136],{"class":271},[261,594,595],{"class":344},"encoding",[261,597,348],{"class":267},[261,599,383],{"class":333},[261,601,602],{"class":271},") ",[261,604,605],{"class":267},"as",[261,607,608],{"class":271}," fh:\n",[261,610,612],{"class":263,"line":611},10,[261,613,614],{"class":271},"            fh.write(text)\n",[261,616,618],{"class":263,"line":617},11,[261,619,620],{"class":271},"            fh.flush()\n",[261,622,624],{"class":263,"line":623},12,[261,625,626],{"class":271},"            os.fsync(fh.fileno())\n",[261,628,630],{"class":263,"line":629},13,[261,631,632],{"class":271},"        os.replace(tmp, path)\n",[261,634,636,639,642],{"class":263,"line":635},14,[261,637,638],{"class":267},"    except",[261,640,641],{"class":306}," BaseException",[261,643,315],{"class":271},[261,645,647,650,653,655,657],{"class":263,"line":646},15,[261,648,649],{"class":271},"        Path(tmp).unlink(",[261,651,652],{"class":344},"missing_ok",[261,654,348],{"class":267},[261,656,351],{"class":306},[261,658,572],{"class":271},[261,660,662],{"class":263,"line":661},16,[261,663,664],{"class":267},"        raise\n",[10,666,667,668,670,671,674,675,678,679,136,683,136,687,136,691,435,695,196],{},"The rest of the filesystem topic deals with portability and placement. ",[53,669,55],{}," removes a whole family of platform bugs caused by treating paths as strings. ",[53,672,673],{},"platformdirs"," puts your tool's own files — config the user edits, state the tool keeps, caches it can rebuild — in the directories Linux, macOS and Windows each designate for them, instead of scattering dot-files across the home directory or the user's projects. ",[53,676,677],{},"tempfile"," creates scratch space with unpredictable names that cleans up after itself, even on Ctrl+C. And when two runs of your tool can touch the same state — cron overlapping with itself, two terminals, a CI matrix — an operating-system lock prevents lost updates that atomic writes alone cannot. The guides are ",[38,680,682],{"href":681},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","writing files atomically in Python CLIs",[38,684,686],{"href":685},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F","cross-platform paths with pathlib",[38,688,690],{"href":689},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs",[38,692,694],{"href":693},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories\u002F","safe temporary files and directories",[38,696,698],{"href":697},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002F","file locking for concurrent CLI runs",[21,700,702],{"id":701},"talking-to-web-apis","Talking to web APIs",[10,704,705,706,713,714,717,718,721],{},"A large share of internal CLIs are front ends to an API. The patterns that keep them dependable are straightforward once named. Create ",[35,707,708,709,712],{},"one ",[53,710,711],{},"httpx.Client"," per run"," — configured with a base URL, authentication, timeouts and a ",[53,715,716],{},"User-Agent"," that includes your tool's version — and reuse it so connections are pooled. Put every endpoint behind a function in an API module that returns typed objects and raises one error type, so commands can render tables or JSON without knowing HTTP exists. Set ",[35,719,720],{},"connect and read timeouts deliberately",", because a request without one is a potential hang.",[10,723,724,725,728,729,732,733,736,737,740],{},"When things go wrong, respond according to who caused it. Client errors (4xx) are reported, clearly, using the server's own message where it helps. Server errors, rate limits and network failures are retried — a few times, with capped exponential backoff and jitter, honouring ",[53,726,727],{},"Retry-After",", and only for requests that are safe to repeat. Lists are fetched with generators that follow the API's pagination, so a ",[53,730,731],{},"--limit"," stops early and ",[53,734,735],{},"--all"," streams rather than collecting everything first. For authentication, the OAuth device flow gives a CLI SSO-compatible login without ever handling a password. And large downloads stream to a partial file with a progress bar, verify a checksum and rename into place, resuming with a ",[53,738,739],{},"Range"," request if interrupted.",[10,742,743,744,136,748,136,752,136,756,435,760,196],{},"Each of those has a guide: ",[38,745,747],{"href":746},"\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",[38,749,751],{"href":750},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls\u002F","retries and backoff for CLI HTTP calls",[38,753,755],{"href":754},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli\u002F","paginating API results in a CLI",[38,757,759],{"href":758},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis\u002F","OAuth device flow login for CLIs",[38,761,763],{"href":762},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002F","downloading files with progress in Python",[21,765,767],{"id":766},"doing-several-things-at-once","Doing several things at once",[10,769,770,771,774],{},"Concurrency is the most effective performance tool a CLI has — checking 300 URLs with sixteen workers is roughly sixteen times faster than one at a time — and the one most likely to make it misbehave. The concurrency topic starts from a simple choice: ",[35,772,773],{},"threads or asyncio for work that waits, processes for work that computes",". Standard CPython's global interpreter lock means threads do not speed up pure-Python computation, but they are excellent for overlapping network and disk waits, and they let you keep existing synchronous code.",[10,776,777,778,781,782,785,786,789,790,793],{},"Three habits make concurrent commands safe. Keep all concurrency ",[35,779,780],{},"inside one function"," that returns ordinary results, so commands stay synchronous and testable. ",[35,783,784],{},"Bound it"," with a pool size or semaphore exposed as ",[53,787,788],{},"--jobs",". And ",[35,791,792],{},"collect failures as values"," — one bad item should not sink a batch of three hundred — reporting them together at the end with an exit code that reflects the whole run.",[10,795,796,797,800,801,804,805,136,809,136,813,136,817,435,821,196],{},"Stopping is where concurrent CLIs most often disappoint. Ctrl+C should cancel queued work, let running work clean up, report what was done and exit with 130; with asyncio, modern Python turns the first Ctrl+C into cancellation that propagates through every task in a ",[53,798,799],{},"TaskGroup",", provided your code does not swallow ",[53,802,803],{},"CancelledError",". And when concurrency meets a rate-limited API, a token bucket keeps you under the quota instead of provoking a stream of 429 responses. The guides: ",[38,806,808],{"href":807},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click\u002F","running async code in Typer and Click",[38,810,812],{"href":811},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","parallelising CLI work with thread pools",[38,814,816],{"href":815},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks\u002F","multiprocessing for CPU-bound CLI tasks",[38,818,820],{"href":819},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c\u002F","cancelling async tasks on Ctrl+C",[38,822,824],{"href":823},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis\u002F","rate-limiting concurrent requests in CLIs",[21,826,828],{"id":827},"keeping-credentials-secret","Keeping credentials secret",[10,830,831],{},"CLIs handle powerful credentials and have more ways to leak them than most software. Command-line arguments are visible to every user on the machine and saved in shell history; config files get backed up and published in dotfile repositories; debug logs and tracebacks get pasted into issue trackers; environment variables are inherited by every child process. The secrets topic addresses each channel.",[10,833,834,835,838,839,842,843,846],{},"For people, tokens belong in the operating system's keychain via ",[53,836,837],{},"keyring",", obtained through ",[53,840,841],{},"mytool auth login"," from a hidden prompt, stdin or a browser-based device flow, never from an argument. For automation, credentials come from environment variables and — better, for containers — mounted files, using the ",[53,844,845],{},"_FILE"," convention operators already know. Inside the tool, secrets are wrapped in a type whose string form is masked, so accidental printing is harmless, and a redacting logging filter masks registered values and common token patterns in everything written to stderr or log files, including third-party library output and tracebacks. For people with several accounts, named profiles keep settings in config and each account's token in its own keychain entry, with the active profile always visible and production guarded by confirmation.",[10,848,849,850,136,854,136,858,136,862,435,866,196],{},"The guides are ",[38,851,853],{"href":852},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring\u002F","storing tokens with keyring",[38,855,857],{"href":856},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F","reading secrets from env and files",[38,859,861],{"href":860},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002F","prompting for passwords securely",[38,863,865],{"href":864},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002F","redacting secrets from CLI output and logs",[38,867,869],{"href":868},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts\u002F","supporting multiple profiles and accounts",[21,871,873],{"id":872},"commands-that-keep-running","Commands that keep running",[10,875,876,877,879,880,882,883,886],{},"Finally, some commands do not exit after a second: watch modes, workers under systemd or Kubernetes, and jobs run nightly from a scheduler. They are stopped by signals rather than by finishing — and Python's default response to ",[53,878,99],{},", the signal every supervisor sends, is to die immediately without running any cleanup. The long-running topic shows how to treat ",[53,881,99],{}," like Ctrl+C, structure loops around bounded units of work with interruptible waits, exit with the conventional ",[53,884,885],{},"128 + signal"," codes, and remember that as PID 1 in a container you get no default signal handling at all.",[10,888,889,890,893,894,897,898,136,902,136,906,435,910,196],{},"For developers, a good ",[53,891,892],{},"--watch"," flag debounces file events into one rebuild per save, ignores its own output directory so it cannot loop, and keeps watching after a failed build. For scheduled runs, commands must be non-interactive, idempotent, overlap-safe and explicit about paths, and systemd timers add logging, catch-up of missed runs and overlap prevention on top of cron. And because a process that exists is not necessarily a process that works, a heartbeat written after each unit of work plus a ",[53,895,896],{},"health"," command gives probes and people a way to tell the difference, while a dead-man switch catches the scheduled job that silently stopped running. The guides: ",[38,899,901],{"href":900},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown\u002F","handling SIGTERM and graceful shutdown",[38,903,905],{"href":904},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles\u002F","building a watch mode with watchfiles",[38,907,909],{"href":908},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd\u002F","running a CLI on a schedule with cron and systemd",[38,911,913],{"href":912},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis\u002F","health checks and heartbeats for long-running CLIs",[21,915,917],{"id":916},"testing-code-that-touches-the-system","Testing code that touches the system",[10,919,920],{},"Code at these boundaries has a reputation for being hard to test, and it is — if the boundary calls are scattered through the commands. With each boundary wrapped in one module, testing splits cleanly into two kinds.",[10,922,923,926,927,136,930,435,933,936,937,940,941,944,945,948,949,952,953,435,956,959],{},[35,924,925],{},"Unit tests replace the boundary."," A command that deploys by calling ",[53,928,929],{},"git.current_branch()",[53,931,932],{},"api.list_builds()",[53,934,935],{},"files.write_atomic()"," can be tested by substituting fakes for those three functions and asserting on what the command did with them. No subprocess runs, no network is touched, no real file is written, and the test takes milliseconds. ",[53,938,939],{},"httpx.MockTransport"," replaces the network below your API module; ",[53,942,943],{},"monkeypatch.setattr"," replaces a runner function; ",[53,946,947],{},"tmp_path"," gives every test a private directory; ",[53,950,951],{},"keyring.set_keyring()"," installs an in-memory credential store; and injected ",[53,954,955],{},"clock",[53,957,958],{},"sleep"," functions make retry, rate-limit and timeout logic deterministic.",[252,961,963],{"className":254,"code":962,"language":256,"meta":257,"style":257},"import httpx\nfrom typer.testing import CliRunner\n\nfrom mytool import cli\nfrom mytool.api import make_client\n\n\ndef test_status_command_offline(monkeypatch, tmp_path):\n    def handler(request: httpx.Request) -> httpx.Response:\n        return httpx.Response(200, json=[{\"name\": \"web\", \"owner\": \"ana\", \"build_count\": 3}])\n\n    monkeypatch.setattr(cli, \"client_factory\",\n                        lambda url, token: make_client(url, token, transport=httpx.MockTransport(handler)))\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"test-token\")\n    monkeypatch.setenv(\"MYTOOL_STATE_DIR\", str(tmp_path))\n    result = CliRunner().invoke(cli.app, [\"projects\", \"--json\"])\n    assert result.exit_code == 0\n",[53,964,965,972,984,988,1000,1012,1016,1020,1030,1041,1094,1098,1108,1124,1139,1153,1174],{"__ignoreMap":257},[261,966,967,969],{"class":263,"line":264},[261,968,268],{"class":267},[261,970,971],{"class":271}," httpx\n",[261,973,974,976,979,981],{"class":263,"line":275},[261,975,493],{"class":267},[261,977,978],{"class":271}," typer.testing ",[261,980,268],{"class":267},[261,982,983],{"class":271}," CliRunner\n",[261,985,986],{"class":263,"line":282},[261,987,279],{"emptyLinePlaceholder":278},[261,989,990,992,995,997],{"class":263,"line":287},[261,991,493],{"class":267},[261,993,994],{"class":271}," mytool ",[261,996,268],{"class":267},[261,998,999],{"class":271}," cli\n",[261,1001,1002,1004,1007,1009],{"class":263,"line":318},[261,1003,493],{"class":267},[261,1005,1006],{"class":271}," mytool.api ",[261,1008,268],{"class":267},[261,1010,1011],{"class":271}," make_client\n",[261,1013,1014],{"class":263,"line":327},[261,1015,279],{"emptyLinePlaceholder":278},[261,1017,1018],{"class":263,"line":375},[261,1019,279],{"emptyLinePlaceholder":278},[261,1021,1022,1024,1027],{"class":263,"line":411},[261,1023,290],{"class":267},[261,1025,1026],{"class":293}," test_status_command_offline",[261,1028,1029],{"class":271},"(monkeypatch, tmp_path):\n",[261,1031,1032,1035,1038],{"class":263,"line":582},[261,1033,1034],{"class":267},"    def",[261,1036,1037],{"class":293}," handler",[261,1039,1040],{"class":271},"(request: httpx.Request) -> httpx.Response:\n",[261,1042,1043,1046,1049,1052,1054,1057,1059,1062,1065,1068,1071,1073,1076,1078,1081,1083,1086,1088,1091],{"class":263,"line":611},[261,1044,1045],{"class":267},"        return",[261,1047,1048],{"class":271}," httpx.Response(",[261,1050,1051],{"class":306},"200",[261,1053,136],{"class":271},[261,1055,1056],{"class":344},"json",[261,1058,348],{"class":267},[261,1060,1061],{"class":271},"[{",[261,1063,1064],{"class":333},"\"name\"",[261,1066,1067],{"class":271},": ",[261,1069,1070],{"class":333},"\"web\"",[261,1072,136],{"class":271},[261,1074,1075],{"class":333},"\"owner\"",[261,1077,1067],{"class":271},[261,1079,1080],{"class":333},"\"ana\"",[261,1082,136],{"class":271},[261,1084,1085],{"class":333},"\"build_count\"",[261,1087,1067],{"class":271},[261,1089,1090],{"class":306},"3",[261,1092,1093],{"class":271},"}])\n",[261,1095,1096],{"class":263,"line":617},[261,1097,279],{"emptyLinePlaceholder":278},[261,1099,1100,1103,1106],{"class":263,"line":623},[261,1101,1102],{"class":271},"    monkeypatch.setattr(cli, ",[261,1104,1105],{"class":333},"\"client_factory\"",[261,1107,372],{"class":271},[261,1109,1110,1113,1116,1119,1121],{"class":263,"line":629},[261,1111,1112],{"class":267},"                        lambda",[261,1114,1115],{"class":271}," url, token: make_client(url, token, ",[261,1117,1118],{"class":344},"transport",[261,1120,348],{"class":267},[261,1122,1123],{"class":271},"httpx.MockTransport(handler)))\n",[261,1125,1126,1129,1132,1134,1137],{"class":263,"line":635},[261,1127,1128],{"class":271},"    monkeypatch.setenv(",[261,1130,1131],{"class":333},"\"MYTOOL_TOKEN\"",[261,1133,136],{"class":271},[261,1135,1136],{"class":333},"\"test-token\"",[261,1138,572],{"class":271},[261,1140,1141,1143,1146,1148,1150],{"class":263,"line":646},[261,1142,1128],{"class":271},[261,1144,1145],{"class":333},"\"MYTOOL_STATE_DIR\"",[261,1147,136],{"class":271},[261,1149,307],{"class":306},[261,1151,1152],{"class":271},"(tmp_path))\n",[261,1154,1155,1158,1160,1163,1166,1168,1171],{"class":263,"line":661},[261,1156,1157],{"class":271},"    result ",[261,1159,348],{"class":267},[261,1161,1162],{"class":271}," CliRunner().invoke(cli.app, [",[261,1164,1165],{"class":333},"\"projects\"",[261,1167,136],{"class":271},[261,1169,1170],{"class":333},"\"--json\"",[261,1172,1173],{"class":271},"])\n",[261,1175,1177,1180,1183,1186],{"class":263,"line":1176},17,[261,1178,1179],{"class":267},"    assert",[261,1181,1182],{"class":271}," result.exit_code ",[261,1184,1185],{"class":267},"==",[261,1187,1188],{"class":306}," 0\n",[10,1190,1191,1194,1195,1197,1198,1200,1201,1203],{},[35,1192,1193],{},"A few integration tests use the real thing."," Parsing git's porcelain output should be tested against a real repository created in ",[53,1196,947],{},"; signal handling should be tested by sending a real ",[53,1199,99],{}," to a real subprocess; a watch mode should see a real file change. Keep these few, fast and clearly marked, so they can be skipped where the tool or platform is unavailable — a Windows runner has no ",[53,1202,99],{},", a slim container may have no git.",[10,1205,1206,1207,196],{},"Each guide in this section ends with a \"Testing the behaviour\" section in exactly this spirit, and the general techniques are collected in ",[38,1208,1210],{"href":1209},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","mocking filesystem and network in CLI tests",[21,1212,1214],{"id":1213},"how-this-fits-with-the-rest-of-the-site","How this fits with the rest of the site",[10,1216,1217],{},"This section assumes a CLI that is already well structured and well packaged, and it connects back to the other tracks at many points:",[29,1219,1220,1236,1255,1265],{},[32,1221,1222,1223,1226,1227,1231,1232,196],{},"The ",[35,1224,1225],{},"command structure"," these patterns plug into — thin commands, shared state on the Click context, dependency injection at the edge — is covered in ",[38,1228,1230],{"href":1229},"\u002Fmodern-python-cli-frameworks-architecture\u002F","Modern Python CLI Frameworks & Architecture",", especially ",[38,1233,1235],{"href":1234},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands\u002F","dependency injection patterns for CLI commands",[32,1237,1222,1238,1241,1242,1246,1247,435,1251,196],{},[35,1239,1240],{},"user-facing side"," of every failure described here — messages, exit codes, logging, TTY detection — lives in ",[38,1243,1245],{"href":1244},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing & User Experience",", particularly ",[38,1248,1250],{"href":1249},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","error handling and exit codes",[38,1252,1254],{"href":1253},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging for CLI apps",[32,1256,1257,1260,1261,196],{},[35,1258,1259],{},"Testing"," the boundaries without touching the real system — mock transports, temporary directories, in-memory keyrings, fake clocks — builds on ",[38,1262,1264],{"href":1263},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002F","testing Python CLI applications",[32,1266,1267,1270,1271,1275,1276,196],{},[35,1268,1269],{},"Installing"," the tool so schedulers and supervisors can find it at a stable absolute path is covered in ",[38,1272,1274],{"href":1273},"\u002Fproject-setup-dependency-management\u002F","Project Setup & Dependency Management",", including ",[38,1277,1279],{"href":1278},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx\u002F","installing and distributing CLIs with pipx",[21,1281,1283],{"id":1282},"a-suggested-path-through-the-section","A suggested path through the section",[10,1285,1286],{},"If you are reading the section as a whole rather than looking for one answer, this order builds each habit on the one before:",[17,1288],{"name":1289},"rt-learning-path",[10,1291,1292],{},"Start with subprocesses, because argument lists, explicit timeouts and error translation are the simplest form of the boundary pattern. Files come next, because atomic writes and locks are needed by nearly everything after. HTTP builds the full client-module pattern. Concurrency multiplies whatever the HTTP and subprocess layers do, so it helps to have those solid first. Secrets cut across everything, and long-running commands pull every previous topic together — signals, locks, retries, credentials from the environment and health reporting.",[21,1294,1296],{"id":1295},"key-takeaways","Key takeaways",[29,1298,1299,1302,1308,1314,1320,1323,1326],{},[32,1300,1301],{},"Every boundary your CLI crosses — processes, files, network, credentials, signals — deserves one wrapping module, a limit, and one error type mapped to a meaningful exit code.",[32,1303,1304,1305,1307],{},"Pass argument lists to ",[53,1306,222],{},", never command strings, and set timeouts that stop whole process trees.",[32,1309,1310,1311,1313],{},"Write important files atomically, keep your tool's files in ",[53,1312,673],{}," locations, and lock shared state.",[32,1315,1316,1317,1319],{},"Use one configured ",[53,1318,711],{},", deliberate timeouts, safe retries with backoff, and generator-based pagination.",[32,1321,1322],{},"Match concurrency to the work, bound it, collect failures as values and design for Ctrl+C from the start.",[32,1324,1325],{},"Keep secrets out of argv, config files and logs; use the keychain for people and env vars or files for automation.",[32,1327,1328,1329,1331],{},"Treat ",[53,1330,99],{}," like Ctrl+C, make scheduled commands idempotent and overlap-safe, and publish a heartbeat.",[21,1333,1335],{"id":1334},"frequently-asked-questions","Frequently asked questions",[1337,1338,1340],"h3",{"id":1339},"do-i-need-all-of-this-for-a-small-internal-script","Do I need all of this for a small internal script?",[10,1342,1343],{},"No. For a fifty-line script run by its author, most of this is overkill. The patterns start paying for themselves when a tool has other users, runs unattended, or touches anything important: that is when a torn config file, a hung request or a leaked token stops being a curiosity and becomes an incident. Adopt them boundary by boundary as the tool grows.",[1337,1345,1347],{"id":1346},"should-these-helper-modules-become-a-shared-internal-library","Should these helper modules become a shared internal library?",[10,1349,1350,1351,435,1354,1356],{},"Often, yes — once two or three tools in an organisation have grown their own ",[53,1352,1353],{},"proc.py",[53,1355,127],{},", extracting them into a small shared package with its own tests is worthwhile. Keep it small and dependency-light, because every CLI that depends on it inherits its import time and version constraints.",[1337,1358,1360],{"id":1359},"why-does-the-site-favour-httpx-keyring-platformdirs-and-watchfiles","Why does the site favour httpx, keyring, platformdirs and watchfiles?",[10,1362,1363,1364,136,1367,136,1370,1373],{},"Each is the current, actively maintained, cross-platform default for its job, with a small API and good test support. None is mandatory: ",[53,1365,1366],{},"requests",[53,1368,1369],{},"appdirs",[53,1371,1372],{},"watchdog"," and others work, and the patterns — one client module, the right directories, debounced events — transfer directly.",[1337,1375,1377],{"id":1376},"how-do-these-patterns-affect-startup-time","How do these patterns affect startup time?",[10,1379,1380,1381,1384,1385,196],{},"Libraries like httpx, keyring and Rich each add import time that you pay on every invocation, including ",[53,1382,1383],{},"--help"," and shell completion. Import them inside the modules that need them and load those lazily from the commands that use them, as described in ",[38,1386,1388],{"href":1387},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[1337,1390,1392],{"id":1391},"is-windows-supported-by-everything-here","Is Windows supported by everything here?",[10,1394,1395,1396,1398,1399,1402],{},"Mostly. Subprocess argument lists, pathlib, platformdirs, tempfile, filelock, httpx, keyring and asyncio all work on Windows. The exceptions are POSIX-specific: process groups and ",[53,1397,99],{}," handling, ",[53,1400,1401],{},"fcntl","-level details, and cron and systemd. Each guide calls out the Windows differences where they matter.",[21,1404,1406],{"id":1405},"related","Related",[29,1408,1409,1414,1418,1422,1426,1430,1434,1439,1443],{},[32,1410,1411,1412],{},"Down: ",[38,1413,41],{"href":40},[32,1415,1411,1416],{},[38,1417,50],{"href":49},[32,1419,1411,1420],{},[38,1421,64],{"href":63},[32,1423,1411,1424],{},[38,1425,77],{"href":76},[32,1427,1411,1428],{},[38,1429,86],{"href":85},[32,1431,1411,1432],{},[38,1433,95],{"href":94},[32,1435,1436,1437],{},"Sideways: ",[38,1438,1230],{"href":1229},[32,1440,1436,1441],{},[38,1442,1245],{"href":1244},[32,1444,1436,1445],{},[38,1446,1274],{"href":1273},[1448,1449,1450],"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":257,"searchDepth":275,"depth":275,"links":1452},[1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1472],{"id":23,"depth":275,"text":24},{"id":103,"depth":275,"text":104},{"id":199,"depth":275,"text":200},{"id":453,"depth":275,"text":454},{"id":701,"depth":275,"text":702},{"id":766,"depth":275,"text":767},{"id":827,"depth":275,"text":828},{"id":872,"depth":275,"text":873},{"id":916,"depth":275,"text":917},{"id":1213,"depth":275,"text":1214},{"id":1282,"depth":275,"text":1283},{"id":1295,"depth":275,"text":1296},{"id":1334,"depth":275,"text":1335,"children":1466},[1467,1468,1469,1470,1471],{"id":1339,"depth":282,"text":1340},{"id":1346,"depth":282,"text":1347},{"id":1359,"depth":282,"text":1360},{"id":1376,"depth":282,"text":1377},{"id":1391,"depth":282,"text":1392},{"id":1405,"depth":275,"text":1406},"2026-09-18","How Python CLIs work with the rest of the system: subprocesses, files, HTTP APIs, concurrency, secrets and long-running commands, each handled safely.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration",{"title":5,"description":1474},"cli-runtime-systems-integration\u002Findex",[1483,222,1484,1485,1486,1487],"runtime","filesystem","http","concurrency","security","Swo1eIpapycbWFyuroK4vMXL60q5GWGG8RI-oZRg_QI",[1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1526,1529,1532,1535,1538,1541,1544,1547,1550,1553,1556,1559,1562,1565,1568,1571,1574,1577,1580,1583,1586,1589,1592,1595,1598,1601,1604,1607,1610,1613,1616,1619,1622,1625,1628,1631,1634,1637,1640,1643,1646,1649,1652,1655,1658,1661,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1705,1708,1709,1712,1715,1718,1721,1724,1727,1730,1733,1736,1739,1742,1745,1748,1751,1754,1757,1760,1763,1766,1769,1772,1775,1778,1781,1784,1787,1790,1793,1796,1799,1802,1805,1808,1811,1814,1817,1820,1823,1826,1829,1832,1835,1838,1841,1844,1847,1850,1853,1856,1859,1862,1865,1868,1871,1874,1877,1880,1883,1886,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978,1981,1984,1987,1990,1993,1996,1999,2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032],{"path":1491,"title":1492},"\u002Fabout","About Python CLI Toolcraft",{"path":1494,"title":1495},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1497,"title":1498},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1500,"title":1501},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1503,"title":1504},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1506,"title":1507},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1509,"title":1510},"\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":1512,"title":1513},"\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":1515,"title":1516},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1518,"title":1519},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1521,"title":1522},"\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":1524,"title":1525},"\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":1527,"title":1528},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1530,"title":1531},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1533,"title":1534},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1536,"title":1537},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1539,"title":1540},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1542,"title":1543},"\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":1545,"title":1546},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1548,"title":1549},"\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":1551,"title":1552},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1554,"title":1555},"\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":1557,"title":1558},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1560,"title":1561},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1563,"title":1564},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1566,"title":1567},"\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":1569,"title":1570},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1572,"title":1573},"\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":1575,"title":1576},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1578,"title":1579},"\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":1581,"title":1582},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1584,"title":1585},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1587,"title":1588},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1590,"title":1591},"\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":1593,"title":1594},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1596,"title":1597},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1599,"title":1600},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1602,"title":1603},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1605,"title":1606},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1608,"title":1609},"\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":1611,"title":1612},"\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":1614,"title":1615},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1617,"title":1618},"\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":1620,"title":1621},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1623,"title":1624},"\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":1626,"title":1627},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1629,"title":1630},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1632,"title":1633},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1635,"title":1636},"\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":1638,"title":1639},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1641,"title":1642},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1644,"title":1645},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1647,"title":1648},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1650,"title":1651},"\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":1653,"title":1654},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1656,"title":1657},"\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":1659,"title":1660},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1662,"title":64},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":1664,"title":1665},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1667,"title":1668},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1670,"title":1671},"\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":1673,"title":1674},"\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":1676,"title":1677},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1679,"title":1680},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1682,"title":1683},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1685,"title":1686},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1688,"title":1689},"\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":1691,"title":1692},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1694,"title":1695},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1697,"title":1698},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":1700,"title":1701},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":1703,"title":1704},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":1706,"title":1707},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":1479,"title":5},{"path":1710,"title":1711},"\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":1713,"title":1714},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":1716,"title":1717},"\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":1719,"title":1720},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":1722,"title":1723},"\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":1725,"title":1726},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":1728,"title":1729},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":1731,"title":1732},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":1734,"title":1735},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":1737,"title":1738},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":1740,"title":1741},"\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":1743,"title":1744},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":1746,"title":1747},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":1749,"title":1750},"\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":1752,"title":1753},"\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":1755,"title":1756},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":1758,"title":1759},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1761,"title":1762},"\u002F","Python CLI Toolcraft",{"path":1764,"title":1765},"\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":1767,"title":1768},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1770,"title":1771},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1773,"title":1774},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1776,"title":1777},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1779,"title":1780},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1782,"title":1783},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1785,"title":1786},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1788,"title":1789},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1791,"title":1792},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":1794,"title":1795},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":1797,"title":1798},"\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":1800,"title":1801},"\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":1803,"title":1804},"\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":1806,"title":1807},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":1809,"title":1810},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":1812,"title":1813},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1815,"title":1816},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":1818,"title":1819},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":1821,"title":1822},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1824,"title":1825},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":1827,"title":1828},"\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":1830,"title":1831},"\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":1833,"title":1834},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1836,"title":1837},"\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":1839,"title":1840},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1842,"title":1843},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":1845,"title":1846},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1848,"title":1849},"\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":1851,"title":1852},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1854,"title":1855},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1857,"title":1858},"\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":1860,"title":1861},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":1863,"title":1864},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1866,"title":1867},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1869,"title":1870},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1872,"title":1873},"\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":1875,"title":1876},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":1878,"title":1879},"\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":1881,"title":1882},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1884,"title":1885},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1887,"title":1888},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":1890,"title":1891},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":1893,"title":1894},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":1896,"title":1897},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":1899,"title":1900},"\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":1902,"title":1903},"\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":1905,"title":1906},"\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":1908,"title":1909},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":1911,"title":1912},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1914,"title":1915},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1917,"title":1918},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":1920,"title":1921},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1923,"title":1924},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1926,"title":1927},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1929,"title":1930},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1932,"title":1933},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":1935,"title":1936},"\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":1938,"title":1274},"\u002Fproject-setup-dependency-management",{"path":1940,"title":1941},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":1943,"title":1944},"\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":1946,"title":1947},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":1949,"title":1950},"\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":1952,"title":1953},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1955,"title":1956},"\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":1958,"title":1959},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1961,"title":1962},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1964,"title":1965},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":1967,"title":1968},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1970,"title":1971},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":1973,"title":1974},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1976,"title":1977},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1979,"title":1980},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1982,"title":1983},"\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":1985,"title":1986},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1988,"title":1989},"\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":1991,"title":1992},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":1994,"title":1995},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1997,"title":1998},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2000,"title":2001},"\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":2003,"title":2004},"\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":2006,"title":2007},"\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":2009,"title":2010},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2012,"title":2013},"\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":2015,"title":2016},"\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":2018,"title":2019},"\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":2021,"title":2022},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2024,"title":2025},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2027,"title":2028},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2030,"title":2031},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2033,"title":2034},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905049]