[{"data":1,"prerenderedAt":2552},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F":3,"content-directory":2005},{"id":4,"title":5,"body":6,"date":1991,"description":1992,"difficulty":1993,"draft":1994,"extension":1995,"meta":1996,"navigation":277,"path":1997,"seo":1998,"stem":1999,"tags":2000,"updated":1991,"__hash__":2004},"content\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Findex.md","Filesystem Paths and Atomic Writes for CLIs",{"type":7,"value":8,"toc":1969},"minimark",[9,18,41,45,50,120,124,127,130,144,173,184,203,214,218,225,228,520,541,545,564,899,926,930,941,944,950,1036,1056,1060,1070,1244,1259,1263,1266,1377,1401,1405,1408,1424,1442,1463,1471,1475,1503,1770,1777,1800,1804,1842,1846,1854,1869,1876,1882,1886,1893,1897,1910,1914,1920,1924,1965],[10,11,12,13,17],"p",{},"A command-line tool is, more often than not, a program that reads some files and writes some others. That makes the filesystem the place where a CLI's bugs do the most lasting damage. A crash in the middle of a write leaves a config file empty. A path built with string concatenation works on the author's Mac and breaks on a colleague's Windows laptop. Two cron jobs running the same command at once silently lose each other's updates. A tool that drops ",[14,15,16],"code",{},".mytool-cache"," into whatever directory it was run from leaves litter across every project on the machine. None of these show up in a quick manual test, and all of them show up eventually in real use.",[10,19,20,21,24,25,30,31,35,36,40],{},"This topic covers the file-handling habits that separate a dependable CLI from a script: writing atomically, using ",[14,22,23],{},"pathlib"," as the one path API, keeping config, cache and state in the right per-user directories, creating temporary files safely, and locking when concurrent runs are possible. It sits in the ",[26,27,29],"a",{"href":28},"\u002Fcli-runtime-systems-integration\u002F","CLI Runtime & Systems Integration"," section alongside ",[26,32,34],{"href":33},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002F","running subprocesses"," and ",[26,37,39],{"href":38},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","secrets and credentials",", which both lean on the patterns here.",[42,43],"inline-diagram",{"name":44},"fs-topic-map",[46,47,49],"h2",{"id":48},"tldr","TL;DR",[51,52,53,69,83,93,106],"ul",{},[54,55,56,60,61,64,65,68],"li",{},[57,58,59],"strong",{},"Never overwrite a file in place."," Write to a temporary file in the same directory, ",[14,62,63],{},"fsync"," it, then ",[14,66,67],{},"os.replace()"," it over the target. Readers see the old file or the new one, never a torn one.",[54,70,71,78,79,82],{},[57,72,73,74,77],{},"Use ",[14,75,76],{},"pathlib.Path"," everywhere"," inside your code, and read and write text with an explicit ",[14,80,81],{},"encoding=\"utf-8\"",".",[54,84,85,88,89,92],{},[57,86,87],{},"Put your own files in per-user directories"," from ",[14,90,91],{},"platformdirs",": config the user edits, state the tool owns, cache the tool can throw away. Never write into the current directory unless the user asked for output there.",[54,94,95,101,102,105],{},[57,96,97,98],{},"Create temporary files with ",[14,99,100],{},"tempfile",", inside ",[14,103,104],{},"with"," blocks, so they are unpredictable and always cleaned up.",[54,107,108,111,112,115,116,119],{},[57,109,110],{},"Lock when two runs could collide."," An OS-level lock (via ",[14,113,114],{},"filelock"," or ",[14,117,118],{},"fcntl.flock",") releases automatically if the process dies.",[46,121,123],{"id":122},"five-ways-file-handling-goes-wrong","Five ways file handling goes wrong",[10,125,126],{},"It is worth naming the failure modes, because each one maps to a technique in this topic and each one is invisible until it happens to a user.",[42,128],{"name":129},"fs-failure-modes",[10,131,132,135,136,139,140,82],{},[57,133,134],{},"Torn writes."," ",[14,137,138],{},"open(path, \"w\")"," truncates the file the moment it opens. If the process then crashes, is killed by a timeout, loses power, or hits a full disk, the file is left empty or half-written. For a user's config or a tool's state file, that often means the next run fails to start at all. The cure is the ",[26,141,143],{"href":142},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","atomic write pattern",[10,145,146,135,149,152,153,35,156,159,160,163,164,167,168,172],{},[57,147,148],{},"Platform assumptions.",[14,150,151],{},"\"~\u002F.mytool\u002F\" + name",", ",[14,154,155],{},"path.split(\"\u002F\")[-1]",[14,157,158],{},"os.path.join(root, \"a\u002Fb\")"," all encode a Unix view of paths. On Windows, ",[14,161,162],{},"~"," is not expanded by ",[14,165,166],{},"open()",", separators differ, and drive letters change the rules for absolute paths. ",[26,169,171],{"href":170},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F","Cross-platform paths with pathlib"," replaces the string manipulation with an API that gets it right everywhere.",[10,174,175,178,179,183],{},[57,176,177],{},"Concurrent runs."," Cron fires while a previous run is still going; a developer opens two terminals; CI runs a matrix of jobs on one runner. Two processes each read a state file, change it, and write it back — and one update disappears without an error. Atomic writes do not fix this; ",[26,180,182],{"href":181},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002F","file locking for concurrent CLI runs"," does.",[10,185,186,189,190,193,194,197,198,202],{},[57,187,188],{},"Leaked temporary files."," A tool that extracts archives or renders documents into ",[14,191,192],{},"\u002Ftmp"," and forgets to clean up after a failure fills disks slowly. One that uses predictable names like ",[14,195,196],{},"\u002Ftmp\u002Fmytool.out"," is also exposed to symlink attacks on shared machines. ",[26,199,201],{"href":200},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories\u002F","Safe temporary files and directories"," covers both.",[10,204,205,208,209,213],{},[57,206,207],{},"Files in the wrong place."," Caches in the working directory, config in the home directory root, logs next to the executable. Each is a small annoyance; together they make a tool feel careless. ",[26,210,212],{"href":211},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","Storing app data with platformdirs"," puts each kind of file where the operating system expects it.",[46,215,217],{"id":216},"writing-files-you-cannot-corrupt","Writing files you cannot corrupt",[10,219,220,221,224],{},"The atomic write is the most valuable single technique in this topic, and it is short enough to show here in full. The idea: create a temporary file ",[57,222,223],{},"in the same directory"," as the target, write everything to it, force it to disk, then rename it over the target. A rename within one filesystem is atomic — at any instant the path refers to either the complete old file or the complete new one.",[42,226],{"name":227},"fs-write-lifecycle",[229,230,235],"pre",{"className":231,"code":232,"language":233,"meta":234,"style":234},"language-python shiki shiki-themes github-light github-dark","import os\nimport tempfile\nfrom pathlib import Path\n\n\ndef write_atomic(path: Path, data: str, encoding: str = \"utf-8\") -> None:\n    path = Path(path)\n    fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f\".{path.name}.\", suffix=\".tmp\")\n    try:\n        with os.fdopen(fd, \"w\", encoding=encoding, newline=\"\") as fh:\n            fh.write(data)\n            fh.flush()\n            os.fsync(fh.fileno())\n        if path.exists():\n            os.chmod(tmp, path.stat().st_mode & 0o7777)\n        os.replace(tmp, path)\n    except BaseException:\n        Path(tmp).unlink(missing_ok=True)\n        raise\n","python","",[14,236,237,250,258,272,279,284,322,334,390,398,437,443,449,455,464,481,487,498,514],{"__ignoreMap":234},[238,239,242,246],"span",{"class":240,"line":241},"line",1,[238,243,245],{"class":244},"szBVR","import",[238,247,249],{"class":248},"sVt8B"," os\n",[238,251,253,255],{"class":240,"line":252},2,[238,254,245],{"class":244},[238,256,257],{"class":248}," tempfile\n",[238,259,261,264,267,269],{"class":240,"line":260},3,[238,262,263],{"class":244},"from",[238,265,266],{"class":248}," pathlib ",[238,268,245],{"class":244},[238,270,271],{"class":248}," Path\n",[238,273,275],{"class":240,"line":274},4,[238,276,278],{"emptyLinePlaceholder":277},true,"\n",[238,280,282],{"class":240,"line":281},5,[238,283,278],{"emptyLinePlaceholder":277},[238,285,287,290,294,297,301,304,306,309,313,316,319],{"class":240,"line":286},6,[238,288,289],{"class":244},"def",[238,291,293],{"class":292},"sScJk"," write_atomic",[238,295,296],{"class":248},"(path: Path, data: ",[238,298,300],{"class":299},"sj4cs","str",[238,302,303],{"class":248},", encoding: ",[238,305,300],{"class":299},[238,307,308],{"class":244}," =",[238,310,312],{"class":311},"sZZnC"," \"utf-8\"",[238,314,315],{"class":248},") -> ",[238,317,318],{"class":299},"None",[238,320,321],{"class":248},":\n",[238,323,325,328,331],{"class":240,"line":324},7,[238,326,327],{"class":248},"    path ",[238,329,330],{"class":244},"=",[238,332,333],{"class":248}," Path(path)\n",[238,335,337,340,342,345,349,351,354,357,359,362,365,368,371,374,377,379,382,384,387],{"class":240,"line":336},8,[238,338,339],{"class":248},"    fd, tmp ",[238,341,330],{"class":244},[238,343,344],{"class":248}," tempfile.mkstemp(",[238,346,348],{"class":347},"s4XuR","dir",[238,350,330],{"class":244},[238,352,353],{"class":248},"path.parent, ",[238,355,356],{"class":347},"prefix",[238,358,330],{"class":244},[238,360,361],{"class":244},"f",[238,363,364],{"class":311},"\".",[238,366,367],{"class":299},"{",[238,369,370],{"class":248},"path.name",[238,372,373],{"class":299},"}",[238,375,376],{"class":311},".\"",[238,378,152],{"class":248},[238,380,381],{"class":347},"suffix",[238,383,330],{"class":244},[238,385,386],{"class":311},"\".tmp\"",[238,388,389],{"class":248},")\n",[238,391,393,396],{"class":240,"line":392},9,[238,394,395],{"class":244},"    try",[238,397,321],{"class":248},[238,399,401,404,407,410,412,415,417,420,423,425,428,431,434],{"class":240,"line":400},10,[238,402,403],{"class":244},"        with",[238,405,406],{"class":248}," os.fdopen(fd, ",[238,408,409],{"class":311},"\"w\"",[238,411,152],{"class":248},[238,413,414],{"class":347},"encoding",[238,416,330],{"class":244},[238,418,419],{"class":248},"encoding, ",[238,421,422],{"class":347},"newline",[238,424,330],{"class":244},[238,426,427],{"class":311},"\"\"",[238,429,430],{"class":248},") ",[238,432,433],{"class":244},"as",[238,435,436],{"class":248}," fh:\n",[238,438,440],{"class":240,"line":439},11,[238,441,442],{"class":248},"            fh.write(data)\n",[238,444,446],{"class":240,"line":445},12,[238,447,448],{"class":248},"            fh.flush()\n",[238,450,452],{"class":240,"line":451},13,[238,453,454],{"class":248},"            os.fsync(fh.fileno())\n",[238,456,458,461],{"class":240,"line":457},14,[238,459,460],{"class":244},"        if",[238,462,463],{"class":248}," path.exists():\n",[238,465,467,470,473,476,479],{"class":240,"line":466},15,[238,468,469],{"class":248},"            os.chmod(tmp, path.stat().st_mode ",[238,471,472],{"class":244},"&",[238,474,475],{"class":244}," 0o",[238,477,478],{"class":299},"7777",[238,480,389],{"class":248},[238,482,484],{"class":240,"line":483},16,[238,485,486],{"class":248},"        os.replace(tmp, path)\n",[238,488,490,493,496],{"class":240,"line":489},17,[238,491,492],{"class":244},"    except",[238,494,495],{"class":299}," BaseException",[238,497,321],{"class":248},[238,499,501,504,507,509,512],{"class":240,"line":500},18,[238,502,503],{"class":248},"        Path(tmp).unlink(",[238,505,506],{"class":347},"missing_ok",[238,508,330],{"class":244},[238,510,511],{"class":299},"True",[238,513,389],{"class":248},[238,515,517],{"class":240,"line":516},19,[238,518,519],{"class":244},"        raise\n",[10,521,522,523,526,527,529,530,533,534,536,537,540],{},"Three details make it correct rather than approximately correct. The temp file lives in ",[14,524,525],{},"path.parent",", because a rename across filesystems (from ",[14,528,192],{}," to your home directory, say) is really a copy and is not atomic. ",[14,531,532],{},"os.fsync()"," pushes the data to disk before the rename, so a power loss cannot leave a renamed-but-empty file. And ",[14,535,67],{}," — not ",[14,538,539],{},"os.rename()"," — overwrites the target on Windows as well as POSIX. The full guide adds permission handling, binary data, JSON helpers and tests.",[46,542,544],{"id":543},"one-path-type-used-consistently","One path type, used consistently",[10,546,547,549,550,553,554,556,557,560,561,563],{},[14,548,76],{}," should be the only representation of a path inside your program. Parse arguments into ",[14,551,552],{},"Path"," objects at the edge — Typer does this when you annotate a parameter as ",[14,555,552],{},", and Click does it with ",[14,558,559],{},"click.Path(path_type=Path)"," — and pass them around as ",[14,562,552],{}," until something outside Python needs a string.",[229,565,567],{"className":231,"code":566,"language":233,"meta":234,"style":234},"from pathlib import Path\n\nimport typer\n\napp = typer.Typer()\n\n\n@app.command()\ndef index(\n    root: Path = typer.Argument(Path(\".\"), exists=True, file_okay=False, resolve_path=True),\n    out: Path = typer.Option(Path(\"index.json\"), \"--out\", \"-o\", dir_okay=False),\n) -> None:\n    \"\"\"Index every Markdown file under ROOT.\"\"\"\n    docs = sorted(p.relative_to(root) for p in root.rglob(\"*.md\") if p.is_file())\n    out.parent.mkdir(parents=True, exist_ok=True)\n    out.write_text(\"\\n\".join(d.as_posix() for d in docs) + \"\\n\", encoding=\"utf-8\")\n    typer.echo(f\"indexed {len(docs)} files -> {out}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,568,569,579,583,590,594,604,608,612,620,630,675,709,717,722,758,781,828,869,873,877,893],{"__ignoreMap":234},[238,570,571,573,575,577],{"class":240,"line":241},[238,572,263],{"class":244},[238,574,266],{"class":248},[238,576,245],{"class":244},[238,578,271],{"class":248},[238,580,581],{"class":240,"line":252},[238,582,278],{"emptyLinePlaceholder":277},[238,584,585,587],{"class":240,"line":260},[238,586,245],{"class":244},[238,588,589],{"class":248}," typer\n",[238,591,592],{"class":240,"line":274},[238,593,278],{"emptyLinePlaceholder":277},[238,595,596,599,601],{"class":240,"line":281},[238,597,598],{"class":248},"app ",[238,600,330],{"class":244},[238,602,603],{"class":248}," typer.Typer()\n",[238,605,606],{"class":240,"line":286},[238,607,278],{"emptyLinePlaceholder":277},[238,609,610],{"class":240,"line":324},[238,611,278],{"emptyLinePlaceholder":277},[238,613,614,617],{"class":240,"line":336},[238,615,616],{"class":292},"@app.command",[238,618,619],{"class":248},"()\n",[238,621,622,624,627],{"class":240,"line":392},[238,623,289],{"class":244},[238,625,626],{"class":292}," index",[238,628,629],{"class":248},"(\n",[238,631,632,635,637,640,643,646,649,651,653,655,658,660,663,665,668,670,672],{"class":240,"line":400},[238,633,634],{"class":248},"    root: Path ",[238,636,330],{"class":244},[238,638,639],{"class":248}," typer.Argument(Path(",[238,641,642],{"class":311},"\".\"",[238,644,645],{"class":248},"), ",[238,647,648],{"class":347},"exists",[238,650,330],{"class":244},[238,652,511],{"class":299},[238,654,152],{"class":248},[238,656,657],{"class":347},"file_okay",[238,659,330],{"class":244},[238,661,662],{"class":299},"False",[238,664,152],{"class":248},[238,666,667],{"class":347},"resolve_path",[238,669,330],{"class":244},[238,671,511],{"class":299},[238,673,674],{"class":248},"),\n",[238,676,677,680,682,685,688,690,693,695,698,700,703,705,707],{"class":240,"line":439},[238,678,679],{"class":248},"    out: Path ",[238,681,330],{"class":244},[238,683,684],{"class":248}," typer.Option(Path(",[238,686,687],{"class":311},"\"index.json\"",[238,689,645],{"class":248},[238,691,692],{"class":311},"\"--out\"",[238,694,152],{"class":248},[238,696,697],{"class":311},"\"-o\"",[238,699,152],{"class":248},[238,701,702],{"class":347},"dir_okay",[238,704,330],{"class":244},[238,706,662],{"class":299},[238,708,674],{"class":248},[238,710,711,713,715],{"class":240,"line":445},[238,712,315],{"class":248},[238,714,318],{"class":299},[238,716,321],{"class":248},[238,718,719],{"class":240,"line":451},[238,720,721],{"class":311},"    \"\"\"Index every Markdown file under ROOT.\"\"\"\n",[238,723,724,727,729,732,735,738,741,744,747,750,752,755],{"class":240,"line":457},[238,725,726],{"class":248},"    docs ",[238,728,330],{"class":244},[238,730,731],{"class":299}," sorted",[238,733,734],{"class":248},"(p.relative_to(root) ",[238,736,737],{"class":244},"for",[238,739,740],{"class":248}," p ",[238,742,743],{"class":244},"in",[238,745,746],{"class":248}," root.rglob(",[238,748,749],{"class":311},"\"*.md\"",[238,751,430],{"class":248},[238,753,754],{"class":244},"if",[238,756,757],{"class":248}," p.is_file())\n",[238,759,760,763,766,768,770,772,775,777,779],{"class":240,"line":466},[238,761,762],{"class":248},"    out.parent.mkdir(",[238,764,765],{"class":347},"parents",[238,767,330],{"class":244},[238,769,511],{"class":299},[238,771,152],{"class":248},[238,773,774],{"class":347},"exist_ok",[238,776,330],{"class":244},[238,778,511],{"class":299},[238,780,389],{"class":248},[238,782,783,786,789,792,794,797,799,802,804,807,810,813,815,817,819,821,823,826],{"class":240,"line":483},[238,784,785],{"class":248},"    out.write_text(",[238,787,788],{"class":311},"\"",[238,790,791],{"class":299},"\\n",[238,793,788],{"class":311},[238,795,796],{"class":248},".join(d.as_posix() ",[238,798,737],{"class":244},[238,800,801],{"class":248}," d ",[238,803,743],{"class":244},[238,805,806],{"class":248}," docs) ",[238,808,809],{"class":244},"+",[238,811,812],{"class":311}," \"",[238,814,791],{"class":299},[238,816,788],{"class":311},[238,818,152],{"class":248},[238,820,414],{"class":347},[238,822,330],{"class":244},[238,824,825],{"class":311},"\"utf-8\"",[238,827,389],{"class":248},[238,829,830,833,835,838,841,844,846,849,851,854,856,858,860,863,865,867],{"class":240,"line":489},[238,831,832],{"class":248},"    typer.echo(",[238,834,361],{"class":244},[238,836,837],{"class":311},"\"indexed ",[238,839,840],{"class":299},"{len",[238,842,843],{"class":248},"(docs)",[238,845,373],{"class":299},[238,847,848],{"class":311}," files -> ",[238,850,367],{"class":299},[238,852,853],{"class":248},"out",[238,855,373],{"class":299},[238,857,788],{"class":311},[238,859,152],{"class":248},[238,861,862],{"class":347},"err",[238,864,330],{"class":244},[238,866,511],{"class":299},[238,868,389],{"class":248},[238,870,871],{"class":240,"line":500},[238,872,278],{"emptyLinePlaceholder":277},[238,874,875],{"class":240,"line":516},[238,876,278],{"emptyLinePlaceholder":277},[238,878,880,882,885,888,891],{"class":240,"line":879},20,[238,881,754],{"class":244},[238,883,884],{"class":299}," __name__",[238,886,887],{"class":244}," ==",[238,889,890],{"class":311}," \"__main__\"",[238,892,321],{"class":248},[238,894,896],{"class":240,"line":895},21,[238,897,898],{"class":248},"    app()\n",[10,900,901,902,905,906,909,910,912,913,916,917,920,921,925],{},"Two habits in that snippet are worth adopting everywhere. ",[14,903,904],{},"as_posix()"," gives a stable, forward-slash representation for output that other programs or other platforms will read, while ",[14,907,908],{},"str(path)"," gives the native form for display to a local user. And ",[14,911,81],{}," is explicit on every text read and write: until UTF-8 mode becomes the default (PEP 686, planned for Python 3.15), ",[14,914,915],{},"Path.write_text()"," without it uses the locale encoding, which is still often ",[14,918,919],{},"cp1252"," on Windows. ",[26,922,924],{"href":923},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis\u002F","Validating file and directory paths in CLIs"," covers the argument side — existence, permissions and friendly errors — in more depth.",[46,927,929],{"id":928},"where-your-tools-own-files-belong","Where your tool's own files belong",[10,931,932,933,936,937,940],{},"A CLI deals with two very different kinds of file. ",[57,934,935],{},"The user's files"," — the inputs they point it at and the outputs they ask for — live wherever the user says, and your tool should only touch them when asked. ",[57,938,939],{},"Your tool's own files"," — settings, caches, history, downloaded data, logs — belong in directories the operating system designates for each application, not in the user's projects and not scattered across the home directory.",[42,942],{"name":943},"fs-where-files-live",[10,945,946,947,949],{},"The ",[14,948,91],{}," package knows those directories for Linux (following the XDG base-directory specification), macOS and Windows:",[229,951,953],{"className":231,"code":952,"language":233,"meta":234,"style":234},"from platformdirs import PlatformDirs\n\ndirs = PlatformDirs(\"mytool\", appauthor=False)\nconfig_file = dirs.user_config_path \u002F \"config.toml\"\ncache_dir = dirs.user_cache_path\nstate_file = dirs.user_state_path \u002F \"last-run.json\"\n",[14,954,955,967,971,995,1011,1021],{"__ignoreMap":234},[238,956,957,959,962,964],{"class":240,"line":241},[238,958,263],{"class":244},[238,960,961],{"class":248}," platformdirs ",[238,963,245],{"class":244},[238,965,966],{"class":248}," PlatformDirs\n",[238,968,969],{"class":240,"line":252},[238,970,278],{"emptyLinePlaceholder":277},[238,972,973,976,978,981,984,986,989,991,993],{"class":240,"line":260},[238,974,975],{"class":248},"dirs ",[238,977,330],{"class":244},[238,979,980],{"class":248}," PlatformDirs(",[238,982,983],{"class":311},"\"mytool\"",[238,985,152],{"class":248},[238,987,988],{"class":347},"appauthor",[238,990,330],{"class":244},[238,992,662],{"class":299},[238,994,389],{"class":248},[238,996,997,1000,1002,1005,1008],{"class":240,"line":274},[238,998,999],{"class":248},"config_file ",[238,1001,330],{"class":244},[238,1003,1004],{"class":248}," dirs.user_config_path ",[238,1006,1007],{"class":244},"\u002F",[238,1009,1010],{"class":311}," \"config.toml\"\n",[238,1012,1013,1016,1018],{"class":240,"line":281},[238,1014,1015],{"class":248},"cache_dir ",[238,1017,330],{"class":244},[238,1019,1020],{"class":248}," dirs.user_cache_path\n",[238,1022,1023,1026,1028,1031,1033],{"class":240,"line":286},[238,1024,1025],{"class":248},"state_file ",[238,1027,330],{"class":244},[238,1029,1030],{"class":248}," dirs.user_state_path ",[238,1032,1007],{"class":244},[238,1034,1035],{"class":311}," \"last-run.json\"\n",[10,1037,1038,1039,1042,1043,1046,1047,1050,1051,1055],{},"The distinction between the directories is not pedantry. Users back up their config directory and expect to be able to delete caches freely; system cleaners and container images treat them differently. A good test is to ask \"if this file vanished, what would be lost?\" — nothing means cache, the user's choices means config, and the tool's own memory means state. The ",[26,1040,1041],{"href":211},"platformdirs guide"," covers environment overrides, ",[14,1044,1045],{},"--config"," flags and a ",[14,1048,1049],{},"paths"," command, and ",[26,1052,1054],{"href":1053},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002F","handling configuration files and environment variables"," covers what goes inside the config file.",[46,1057,1059],{"id":1058},"temporary-files-created-and-removed-safely","Temporary files, created and removed safely",[10,1061,1062,1063,1065,1066,1069],{},"When a command needs scratch space — extracting an archive, rendering intermediate files, staging a download — use the ",[14,1064,100],{}," module rather than inventing names. ",[14,1067,1068],{},"TemporaryDirectory()"," as a context manager gives you a private, randomly named directory that is deleted when the block ends, even if an exception is raised:",[229,1071,1073],{"className":231,"code":1072,"language":233,"meta":234,"style":234},"import shutil\nimport subprocess\nimport tempfile\nfrom pathlib import Path\n\n\ndef build_site(source: Path, dest: Path) -> None:\n    with tempfile.TemporaryDirectory(prefix=\"mytool-build-\") as tmp:\n        work = Path(tmp)\n        subprocess.run([\"mkdocs\", \"build\", \"-f\", str(source \u002F \"mkdocs.yml\"), \"-d\", str(work \u002F \"site\")],\n                       check=True)\n        if dest.exists():\n            shutil.rmtree(dest)\n        shutil.copytree(work \u002F \"site\", dest)\n",[14,1074,1075,1082,1089,1095,1105,1109,1113,1127,1149,1159,1209,1220,1227,1232],{"__ignoreMap":234},[238,1076,1077,1079],{"class":240,"line":241},[238,1078,245],{"class":244},[238,1080,1081],{"class":248}," shutil\n",[238,1083,1084,1086],{"class":240,"line":252},[238,1085,245],{"class":244},[238,1087,1088],{"class":248}," subprocess\n",[238,1090,1091,1093],{"class":240,"line":260},[238,1092,245],{"class":244},[238,1094,257],{"class":248},[238,1096,1097,1099,1101,1103],{"class":240,"line":274},[238,1098,263],{"class":244},[238,1100,266],{"class":248},[238,1102,245],{"class":244},[238,1104,271],{"class":248},[238,1106,1107],{"class":240,"line":281},[238,1108,278],{"emptyLinePlaceholder":277},[238,1110,1111],{"class":240,"line":286},[238,1112,278],{"emptyLinePlaceholder":277},[238,1114,1115,1117,1120,1123,1125],{"class":240,"line":324},[238,1116,289],{"class":244},[238,1118,1119],{"class":292}," build_site",[238,1121,1122],{"class":248},"(source: Path, dest: Path) -> ",[238,1124,318],{"class":299},[238,1126,321],{"class":248},[238,1128,1129,1132,1135,1137,1139,1142,1144,1146],{"class":240,"line":336},[238,1130,1131],{"class":244},"    with",[238,1133,1134],{"class":248}," tempfile.TemporaryDirectory(",[238,1136,356],{"class":347},[238,1138,330],{"class":244},[238,1140,1141],{"class":311},"\"mytool-build-\"",[238,1143,430],{"class":248},[238,1145,433],{"class":244},[238,1147,1148],{"class":248}," tmp:\n",[238,1150,1151,1154,1156],{"class":240,"line":392},[238,1152,1153],{"class":248},"        work ",[238,1155,330],{"class":244},[238,1157,1158],{"class":248}," Path(tmp)\n",[238,1160,1161,1164,1167,1169,1172,1174,1177,1179,1181,1184,1186,1189,1191,1194,1196,1198,1201,1203,1206],{"class":240,"line":400},[238,1162,1163],{"class":248},"        subprocess.run([",[238,1165,1166],{"class":311},"\"mkdocs\"",[238,1168,152],{"class":248},[238,1170,1171],{"class":311},"\"build\"",[238,1173,152],{"class":248},[238,1175,1176],{"class":311},"\"-f\"",[238,1178,152],{"class":248},[238,1180,300],{"class":299},[238,1182,1183],{"class":248},"(source ",[238,1185,1007],{"class":244},[238,1187,1188],{"class":311}," \"mkdocs.yml\"",[238,1190,645],{"class":248},[238,1192,1193],{"class":311},"\"-d\"",[238,1195,152],{"class":248},[238,1197,300],{"class":299},[238,1199,1200],{"class":248},"(work ",[238,1202,1007],{"class":244},[238,1204,1205],{"class":311}," \"site\"",[238,1207,1208],{"class":248},")],\n",[238,1210,1211,1214,1216,1218],{"class":240,"line":439},[238,1212,1213],{"class":347},"                       check",[238,1215,330],{"class":244},[238,1217,511],{"class":299},[238,1219,389],{"class":248},[238,1221,1222,1224],{"class":240,"line":445},[238,1223,460],{"class":244},[238,1225,1226],{"class":248}," dest.exists():\n",[238,1228,1229],{"class":240,"line":451},[238,1230,1231],{"class":248},"            shutil.rmtree(dest)\n",[238,1233,1234,1237,1239,1241],{"class":240,"line":457},[238,1235,1236],{"class":248},"        shutil.copytree(work ",[238,1238,1007],{"class":244},[238,1240,1205],{"class":311},[238,1242,1243],{"class":248},", dest)\n",[10,1245,1246,1247,1250,1251,1254,1255,1258],{},"Building into a temporary directory and only copying the result into place once the build succeeds is the directory-level cousin of the atomic write: a failed build never leaves the destination half-updated. Random names created with ",[14,1248,1249],{},"O_EXCL"," also close the race in which another user on a shared machine pre-creates your predictable path as a symlink. The ",[26,1252,1253],{"href":200},"temporary files guide"," covers ",[14,1256,1257],{},"NamedTemporaryFile"," on Windows, keeping temp directories for debugging, and cleanup on Ctrl+C.",[46,1260,1262],{"id":1261},"two-runs-one-file","Two runs, one file",[10,1264,1265],{},"Atomic writes guarantee that every write is complete. They do not guarantee that a write is based on the latest data. If two invocations of your tool read the same state file, both modify it, and both write it back atomically, the file is perfectly well-formed and one of the updates is gone. When your CLI keeps state that concurrent runs update — a download cache index, a queue, a counter, a \"last synced\" marker — you need a lock around the read-modify-write cycle.",[229,1267,1269],{"className":231,"code":1268,"language":233,"meta":234,"style":234},"from filelock import FileLock, Timeout\n\nfrom mytool.paths import dirs\n\nlock = FileLock(dirs.user_state_path \u002F \"sync.lock\", timeout=30)\ntry:\n    with lock:\n        run_sync()  # read state, work, write state atomically\nexcept Timeout:\n    raise SystemExit(\"another sync is running; try again shortly\")\n",[14,1270,1271,1283,1287,1299,1303,1330,1337,1344,1353,1361],{"__ignoreMap":234},[238,1272,1273,1275,1278,1280],{"class":240,"line":241},[238,1274,263],{"class":244},[238,1276,1277],{"class":248}," filelock ",[238,1279,245],{"class":244},[238,1281,1282],{"class":248}," FileLock, Timeout\n",[238,1284,1285],{"class":240,"line":252},[238,1286,278],{"emptyLinePlaceholder":277},[238,1288,1289,1291,1294,1296],{"class":240,"line":260},[238,1290,263],{"class":244},[238,1292,1293],{"class":248}," mytool.paths ",[238,1295,245],{"class":244},[238,1297,1298],{"class":248}," dirs\n",[238,1300,1301],{"class":240,"line":274},[238,1302,278],{"emptyLinePlaceholder":277},[238,1304,1305,1308,1310,1313,1315,1318,1320,1323,1325,1328],{"class":240,"line":281},[238,1306,1307],{"class":248},"lock ",[238,1309,330],{"class":244},[238,1311,1312],{"class":248}," FileLock(dirs.user_state_path ",[238,1314,1007],{"class":244},[238,1316,1317],{"class":311}," \"sync.lock\"",[238,1319,152],{"class":248},[238,1321,1322],{"class":347},"timeout",[238,1324,330],{"class":244},[238,1326,1327],{"class":299},"30",[238,1329,389],{"class":248},[238,1331,1332,1335],{"class":240,"line":286},[238,1333,1334],{"class":244},"try",[238,1336,321],{"class":248},[238,1338,1339,1341],{"class":240,"line":324},[238,1340,1131],{"class":244},[238,1342,1343],{"class":248}," lock:\n",[238,1345,1346,1349],{"class":240,"line":336},[238,1347,1348],{"class":248},"        run_sync()  ",[238,1350,1352],{"class":1351},"sJ8bj","# read state, work, write state atomically\n",[238,1354,1355,1358],{"class":240,"line":392},[238,1356,1357],{"class":244},"except",[238,1359,1360],{"class":248}," Timeout:\n",[238,1362,1363,1366,1369,1372,1375],{"class":240,"line":400},[238,1364,1365],{"class":244},"    raise",[238,1367,1368],{"class":299}," SystemExit",[238,1370,1371],{"class":248},"(",[238,1373,1374],{"class":311},"\"another sync is running; try again shortly\"",[238,1376,389],{"class":248},[10,1378,1379,1381,1382,1384,1385,1388,1389,1392,1393,1396,1397,1400],{},[14,1380,114],{}," uses the operating system's own locking (",[14,1383,118],{}," on POSIX, ",[14,1386,1387],{},"msvcrt.locking"," on Windows), so a lock held by a process that crashes or is killed is released automatically. That is the property hand-rolled \"create a ",[14,1390,1391],{},".lock"," file and delete it at the end\" schemes lack: one ",[14,1394,1395],{},"kill -9"," and every later run waits for a lock nobody holds. The locking guide covers timeouts, ",[14,1398,1399],{},"--no-wait",", and the difference between locking a resource and preventing a second instance of the whole tool.",[46,1402,1404],{"id":1403},"text-bytes-and-line-endings","Text, bytes and line endings",[10,1406,1407],{},"Three smaller decisions come up every time a CLI writes a file, and making them once, in one helper, saves a steady trickle of bug reports.",[10,1409,1410,1413,1414,152,1417,152,1420,1423],{},[57,1411,1412],{},"Text or bytes."," If you are copying, hashing or transforming data you did not create — an uploaded file, an archive member, a download — keep it as bytes (",[14,1415,1416],{},"read_bytes()",[14,1418,1419],{},"write_bytes()",[14,1421,1422],{},"open(..., \"rb\")","). Decoding and re-encoding data you do not own risks changing it. Decode only what you actually parse.",[10,1425,1426,1429,1430,1433,1434,1437,1438,1441],{},[57,1427,1428],{},"Encoding."," For text your tool creates, choose UTF-8 explicitly. For text you read from users, UTF-8 is still the right default, but decide what happens on a bad byte: ",[14,1431,1432],{},"errors=\"strict\""," is correct for configuration (fail loudly and name the file), while ",[14,1435,1436],{},"errors=\"replace\""," suits log files and other content you only display. A byte-order mark from a Windows editor is a common surprise in config files; ",[14,1439,1440],{},"encoding=\"utf-8-sig\""," reads files with or without one.",[10,1443,1444,1447,1448,1450,1451,1454,1455,1458,1459,1462],{},[57,1445,1446],{},"Line endings."," In text mode Python translates ",[14,1449,791],{}," to the platform's line separator on write, so the same code produces ",[14,1452,1453],{},"\\r\\n"," files on Windows. That is usually what a local user wants for files they open in an editor, and usually wrong for files that are committed to git, compared in tests or consumed on other systems. Pass ",[14,1456,1457],{},"newline=\"\""," (or ",[14,1460,1461],{},"newline=\"\\n\"",") when you need byte-identical output everywhere — the atomic write helper above does exactly that.",[10,1464,1465,1466,1470],{},"Tools that process large inputs should also avoid reading whole files into memory: iterate line by line, or in fixed-size chunks for binary data. ",[26,1467,1469],{"href":1468},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams\u002F","Processing large files and NDJSON streams"," covers streaming input without surprises.",[46,1472,1474],{"id":1473},"how-the-pieces-combine","How the pieces combine",[10,1476,1477,1478,1481,1482,1485,1486,152,1489,1492,1493,1496,1497,1499,1500,1502],{},"In practice these techniques compose into a single small module that the rest of your CLI imports — a ",[14,1479,1480],{},"paths.py"," that defines the directories and a ",[14,1483,1484],{},"files.py"," that provides ",[14,1487,1488],{},"write_atomic",[14,1490,1491],{},"read_json"," and a ",[14,1494,1495],{},"locked()"," context manager. Commands then never call ",[14,1498,166],{}," with ",[14,1501,409],{}," directly:",[229,1504,1506],{"className":231,"code":1505,"language":233,"meta":234,"style":234},"from contextlib import contextmanager\nimport json\nfrom pathlib import Path\nfrom typing import Any, Iterator\n\nfrom filelock import FileLock\nfrom platformdirs import PlatformDirs\n\ndirs = PlatformDirs(\"mytool\", appauthor=False, ensure_exists=True)\n\n\n@contextmanager\ndef locked_state(name: str) -> Iterator[dict[str, Any]]:\n    \"\"\"Load a JSON state file under a lock and save it atomically on success.\"\"\"\n    path = dirs.user_state_path \u002F f\"{name}.json\"\n    with FileLock(str(path) + \".lock\", timeout=30):\n        data = json.loads(path.read_text(encoding=\"utf-8\")) if path.exists() else {}\n        yield data\n        write_atomic(path, json.dumps(data, indent=2, sort_keys=True) + \"\\n\")\n",[14,1507,1508,1520,1527,1537,1549,1553,1564,1574,1578,1607,1611,1615,1620,1640,1645,1670,1698,1728,1736],{"__ignoreMap":234},[238,1509,1510,1512,1515,1517],{"class":240,"line":241},[238,1511,263],{"class":244},[238,1513,1514],{"class":248}," contextlib ",[238,1516,245],{"class":244},[238,1518,1519],{"class":248}," contextmanager\n",[238,1521,1522,1524],{"class":240,"line":252},[238,1523,245],{"class":244},[238,1525,1526],{"class":248}," json\n",[238,1528,1529,1531,1533,1535],{"class":240,"line":260},[238,1530,263],{"class":244},[238,1532,266],{"class":248},[238,1534,245],{"class":244},[238,1536,271],{"class":248},[238,1538,1539,1541,1544,1546],{"class":240,"line":274},[238,1540,263],{"class":244},[238,1542,1543],{"class":248}," typing ",[238,1545,245],{"class":244},[238,1547,1548],{"class":248}," Any, Iterator\n",[238,1550,1551],{"class":240,"line":281},[238,1552,278],{"emptyLinePlaceholder":277},[238,1554,1555,1557,1559,1561],{"class":240,"line":286},[238,1556,263],{"class":244},[238,1558,1277],{"class":248},[238,1560,245],{"class":244},[238,1562,1563],{"class":248}," FileLock\n",[238,1565,1566,1568,1570,1572],{"class":240,"line":324},[238,1567,263],{"class":244},[238,1569,961],{"class":248},[238,1571,245],{"class":244},[238,1573,966],{"class":248},[238,1575,1576],{"class":240,"line":336},[238,1577,278],{"emptyLinePlaceholder":277},[238,1579,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1601,1603,1605],{"class":240,"line":392},[238,1581,975],{"class":248},[238,1583,330],{"class":244},[238,1585,980],{"class":248},[238,1587,983],{"class":311},[238,1589,152],{"class":248},[238,1591,988],{"class":347},[238,1593,330],{"class":244},[238,1595,662],{"class":299},[238,1597,152],{"class":248},[238,1599,1600],{"class":347},"ensure_exists",[238,1602,330],{"class":244},[238,1604,511],{"class":299},[238,1606,389],{"class":248},[238,1608,1609],{"class":240,"line":400},[238,1610,278],{"emptyLinePlaceholder":277},[238,1612,1613],{"class":240,"line":439},[238,1614,278],{"emptyLinePlaceholder":277},[238,1616,1617],{"class":240,"line":445},[238,1618,1619],{"class":292},"@contextmanager\n",[238,1621,1622,1624,1627,1630,1632,1635,1637],{"class":240,"line":451},[238,1623,289],{"class":244},[238,1625,1626],{"class":292}," locked_state",[238,1628,1629],{"class":248},"(name: ",[238,1631,300],{"class":299},[238,1633,1634],{"class":248},") -> Iterator[dict[",[238,1636,300],{"class":299},[238,1638,1639],{"class":248},", Any]]:\n",[238,1641,1642],{"class":240,"line":457},[238,1643,1644],{"class":311},"    \"\"\"Load a JSON state file under a lock and save it atomically on success.\"\"\"\n",[238,1646,1647,1649,1651,1653,1655,1658,1660,1662,1665,1667],{"class":240,"line":466},[238,1648,327],{"class":248},[238,1650,330],{"class":244},[238,1652,1030],{"class":248},[238,1654,1007],{"class":244},[238,1656,1657],{"class":244}," f",[238,1659,788],{"class":311},[238,1661,367],{"class":299},[238,1663,1664],{"class":248},"name",[238,1666,373],{"class":299},[238,1668,1669],{"class":311},".json\"\n",[238,1671,1672,1674,1677,1679,1682,1684,1687,1689,1691,1693,1695],{"class":240,"line":483},[238,1673,1131],{"class":244},[238,1675,1676],{"class":248}," FileLock(",[238,1678,300],{"class":299},[238,1680,1681],{"class":248},"(path) ",[238,1683,809],{"class":244},[238,1685,1686],{"class":311}," \".lock\"",[238,1688,152],{"class":248},[238,1690,1322],{"class":347},[238,1692,330],{"class":244},[238,1694,1327],{"class":299},[238,1696,1697],{"class":248},"):\n",[238,1699,1700,1703,1705,1708,1710,1712,1714,1717,1719,1722,1725],{"class":240,"line":489},[238,1701,1702],{"class":248},"        data ",[238,1704,330],{"class":244},[238,1706,1707],{"class":248}," json.loads(path.read_text(",[238,1709,414],{"class":347},[238,1711,330],{"class":244},[238,1713,825],{"class":311},[238,1715,1716],{"class":248},")) ",[238,1718,754],{"class":244},[238,1720,1721],{"class":248}," path.exists() ",[238,1723,1724],{"class":244},"else",[238,1726,1727],{"class":248}," {}\n",[238,1729,1730,1733],{"class":240,"line":500},[238,1731,1732],{"class":244},"        yield",[238,1734,1735],{"class":248}," data\n",[238,1737,1738,1741,1744,1746,1749,1751,1754,1756,1758,1760,1762,1764,1766,1768],{"class":240,"line":516},[238,1739,1740],{"class":248},"        write_atomic(path, json.dumps(data, ",[238,1742,1743],{"class":347},"indent",[238,1745,330],{"class":244},[238,1747,1748],{"class":299},"2",[238,1750,152],{"class":248},[238,1752,1753],{"class":347},"sort_keys",[238,1755,330],{"class":244},[238,1757,511],{"class":299},[238,1759,430],{"class":248},[238,1761,809],{"class":244},[238,1763,812],{"class":311},[238,1765,791],{"class":299},[238,1767,788],{"class":311},[238,1769,389],{"class":248},[10,1771,1772,1773,1776],{},"A command that uses ",[14,1774,1775],{},"with locked_state(\"history\") as state: state[\"runs\"] = state.get(\"runs\", 0) + 1"," gets correct behaviour under crashes, concurrency and every operating system, without any of those concerns appearing in its own code. If the command raises inside the block, nothing is written and the previous state is kept — a transaction in miniature.",[10,1778,1779,1780,1783,1784,1787,1788,1791,1792,1795,1796,82],{},"Testing that module is straightforward because everything is a path. Point ",[14,1781,1782],{},"PlatformDirs"," at ",[14,1785,1786],{},"tmp_path"," in tests (by patching the module-level ",[14,1789,1790],{},"dirs"," or setting ",[14,1793,1794],{},"XDG_*"," variables on Linux) and assert on file contents; the techniques are in ",[26,1797,1799],{"href":1798},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","mocking filesystem and network in CLI tests",[46,1801,1803],{"id":1802},"key-takeaways","Key takeaways",[51,1805,1806,1814,1825,1831,1836,1839],{},[54,1807,1808,1809,152,1811,82],{},"Treat every write to a file that matters as a transaction: temp file in the same directory, ",[14,1810,63],{},[14,1812,1813],{},"os.replace",[54,1815,1816,1817,1819,1820,1822,1823,82],{},"Parse paths into ",[14,1818,76],{}," at the edge and keep them as ",[14,1821,552],{}," objects; always pass ",[14,1824,81],{},[54,1826,1827,1828,1830],{},"Keep the user's files and your tool's files separate, and put the latter in ",[14,1829,91],{}," config, state and cache directories.",[54,1832,73,1833,1835],{},[14,1834,100],{}," with context managers for scratch space; never invent predictable temp names.",[54,1837,1838],{},"Add an OS-level lock around read-modify-write cycles that concurrent runs can reach.",[54,1840,1841],{},"Wrap all of it in one small module so commands never handle raw file writes.",[46,1843,1845],{"id":1844},"frequently-asked-questions","Frequently asked questions",[1847,1848,1850,1851,1853],"h3",{"id":1849},"is-osreplace-really-atomic-on-windows","Is ",[14,1852,67],{}," really atomic on Windows?",[10,1855,1856,1857,1860,1861,1864,1865,1868],{},"It performs the replacement with a single ",[14,1858,1859],{},"MoveFileEx"," call using ",[14,1862,1863],{},"MOVEFILE_REPLACE_EXISTING",", which is atomic on NTFS for files on the same volume. It can fail with ",[14,1866,1867],{},"PermissionError"," if another process has the target open without sharing delete access — antivirus scanners and some editors do this — so wrap it in a short retry loop if your users are on Windows.",[1847,1870,1872,1873,1875],{"id":1871},"do-i-need-fsync-for-every-file-my-cli-writes","Do I need ",[14,1874,63],{}," for every file my CLI writes?",[10,1877,1878,1879,1881],{},"For files whose loss would hurt — configuration, credentials metadata, state that is expensive to rebuild — yes. For caches and large generated outputs that can be recreated, skipping it is a reasonable speed trade-off. The atomic rename still protects you from torn files after a crash of your process; ",[14,1880,63],{}," protects against power loss and kernel crashes.",[1847,1883,1885],{"id":1884},"should-my-tool-follow-symlinks-when-writing","Should my tool follow symlinks when writing?",[10,1887,1888,1889,1892],{},"When the user's config file is a symlink into a dotfiles repository, replacing the symlink with a regular file breaks their setup. Resolve the path with ",[14,1890,1891],{},"Path.resolve()"," before an atomic write so the new file replaces the link's target, and mention it in your docs.",[1847,1894,1896],{"id":1895},"where-should-a-cli-keep-files-when-running-in-a-container","Where should a CLI keep files when running in a container?",[10,1898,1899,1900,1902,1903,1906,1907,1909],{},"The same ",[14,1901,91],{}," locations work, because ",[14,1904,1905],{},"HOME"," and the ",[14,1908,1794],{}," variables are set in most images. For tools that run as a non-root user with a read-only root filesystem, let every directory be overridden by an environment variable or flag so operators can point it at a mounted volume.",[1847,1911,1913],{"id":1912},"how-do-i-make-file-operations-testable","How do I make file operations testable?",[10,1915,1916,1917,1919],{},"Accept paths as parameters rather than computing them deep inside functions, and route your tool's own directories through one module you can patch. Pytest's ",[14,1918,1786],{}," fixture then gives each test a fresh directory, and nothing touches the real home directory.",[46,1921,1923],{"id":1922},"related","Related",[51,1925,1926,1931,1937,1941,1945,1949,1954,1960],{},[54,1927,1928,1929],{},"Up: ",[26,1930,29],{"href":28},[54,1932,1933,1934],{},"Down: ",[26,1935,1936],{"href":142},"Writing files atomically in Python CLIs",[54,1938,1933,1939],{},[26,1940,171],{"href":170},[54,1942,1933,1943],{},[26,1944,212],{"href":211},[54,1946,1933,1947],{},[26,1948,201],{"href":200},[54,1950,1933,1951],{},[26,1952,1953],{"href":181},"File locking for concurrent CLI runs",[54,1955,1956,1957],{},"Sideways: ",[26,1958,1959],{"href":33},"Running subprocesses from Python CLIs",[54,1961,1956,1962],{},[26,1963,1964],{"href":1053},"Handling configuration files and environment variables",[1966,1967,1968],"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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":234,"searchDepth":252,"depth":252,"links":1970},[1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1990],{"id":48,"depth":252,"text":49},{"id":122,"depth":252,"text":123},{"id":216,"depth":252,"text":217},{"id":543,"depth":252,"text":544},{"id":928,"depth":252,"text":929},{"id":1058,"depth":252,"text":1059},{"id":1261,"depth":252,"text":1262},{"id":1403,"depth":252,"text":1404},{"id":1473,"depth":252,"text":1474},{"id":1802,"depth":252,"text":1803},{"id":1844,"depth":252,"text":1845,"children":1982},[1983,1985,1987,1988,1989],{"id":1849,"depth":260,"text":1984},"Is os.replace() really atomic on Windows?",{"id":1871,"depth":260,"text":1986},"Do I need fsync for every file my CLI writes?",{"id":1884,"depth":260,"text":1885},{"id":1895,"depth":260,"text":1896},{"id":1912,"depth":260,"text":1913},{"id":1922,"depth":252,"text":1923},"2026-09-18","Handle files in Python CLIs without corrupting them: atomic writes, pathlib everywhere, per-user config and cache dirs, safe temp files and cross-run locking.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes",{"title":5,"description":1992},"cli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Findex",[2001,23,2002,91,2003],"filesystem","atomic-writes","locking","nKPa1VfS2ad8JegV7FO044f-8C710Dz-h8qIADw3YaE",[2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195,2198,2201,2204,2207,2210,2213,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2279,2282,2285,2288,2291,2294,2297,2300,2303,2306,2309,2312,2315,2318,2321,2324,2327,2330,2333,2336,2339,2342,2345,2348,2351,2354,2357,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549],{"path":2007,"title":2008},"\u002Fabout","About Python CLI Toolcraft",{"path":2010,"title":2011},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2013,"title":2014},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2016,"title":2017},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2019,"title":2020},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2022,"title":2023},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2025,"title":2026},"\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":2028,"title":2029},"\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":2031,"title":2032},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2034,"title":2035},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2037,"title":2038},"\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":2040,"title":2041},"\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":2043,"title":2044},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2046,"title":2047},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2049,"title":2050},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2052,"title":2053},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2055,"title":2056},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2058,"title":2059},"\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":2061,"title":2062},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2064,"title":2065},"\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":2067,"title":2068},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2070,"title":2071},"\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":2073,"title":2074},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2076,"title":2077},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2079,"title":2080},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2082,"title":2083},"\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":2085,"title":2086},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2088,"title":2089},"\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":2091,"title":2092},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2094,"title":2095},"\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":2097,"title":2098},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2100,"title":2101},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2103,"title":2104},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2106,"title":2107},"\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":2109,"title":2110},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2112,"title":2113},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2115,"title":2116},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2118,"title":2119},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2121,"title":2122},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2124,"title":2125},"\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":2127,"title":2128},"\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":2130,"title":2131},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2133,"title":2134},"\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":2136,"title":2137},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2139,"title":2140},"\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":2142,"title":2143},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2145,"title":2146},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2148,"title":2149},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2151,"title":2152},"\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":2154,"title":2155},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2157,"title":2158},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2160,"title":2161},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2163,"title":2164},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2166,"title":2167},"\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":2169,"title":2170},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2172,"title":2173},"\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":2175,"title":2176},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2178,"title":2179},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2181,"title":2182},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2184,"title":2185},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2187,"title":2188},"\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":2190,"title":2191},"\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":2193,"title":2194},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2196,"title":2197},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2199,"title":2200},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2202,"title":2203},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2205,"title":2206},"\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":2208,"title":2209},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2211,"title":2212},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":1997,"title":5},{"path":2215,"title":2216},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2218,"title":2219},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2221,"title":2222},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2224,"title":2225},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2227,"title":2228},"\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":2230,"title":2231},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2233,"title":2234},"\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":2236,"title":2237},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2239,"title":2240},"\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":2242,"title":2243},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2245,"title":2246},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2248,"title":2249},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2251,"title":2252},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2254,"title":2255},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2257,"title":2258},"\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":2260,"title":2261},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2263,"title":2264},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2266,"title":2267},"\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":2269,"title":2270},"\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":2272,"title":2273},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2275,"title":2276},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1007,"title":2278},"Python CLI Toolcraft",{"path":2280,"title":2281},"\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":2283,"title":2284},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2286,"title":2287},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2289,"title":2290},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2292,"title":2293},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2295,"title":2296},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2298,"title":2299},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2301,"title":2302},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2304,"title":2305},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2307,"title":2308},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2310,"title":2311},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2313,"title":2314},"\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":2316,"title":2317},"\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":2319,"title":2320},"\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":2322,"title":2323},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2325,"title":2326},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2328,"title":2329},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2331,"title":2332},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2334,"title":2335},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2337,"title":2338},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2340,"title":2341},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2343,"title":2344},"\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":2346,"title":2347},"\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":2349,"title":2350},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2352,"title":2353},"\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":2355,"title":2356},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2358,"title":2359},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2361,"title":2362},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2364,"title":2365},"\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":2367,"title":2368},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2370,"title":2371},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2373,"title":2374},"\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":2376,"title":2377},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2379,"title":2380},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2382,"title":2383},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2385,"title":2386},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2388,"title":2389},"\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":2391,"title":2392},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2394,"title":2395},"\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":2397,"title":2398},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2400,"title":2401},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2403,"title":2404},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2406,"title":2407},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2409,"title":2410},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2412,"title":2413},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2415,"title":2416},"\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":2418,"title":2419},"\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":2421,"title":2422},"\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":2424,"title":2425},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2427,"title":2428},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2430,"title":2431},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2433,"title":2434},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2436,"title":2437},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2439,"title":2440},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2442,"title":2443},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2445,"title":2446},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2448,"title":2449},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2451,"title":2452},"\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":2454,"title":2455},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2457,"title":2458},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2460,"title":2461},"\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":2463,"title":2464},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2466,"title":2467},"\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":2469,"title":2470},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2472,"title":2473},"\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":2475,"title":2476},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2478,"title":2479},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2481,"title":2482},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2484,"title":2485},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2487,"title":2488},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2490,"title":2491},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2493,"title":2494},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2496,"title":2497},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2499,"title":2500},"\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":2502,"title":2503},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2505,"title":2506},"\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":2508,"title":2509},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2511,"title":2512},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2514,"title":2515},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2517,"title":2518},"\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":2520,"title":2521},"\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":2523,"title":2524},"\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":2526,"title":2527},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2529,"title":2530},"\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":2532,"title":2533},"\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":2535,"title":2536},"\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":2538,"title":2539},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2541,"title":2542},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2544,"title":2545},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2547,"title":2548},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2550,"title":2551},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905049]