[{"data":1,"prerenderedAt":2980},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F":3,"content-directory":2434},{"id":4,"title":5,"body":6,"date":2419,"description":2420,"difficulty":2421,"draft":2422,"extension":2423,"meta":2424,"navigation":145,"path":2425,"seo":2426,"stem":2427,"tags":2428,"updated":2419,"__hash__":2433},"content\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002Findex.md","Writing Files Atomically in Python CLIs",{"type":7,"value":8,"toc":2400},"minimark",[9,36,41,54,58,85,89,100,104,1133,1151,1156,1261,1264,1268,1271,1616,1637,1640,1644,1693,1697,1704,2273,2279,2283,2300,2304,2312,2319,2323,2333,2337,2343,2347,2363,2367,2396],[10,11,12,13,17,18,21,22,25,26,29,30,35],"p",{},"A user runs ",[14,15,16],"code",{},"mytool config set region eu-west-1",", presses Ctrl+C a moment too early — or the laptop battery dies, or the disk fills — and the next run fails with ",[14,19,20],{},"TOMLDecodeError: Expected '=' after a key"," because ",[14,23,24],{},"config.toml"," is now empty. The code that wrote it looked fine: ",[14,27,28],{},"path.write_text(new_content)",". The problem is that writing a file in place is not one operation but several, and a failure between them leaves the file in a state that never should have existed. This guide builds a small, tested helper that makes every important write all-or-nothing, and shows where to use it in a CLI. It is the detailed companion to ",[31,32,34],"a",{"href":33},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","filesystem paths and atomic writes",".",[37,38,40],"h2",{"id":39},"prerequisites","Prerequisites",[42,43,44,48,51],"ul",{},[45,46,47],"li",{},"Python 3.10 or newer.",[45,49,50],{},"A CLI that writes files it or its users care about: config, state, caches that are expensive to rebuild, generated outputs.",[45,52,53],{},"pytest for the tests.",[37,55,57],{"id":56},"what-goes-wrong-with-an-in-place-write","What goes wrong with an in-place write",[10,59,60,63,64,67,68,72,73,76,77,80,81,84],{},[14,61,62],{},"Path.write_text()"," opens the file with mode ",[14,65,66],{},"\"w\"",", which ",[69,70,71],"strong",{},"truncates it to zero bytes immediately",", then writes the new content, then closes it. Only after ",[14,74,75],{},"close()"," — and, for durability, after the operating system flushes its cache to disk — is the new content safely in place. Anything that stops the process between the truncate and the end leaves an empty or partial file: an exception, a ",[14,78,79],{},"KeyboardInterrupt",", a ",[14,82,83],{},"SIGKILL"," from a timeout, the out-of-memory killer, a full disk, or a power cut.",[86,87],"inline-diagram",{"name":88},"fs-crash-compare",[10,90,91,92,95,96,99],{},"The fix is a pattern databases and editors have used for decades. Write the new content to a ",[69,93,94],{},"different"," file, make sure it is completely on disk, then atomically ",[69,97,98],{},"rename"," that file over the original. On POSIX systems and on NTFS, a rename within one filesystem either happens entirely or not at all. Any reader — including your own tool on its next run — sees the complete old file or the complete new one.",[37,101,103],{"id":102},"the-recipe","The recipe",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Ffiles.py\nfrom __future__ import annotations\n\nimport json\nimport os\nimport tempfile\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import IO, Any\n\n\n@contextmanager\ndef atomic_open(path: str | os.PathLike[str], mode: str = \"w\", *,\n                encoding: str | None = \"utf-8\", newline: str | None = \"\",\n                fsync: bool = True) -> Iterator[IO[Any]]:\n    \"\"\"Yield a file handle whose contents replace `path` only on success.\"\"\"\n    if mode not in (\"w\", \"wb\"):\n        raise ValueError(\"atomic_open supports 'w' and 'wb'\")\n    target = Path(path).resolve()           # write through symlinks, not over them\n    target.parent.mkdir(parents=True, exist_ok=True)\n    fd, tmp_name = tempfile.mkstemp(dir=target.parent, prefix=f\".{target.name}.\", suffix=\".tmp\")\n    tmp = Path(tmp_name)\n    try:\n        if \"b\" in mode:\n            fh = os.fdopen(fd, mode)\n        else:\n            fh = os.fdopen(fd, mode, encoding=encoding, newline=newline)\n        with fh:\n            yield fh\n            fh.flush()\n            if fsync:\n                os.fsync(fh.fileno())\n        try:\n            os.chmod(tmp, target.stat().st_mode & 0o7777)   # keep existing permissions\n        except FileNotFoundError:\n            os.chmod(tmp, 0o666 & ~_umask())                # what open() would have used\n        os.replace(tmp, target)\n        if fsync and os.name == \"posix\":\n            _fsync_dir(target.parent)\n    except BaseException:\n        tmp.unlink(missing_ok=True)\n        raise\n\n\ndef write_text_atomic(path: str | os.PathLike[str], text: str, **kw: Any) -> None:\n    with atomic_open(path, \"w\", **kw) as fh:\n        fh.write(text)\n\n\ndef write_bytes_atomic(path: str | os.PathLike[str], data: bytes, **kw: Any) -> None:\n    with atomic_open(path, \"wb\", encoding=None, newline=None, **kw) as fh:\n        fh.write(data)\n\n\ndef write_json_atomic(path: str | os.PathLike[str], obj: Any) -> None:\n    with atomic_open(path, \"w\") as fh:\n        json.dump(obj, fh, indent=2, sort_keys=True, ensure_ascii=False)\n        fh.write(\"\\n\")\n\n\ndef _umask() -> int:\n    current = os.umask(0)\n    os.umask(current)\n    return current\n\n\ndef _fsync_dir(directory: Path) -> None:\n    \"\"\"Persist the rename itself (POSIX); a directory is a file of names.\"\"\"\n    dir_fd = os.open(directory, os.O_RDONLY)\n    try:\n        os.fsync(dir_fd)\n    finally:\n        os.close(dir_fd)\n","python","",[14,112,113,122,140,147,156,164,172,185,198,211,227,232,237,244,288,322,345,351,379,397,412,438,492,503,512,526,537,545,571,580,589,595,604,610,618,639,650,674,680,702,708,719,734,740,745,750,786,809,815,820,825,859,894,900,905,910,935,951,986,1002,1007,1012,1028,1044,1050,1059,1064,1069,1084,1090,1106,1113,1119,1127],{"__ignoreMap":110},[114,115,118],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"sJ8bj","# src\u002Fmytool\u002Ffiles.py\n",[114,123,125,129,133,136],{"class":116,"line":124},2,[114,126,128],{"class":127},"szBVR","from",[114,130,132],{"class":131},"sj4cs"," __future__",[114,134,135],{"class":127}," import",[114,137,139],{"class":138},"sVt8B"," annotations\n",[114,141,143],{"class":116,"line":142},3,[114,144,146],{"emptyLinePlaceholder":145},true,"\n",[114,148,150,153],{"class":116,"line":149},4,[114,151,152],{"class":127},"import",[114,154,155],{"class":138}," json\n",[114,157,159,161],{"class":116,"line":158},5,[114,160,152],{"class":127},[114,162,163],{"class":138}," os\n",[114,165,167,169],{"class":116,"line":166},6,[114,168,152],{"class":127},[114,170,171],{"class":138}," tempfile\n",[114,173,175,177,180,182],{"class":116,"line":174},7,[114,176,128],{"class":127},[114,178,179],{"class":138}," collections.abc ",[114,181,152],{"class":127},[114,183,184],{"class":138}," Iterator\n",[114,186,188,190,193,195],{"class":116,"line":187},8,[114,189,128],{"class":127},[114,191,192],{"class":138}," contextlib ",[114,194,152],{"class":127},[114,196,197],{"class":138}," contextmanager\n",[114,199,201,203,206,208],{"class":116,"line":200},9,[114,202,128],{"class":127},[114,204,205],{"class":138}," pathlib ",[114,207,152],{"class":127},[114,209,210],{"class":138}," Path\n",[114,212,214,216,219,221,224],{"class":116,"line":213},10,[114,215,128],{"class":127},[114,217,218],{"class":138}," typing ",[114,220,152],{"class":127},[114,222,223],{"class":131}," IO",[114,225,226],{"class":138},", Any\n",[114,228,230],{"class":116,"line":229},11,[114,231,146],{"emptyLinePlaceholder":145},[114,233,235],{"class":116,"line":234},12,[114,236,146],{"emptyLinePlaceholder":145},[114,238,240],{"class":116,"line":239},13,[114,241,243],{"class":242},"sScJk","@contextmanager\n",[114,245,247,250,253,256,259,262,265,267,270,272,275,279,282,285],{"class":116,"line":246},14,[114,248,249],{"class":127},"def",[114,251,252],{"class":242}," atomic_open",[114,254,255],{"class":138},"(path: ",[114,257,258],{"class":131},"str",[114,260,261],{"class":127}," |",[114,263,264],{"class":138}," os.PathLike[",[114,266,258],{"class":131},[114,268,269],{"class":138},"], mode: ",[114,271,258],{"class":131},[114,273,274],{"class":127}," =",[114,276,278],{"class":277},"sZZnC"," \"w\"",[114,280,281],{"class":138},", ",[114,283,284],{"class":127},"*",[114,286,287],{"class":138},",\n",[114,289,291,294,296,298,301,303,306,309,311,313,315,317,320],{"class":116,"line":290},15,[114,292,293],{"class":138},"                encoding: ",[114,295,258],{"class":131},[114,297,261],{"class":127},[114,299,300],{"class":131}," None",[114,302,274],{"class":127},[114,304,305],{"class":277}," \"utf-8\"",[114,307,308],{"class":138},", newline: ",[114,310,258],{"class":131},[114,312,261],{"class":127},[114,314,300],{"class":131},[114,316,274],{"class":127},[114,318,319],{"class":277}," \"\"",[114,321,287],{"class":138},[114,323,325,328,331,333,336,339,342],{"class":116,"line":324},16,[114,326,327],{"class":138},"                fsync: ",[114,329,330],{"class":131},"bool",[114,332,274],{"class":127},[114,334,335],{"class":131}," True",[114,337,338],{"class":138},") -> Iterator[",[114,340,341],{"class":131},"IO",[114,343,344],{"class":138},"[Any]]:\n",[114,346,348],{"class":116,"line":347},17,[114,349,350],{"class":277},"    \"\"\"Yield a file handle whose contents replace `path` only on success.\"\"\"\n",[114,352,354,357,360,363,366,369,371,373,376],{"class":116,"line":353},18,[114,355,356],{"class":127},"    if",[114,358,359],{"class":138}," mode ",[114,361,362],{"class":127},"not",[114,364,365],{"class":127}," in",[114,367,368],{"class":138}," (",[114,370,66],{"class":277},[114,372,281],{"class":138},[114,374,375],{"class":277},"\"wb\"",[114,377,378],{"class":138},"):\n",[114,380,382,385,388,391,394],{"class":116,"line":381},19,[114,383,384],{"class":127},"        raise",[114,386,387],{"class":131}," ValueError",[114,389,390],{"class":138},"(",[114,392,393],{"class":277},"\"atomic_open supports 'w' and 'wb'\"",[114,395,396],{"class":138},")\n",[114,398,400,403,406,409],{"class":116,"line":399},20,[114,401,402],{"class":138},"    target ",[114,404,405],{"class":127},"=",[114,407,408],{"class":138}," Path(path).resolve()           ",[114,410,411],{"class":120},"# write through symlinks, not over them\n",[114,413,415,418,422,424,427,429,432,434,436],{"class":116,"line":414},21,[114,416,417],{"class":138},"    target.parent.mkdir(",[114,419,421],{"class":420},"s4XuR","parents",[114,423,405],{"class":127},[114,425,426],{"class":131},"True",[114,428,281],{"class":138},[114,430,431],{"class":420},"exist_ok",[114,433,405],{"class":127},[114,435,426],{"class":131},[114,437,396],{"class":138},[114,439,441,444,446,449,452,454,457,460,462,465,468,471,474,477,480,482,485,487,490],{"class":116,"line":440},22,[114,442,443],{"class":138},"    fd, tmp_name ",[114,445,405],{"class":127},[114,447,448],{"class":138}," tempfile.mkstemp(",[114,450,451],{"class":420},"dir",[114,453,405],{"class":127},[114,455,456],{"class":138},"target.parent, ",[114,458,459],{"class":420},"prefix",[114,461,405],{"class":127},[114,463,464],{"class":127},"f",[114,466,467],{"class":277},"\".",[114,469,470],{"class":131},"{",[114,472,473],{"class":138},"target.name",[114,475,476],{"class":131},"}",[114,478,479],{"class":277},".\"",[114,481,281],{"class":138},[114,483,484],{"class":420},"suffix",[114,486,405],{"class":127},[114,488,489],{"class":277},"\".tmp\"",[114,491,396],{"class":138},[114,493,495,498,500],{"class":116,"line":494},23,[114,496,497],{"class":138},"    tmp ",[114,499,405],{"class":127},[114,501,502],{"class":138}," Path(tmp_name)\n",[114,504,506,509],{"class":116,"line":505},24,[114,507,508],{"class":127},"    try",[114,510,511],{"class":138},":\n",[114,513,515,518,521,523],{"class":116,"line":514},25,[114,516,517],{"class":127},"        if",[114,519,520],{"class":277}," \"b\"",[114,522,365],{"class":127},[114,524,525],{"class":138}," mode:\n",[114,527,529,532,534],{"class":116,"line":528},26,[114,530,531],{"class":138},"            fh ",[114,533,405],{"class":127},[114,535,536],{"class":138}," os.fdopen(fd, mode)\n",[114,538,540,543],{"class":116,"line":539},27,[114,541,542],{"class":127},"        else",[114,544,511],{"class":138},[114,546,548,550,552,555,558,560,563,566,568],{"class":116,"line":547},28,[114,549,531],{"class":138},[114,551,405],{"class":127},[114,553,554],{"class":138}," os.fdopen(fd, mode, ",[114,556,557],{"class":420},"encoding",[114,559,405],{"class":127},[114,561,562],{"class":138},"encoding, ",[114,564,565],{"class":420},"newline",[114,567,405],{"class":127},[114,569,570],{"class":138},"newline)\n",[114,572,574,577],{"class":116,"line":573},29,[114,575,576],{"class":127},"        with",[114,578,579],{"class":138}," fh:\n",[114,581,583,586],{"class":116,"line":582},30,[114,584,585],{"class":127},"            yield",[114,587,588],{"class":138}," fh\n",[114,590,592],{"class":116,"line":591},31,[114,593,594],{"class":138},"            fh.flush()\n",[114,596,598,601],{"class":116,"line":597},32,[114,599,600],{"class":127},"            if",[114,602,603],{"class":138}," fsync:\n",[114,605,607],{"class":116,"line":606},33,[114,608,609],{"class":138},"                os.fsync(fh.fileno())\n",[114,611,613,616],{"class":116,"line":612},34,[114,614,615],{"class":127},"        try",[114,617,511],{"class":138},[114,619,621,624,627,630,633,636],{"class":116,"line":620},35,[114,622,623],{"class":138},"            os.chmod(tmp, target.stat().st_mode ",[114,625,626],{"class":127},"&",[114,628,629],{"class":127}," 0o",[114,631,632],{"class":131},"7777",[114,634,635],{"class":138},")   ",[114,637,638],{"class":120},"# keep existing permissions\n",[114,640,642,645,648],{"class":116,"line":641},36,[114,643,644],{"class":127},"        except",[114,646,647],{"class":131}," FileNotFoundError",[114,649,511],{"class":138},[114,651,653,656,659,662,665,668,671],{"class":116,"line":652},37,[114,654,655],{"class":138},"            os.chmod(tmp, ",[114,657,658],{"class":127},"0o",[114,660,661],{"class":131},"666",[114,663,664],{"class":127}," &",[114,666,667],{"class":127}," ~",[114,669,670],{"class":138},"_umask())                ",[114,672,673],{"class":120},"# what open() would have used\n",[114,675,677],{"class":116,"line":676},38,[114,678,679],{"class":138},"        os.replace(tmp, target)\n",[114,681,683,685,688,691,694,697,700],{"class":116,"line":682},39,[114,684,517],{"class":127},[114,686,687],{"class":138}," fsync ",[114,689,690],{"class":127},"and",[114,692,693],{"class":138}," os.name ",[114,695,696],{"class":127},"==",[114,698,699],{"class":277}," \"posix\"",[114,701,511],{"class":138},[114,703,705],{"class":116,"line":704},40,[114,706,707],{"class":138},"            _fsync_dir(target.parent)\n",[114,709,711,714,717],{"class":116,"line":710},41,[114,712,713],{"class":127},"    except",[114,715,716],{"class":131}," BaseException",[114,718,511],{"class":138},[114,720,722,725,728,730,732],{"class":116,"line":721},42,[114,723,724],{"class":138},"        tmp.unlink(",[114,726,727],{"class":420},"missing_ok",[114,729,405],{"class":127},[114,731,426],{"class":131},[114,733,396],{"class":138},[114,735,737],{"class":116,"line":736},43,[114,738,739],{"class":127},"        raise\n",[114,741,743],{"class":116,"line":742},44,[114,744,146],{"emptyLinePlaceholder":145},[114,746,748],{"class":116,"line":747},45,[114,749,146],{"emptyLinePlaceholder":145},[114,751,753,755,758,760,762,764,766,768,771,773,775,778,781,784],{"class":116,"line":752},46,[114,754,249],{"class":127},[114,756,757],{"class":242}," write_text_atomic",[114,759,255],{"class":138},[114,761,258],{"class":131},[114,763,261],{"class":127},[114,765,264],{"class":138},[114,767,258],{"class":131},[114,769,770],{"class":138},"], text: ",[114,772,258],{"class":131},[114,774,281],{"class":138},[114,776,777],{"class":127},"**",[114,779,780],{"class":138},"kw: Any) -> ",[114,782,783],{"class":131},"None",[114,785,511],{"class":138},[114,787,789,792,795,797,799,801,804,807],{"class":116,"line":788},47,[114,790,791],{"class":127},"    with",[114,793,794],{"class":138}," atomic_open(path, ",[114,796,66],{"class":277},[114,798,281],{"class":138},[114,800,777],{"class":127},[114,802,803],{"class":138},"kw) ",[114,805,806],{"class":127},"as",[114,808,579],{"class":138},[114,810,812],{"class":116,"line":811},48,[114,813,814],{"class":138},"        fh.write(text)\n",[114,816,818],{"class":116,"line":817},49,[114,819,146],{"emptyLinePlaceholder":145},[114,821,823],{"class":116,"line":822},50,[114,824,146],{"emptyLinePlaceholder":145},[114,826,828,830,833,835,837,839,841,843,846,849,851,853,855,857],{"class":116,"line":827},51,[114,829,249],{"class":127},[114,831,832],{"class":242}," write_bytes_atomic",[114,834,255],{"class":138},[114,836,258],{"class":131},[114,838,261],{"class":127},[114,840,264],{"class":138},[114,842,258],{"class":131},[114,844,845],{"class":138},"], data: ",[114,847,848],{"class":131},"bytes",[114,850,281],{"class":138},[114,852,777],{"class":127},[114,854,780],{"class":138},[114,856,783],{"class":131},[114,858,511],{"class":138},[114,860,862,864,866,868,870,872,874,876,878,880,882,884,886,888,890,892],{"class":116,"line":861},52,[114,863,791],{"class":127},[114,865,794],{"class":138},[114,867,375],{"class":277},[114,869,281],{"class":138},[114,871,557],{"class":420},[114,873,405],{"class":127},[114,875,783],{"class":131},[114,877,281],{"class":138},[114,879,565],{"class":420},[114,881,405],{"class":127},[114,883,783],{"class":131},[114,885,281],{"class":138},[114,887,777],{"class":127},[114,889,803],{"class":138},[114,891,806],{"class":127},[114,893,579],{"class":138},[114,895,897],{"class":116,"line":896},53,[114,898,899],{"class":138},"        fh.write(data)\n",[114,901,903],{"class":116,"line":902},54,[114,904,146],{"emptyLinePlaceholder":145},[114,906,908],{"class":116,"line":907},55,[114,909,146],{"emptyLinePlaceholder":145},[114,911,913,915,918,920,922,924,926,928,931,933],{"class":116,"line":912},56,[114,914,249],{"class":127},[114,916,917],{"class":242}," write_json_atomic",[114,919,255],{"class":138},[114,921,258],{"class":131},[114,923,261],{"class":127},[114,925,264],{"class":138},[114,927,258],{"class":131},[114,929,930],{"class":138},"], obj: Any) -> ",[114,932,783],{"class":131},[114,934,511],{"class":138},[114,936,938,940,942,944,947,949],{"class":116,"line":937},57,[114,939,791],{"class":127},[114,941,794],{"class":138},[114,943,66],{"class":277},[114,945,946],{"class":138},") ",[114,948,806],{"class":127},[114,950,579],{"class":138},[114,952,954,957,960,962,965,967,970,972,974,976,979,981,984],{"class":116,"line":953},58,[114,955,956],{"class":138},"        json.dump(obj, fh, ",[114,958,959],{"class":420},"indent",[114,961,405],{"class":127},[114,963,964],{"class":131},"2",[114,966,281],{"class":138},[114,968,969],{"class":420},"sort_keys",[114,971,405],{"class":127},[114,973,426],{"class":131},[114,975,281],{"class":138},[114,977,978],{"class":420},"ensure_ascii",[114,980,405],{"class":127},[114,982,983],{"class":131},"False",[114,985,396],{"class":138},[114,987,989,992,995,998,1000],{"class":116,"line":988},59,[114,990,991],{"class":138},"        fh.write(",[114,993,994],{"class":277},"\"",[114,996,997],{"class":131},"\\n",[114,999,994],{"class":277},[114,1001,396],{"class":138},[114,1003,1005],{"class":116,"line":1004},60,[114,1006,146],{"emptyLinePlaceholder":145},[114,1008,1010],{"class":116,"line":1009},61,[114,1011,146],{"emptyLinePlaceholder":145},[114,1013,1015,1017,1020,1023,1026],{"class":116,"line":1014},62,[114,1016,249],{"class":127},[114,1018,1019],{"class":242}," _umask",[114,1021,1022],{"class":138},"() -> ",[114,1024,1025],{"class":131},"int",[114,1027,511],{"class":138},[114,1029,1031,1034,1036,1039,1042],{"class":116,"line":1030},63,[114,1032,1033],{"class":138},"    current ",[114,1035,405],{"class":127},[114,1037,1038],{"class":138}," os.umask(",[114,1040,1041],{"class":131},"0",[114,1043,396],{"class":138},[114,1045,1047],{"class":116,"line":1046},64,[114,1048,1049],{"class":138},"    os.umask(current)\n",[114,1051,1053,1056],{"class":116,"line":1052},65,[114,1054,1055],{"class":127},"    return",[114,1057,1058],{"class":138}," current\n",[114,1060,1062],{"class":116,"line":1061},66,[114,1063,146],{"emptyLinePlaceholder":145},[114,1065,1067],{"class":116,"line":1066},67,[114,1068,146],{"emptyLinePlaceholder":145},[114,1070,1072,1074,1077,1080,1082],{"class":116,"line":1071},68,[114,1073,249],{"class":127},[114,1075,1076],{"class":242}," _fsync_dir",[114,1078,1079],{"class":138},"(directory: Path) -> ",[114,1081,783],{"class":131},[114,1083,511],{"class":138},[114,1085,1087],{"class":116,"line":1086},69,[114,1088,1089],{"class":277},"    \"\"\"Persist the rename itself (POSIX); a directory is a file of names.\"\"\"\n",[114,1091,1093,1096,1098,1101,1104],{"class":116,"line":1092},70,[114,1094,1095],{"class":138},"    dir_fd ",[114,1097,405],{"class":127},[114,1099,1100],{"class":138}," os.open(directory, os.",[114,1102,1103],{"class":131},"O_RDONLY",[114,1105,396],{"class":138},[114,1107,1109,1111],{"class":116,"line":1108},71,[114,1110,508],{"class":127},[114,1112,511],{"class":138},[114,1114,1116],{"class":116,"line":1115},72,[114,1117,1118],{"class":138},"        os.fsync(dir_fd)\n",[114,1120,1122,1125],{"class":116,"line":1121},73,[114,1123,1124],{"class":127},"    finally",[114,1126,511],{"class":138},[114,1128,1130],{"class":116,"line":1129},74,[114,1131,1132],{"class":138},"        os.close(dir_fd)\n",[10,1134,1135,1136,281,1139,1142,1143,1146,1147,1150],{},"The context-manager form matters more than it looks. Serialisers such as ",[14,1137,1138],{},"json.dump",[14,1140,1141],{},"tomli_w.dump"," or ",[14,1144,1145],{},"csv.writer"," want a file handle, and building the whole output as a string first doubles peak memory for large files. With ",[14,1148,1149],{},"atomic_open",", they write straight into the temporary file, and if the serialiser raises halfway — a value that is not JSON-serialisable, say — the exception propagates, the temporary file is deleted, and the original is untouched.",[1152,1153,1155],"h3",{"id":1154},"why-each-step-is-there","Why each step is there",[42,1157,1158,1190,1206,1230,1238,1253],{},[45,1159,1160,1165,1166,1169,1170,1173,1174,1177,1178,1181,1182,1185,1186,1189],{},[69,1161,1162],{},[14,1163,1164],{},"mkstemp(dir=target.parent)"," creates the temporary file in the ",[69,1167,1168],{},"same directory"," with a random name and ",[14,1171,1172],{},"O_EXCL",", so it cannot collide with another process's temp file. The same directory guarantees the same filesystem; ",[14,1175,1176],{},"os.replace"," from ",[14,1179,1180],{},"\u002Ftmp"," to ",[14,1183,1184],{},"~\u002F.config"," would fail with ",[14,1187,1188],{},"EXDEV"," or silently become a non-atomic copy in a naive fallback.",[45,1191,1192,1201,1202,1205],{},[69,1193,1194,1197,1198],{},[14,1195,1196],{},"flush()"," then ",[14,1199,1200],{},"os.fsync()"," moves data from Python's buffer to the OS, then from the OS cache to the disk. Without ",[14,1203,1204],{},"fsync",", a power loss shortly after the rename can leave a correctly named but empty file on some filesystems — the exact failure you were trying to prevent.",[45,1207,1208,1211,1212,1215,1216,1218,1219,1222,1223,1225,1226,1229],{},[69,1209,1210],{},"Copying permissions"," preserves a ",[14,1213,1214],{},"0600"," credentials file as ",[14,1217,1214],{},". ",[14,1220,1221],{},"mkstemp"," always creates files as ",[14,1224,1214],{},", so without the ",[14,1227,1228],{},"chmod"," a world-readable config would quietly become private, and new files would not respect the user's umask.",[45,1231,1232,1237],{},[69,1233,1234],{},[14,1235,1236],{},"Path.resolve()"," first means that if the user's config is a symlink into their dotfiles repository, you replace the file the link points to rather than replacing the link with a regular file.",[45,1239,1240,1244,1245,1248,1249,1252],{},[69,1241,1242],{},[14,1243,1176],{}," overwrites an existing target on Windows too; ",[14,1246,1247],{},"os.rename"," raises ",[14,1250,1251],{},"FileExistsError"," there.",[45,1254,1255,1260],{},[69,1256,1257,1258],{},"Directory ",[14,1259,1204],{}," makes the rename itself durable on POSIX. It is cheap and optional; include it for files that must survive a power cut.",[86,1262],{"name":1263},"fs-atomic-steps-terminal",[37,1265,1267],{"id":1266},"using-it-in-commands","Using it in commands",[10,1269,1270],{},"Replace in-place writes for anything that matters. A typical config command becomes:",[105,1272,1274],{"className":107,"code":1273,"language":109,"meta":110,"style":110},"# src\u002Fmytool\u002Fcli.py\nimport tomllib\nfrom pathlib import Path\n\nimport tomli_w\nimport typer\n\nfrom mytool.files import atomic_open\n\napp = typer.Typer()\nCONFIG = Path.home() \u002F \".config\" \u002F \"mytool\" \u002F \"config.toml\"\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"mytool configuration.\"\"\"\n\n\n@app.command(\"set\")\ndef set_value(key: str, value: str) -> None:\n    \"\"\"Set KEY to VALUE in the user config.\"\"\"\n    data = tomllib.loads(CONFIG.read_text(encoding=\"utf-8\")) if CONFIG.exists() else {}\n    data[key] = value\n    with atomic_open(CONFIG, \"wb\", encoding=None, newline=None) as fh:\n        tomli_w.dump(data, fh)\n    typer.echo(f\"{key} = {value!r}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1275,1276,1281,1288,1298,1302,1309,1316,1320,1332,1336,1346,1373,1377,1381,1389,1402,1407,1411,1415,1427,1451,1456,1496,1506,1541,1546,1588,1592,1596,1611],{"__ignoreMap":110},[114,1277,1278],{"class":116,"line":117},[114,1279,1280],{"class":120},"# src\u002Fmytool\u002Fcli.py\n",[114,1282,1283,1285],{"class":116,"line":124},[114,1284,152],{"class":127},[114,1286,1287],{"class":138}," tomllib\n",[114,1289,1290,1292,1294,1296],{"class":116,"line":142},[114,1291,128],{"class":127},[114,1293,205],{"class":138},[114,1295,152],{"class":127},[114,1297,210],{"class":138},[114,1299,1300],{"class":116,"line":149},[114,1301,146],{"emptyLinePlaceholder":145},[114,1303,1304,1306],{"class":116,"line":158},[114,1305,152],{"class":127},[114,1307,1308],{"class":138}," tomli_w\n",[114,1310,1311,1313],{"class":116,"line":166},[114,1312,152],{"class":127},[114,1314,1315],{"class":138}," typer\n",[114,1317,1318],{"class":116,"line":174},[114,1319,146],{"emptyLinePlaceholder":145},[114,1321,1322,1324,1327,1329],{"class":116,"line":187},[114,1323,128],{"class":127},[114,1325,1326],{"class":138}," mytool.files ",[114,1328,152],{"class":127},[114,1330,1331],{"class":138}," atomic_open\n",[114,1333,1334],{"class":116,"line":200},[114,1335,146],{"emptyLinePlaceholder":145},[114,1337,1338,1341,1343],{"class":116,"line":213},[114,1339,1340],{"class":138},"app ",[114,1342,405],{"class":127},[114,1344,1345],{"class":138}," typer.Typer()\n",[114,1347,1348,1351,1353,1356,1359,1362,1365,1368,1370],{"class":116,"line":229},[114,1349,1350],{"class":131},"CONFIG",[114,1352,274],{"class":127},[114,1354,1355],{"class":138}," Path.home() ",[114,1357,1358],{"class":127},"\u002F",[114,1360,1361],{"class":277}," \".config\"",[114,1363,1364],{"class":127}," \u002F",[114,1366,1367],{"class":277}," \"mytool\"",[114,1369,1364],{"class":127},[114,1371,1372],{"class":277}," \"config.toml\"\n",[114,1374,1375],{"class":116,"line":234},[114,1376,146],{"emptyLinePlaceholder":145},[114,1378,1379],{"class":116,"line":239},[114,1380,146],{"emptyLinePlaceholder":145},[114,1382,1383,1386],{"class":116,"line":246},[114,1384,1385],{"class":242},"@app.callback",[114,1387,1388],{"class":138},"()\n",[114,1390,1391,1393,1396,1398,1400],{"class":116,"line":290},[114,1392,249],{"class":127},[114,1394,1395],{"class":242}," main",[114,1397,1022],{"class":138},[114,1399,783],{"class":131},[114,1401,511],{"class":138},[114,1403,1404],{"class":116,"line":324},[114,1405,1406],{"class":277},"    \"\"\"mytool configuration.\"\"\"\n",[114,1408,1409],{"class":116,"line":347},[114,1410,146],{"emptyLinePlaceholder":145},[114,1412,1413],{"class":116,"line":353},[114,1414,146],{"emptyLinePlaceholder":145},[114,1416,1417,1420,1422,1425],{"class":116,"line":381},[114,1418,1419],{"class":242},"@app.command",[114,1421,390],{"class":138},[114,1423,1424],{"class":277},"\"set\"",[114,1426,396],{"class":138},[114,1428,1429,1431,1434,1437,1439,1442,1444,1447,1449],{"class":116,"line":399},[114,1430,249],{"class":127},[114,1432,1433],{"class":242}," set_value",[114,1435,1436],{"class":138},"(key: ",[114,1438,258],{"class":131},[114,1440,1441],{"class":138},", value: ",[114,1443,258],{"class":131},[114,1445,1446],{"class":138},") -> ",[114,1448,783],{"class":131},[114,1450,511],{"class":138},[114,1452,1453],{"class":116,"line":414},[114,1454,1455],{"class":277},"    \"\"\"Set KEY to VALUE in the user config.\"\"\"\n",[114,1457,1458,1461,1463,1466,1468,1471,1473,1475,1478,1481,1484,1487,1490,1493],{"class":116,"line":440},[114,1459,1460],{"class":138},"    data ",[114,1462,405],{"class":127},[114,1464,1465],{"class":138}," tomllib.loads(",[114,1467,1350],{"class":131},[114,1469,1470],{"class":138},".read_text(",[114,1472,557],{"class":420},[114,1474,405],{"class":127},[114,1476,1477],{"class":277},"\"utf-8\"",[114,1479,1480],{"class":138},")) ",[114,1482,1483],{"class":127},"if",[114,1485,1486],{"class":131}," CONFIG",[114,1488,1489],{"class":138},".exists() ",[114,1491,1492],{"class":127},"else",[114,1494,1495],{"class":138}," {}\n",[114,1497,1498,1501,1503],{"class":116,"line":494},[114,1499,1500],{"class":138},"    data[key] ",[114,1502,405],{"class":127},[114,1504,1505],{"class":138}," value\n",[114,1507,1508,1510,1513,1515,1517,1519,1521,1523,1525,1527,1529,1531,1533,1535,1537,1539],{"class":116,"line":505},[114,1509,791],{"class":127},[114,1511,1512],{"class":138}," atomic_open(",[114,1514,1350],{"class":131},[114,1516,281],{"class":138},[114,1518,375],{"class":277},[114,1520,281],{"class":138},[114,1522,557],{"class":420},[114,1524,405],{"class":127},[114,1526,783],{"class":131},[114,1528,281],{"class":138},[114,1530,565],{"class":420},[114,1532,405],{"class":127},[114,1534,783],{"class":131},[114,1536,946],{"class":138},[114,1538,806],{"class":127},[114,1540,579],{"class":138},[114,1542,1543],{"class":116,"line":514},[114,1544,1545],{"class":138},"        tomli_w.dump(data, fh)\n",[114,1547,1548,1551,1553,1555,1557,1560,1562,1565,1567,1570,1573,1575,1577,1579,1582,1584,1586],{"class":116,"line":528},[114,1549,1550],{"class":138},"    typer.echo(",[114,1552,464],{"class":127},[114,1554,994],{"class":277},[114,1556,470],{"class":131},[114,1558,1559],{"class":138},"key",[114,1561,476],{"class":131},[114,1563,1564],{"class":277}," = ",[114,1566,470],{"class":131},[114,1568,1569],{"class":138},"value",[114,1571,1572],{"class":127},"!r",[114,1574,476],{"class":131},[114,1576,994],{"class":277},[114,1578,281],{"class":138},[114,1580,1581],{"class":420},"err",[114,1583,405],{"class":127},[114,1585,426],{"class":131},[114,1587,396],{"class":138},[114,1589,1590],{"class":116,"line":539},[114,1591,146],{"emptyLinePlaceholder":145},[114,1593,1594],{"class":116,"line":547},[114,1595,146],{"emptyLinePlaceholder":145},[114,1597,1598,1600,1603,1606,1609],{"class":116,"line":573},[114,1599,1483],{"class":127},[114,1601,1602],{"class":131}," __name__",[114,1604,1605],{"class":127}," ==",[114,1607,1608],{"class":277}," \"__main__\"",[114,1610,511],{"class":138},[114,1612,1613],{"class":116,"line":582},[114,1614,1615],{"class":138},"    app()\n",[10,1617,1618,1619,1622,1623,1627,1628,1631,1632,1636],{},"In a real tool, the config path would come from ",[14,1620,1621],{},"platformdirs"," rather than being hard-coded — see ",[31,1624,1626],{"href":1625},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs",". And if two invocations might run ",[14,1629,1630],{},"set"," at once, the read-modify-write needs a lock as well; atomicity prevents torn files, not lost updates. ",[31,1633,1635],{"href":1634},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002F","File locking for concurrent CLI runs"," adds that.",[86,1638],{"name":1639},"fs-atomic-checklist",[37,1641,1643],{"id":1642},"ux-considerations","UX considerations",[42,1645,1646,1660,1677,1683],{},[45,1647,1648,1651,1652,1655,1656,1659],{},[69,1649,1650],{},"Output files the user asked for deserve the same care."," ",[14,1653,1654],{},"mytool export -o report.csv"," that fails halfway should leave the previous ",[14,1657,1658],{},"report.csv"," intact, not a truncated one the user may not notice is incomplete.",[45,1661,1662,1665,1666,1669,1670,1673,1674,1676],{},[69,1663,1664],{},"Don't leave dot-files behind."," The temp files are hidden (leading dot) so they do not clutter a listing during the write, and the ",[14,1667,1668],{},"except"," branch removes them on failure. If you find stray ",[14,1671,1672],{},".config.toml.*.tmp"," files, something bypassed the helper — usually a ",[14,1675,83],{},", which no code can intercept. Consider deleting stale ones older than a day on startup.",[45,1678,1679,1682],{},[69,1680,1681],{},"Report the path you wrote."," A single line on stderr (\"wrote ~\u002F.config\u002Fmytool\u002Fconfig.toml\") confirms success and tells the user where to look.",[45,1684,1685,1688,1689,1692],{},[69,1686,1687],{},"Keep writing to stdout streaming."," Atomic writes are for files. When the user passes ",[14,1690,1691],{},"-o -"," for stdout, write directly; you cannot atomically replace a pipe.",[37,1694,1696],{"id":1695},"testing-the-behaviour","Testing the behaviour",[10,1698,1699,1700,1703],{},"The key property is negative: when something fails mid-write, the old content survives and no temp file is left. Test it by failing inside the ",[14,1701,1702],{},"with"," block:",[105,1705,1707],{"className":107,"code":1706,"language":109,"meta":110,"style":110},"# tests\u002Ftest_files.py\nimport json\nimport os\nimport stat\nimport sys\n\nimport pytest\n\nfrom mytool.files import atomic_open, write_json_atomic, write_text_atomic\n\n\ndef test_replaces_content(tmp_path):\n    p = tmp_path \u002F \"a.txt\"\n    p.write_text(\"old\", encoding=\"utf-8\")\n    write_text_atomic(p, \"new\")\n    assert p.read_text(encoding=\"utf-8\") == \"new\"\n\n\ndef test_failure_keeps_old_file_and_cleans_up(tmp_path):\n    p = tmp_path \u002F \"state.json\"\n    write_json_atomic(p, {\"n\": 1})\n    with pytest.raises(TypeError):\n        write_json_atomic(p, {\"n\": object()})   # not serialisable, fails mid-dump\n    assert json.loads(p.read_text()) == {\"n\": 1}\n    assert sorted(os.listdir(tmp_path)) == [\"state.json\"]\n\n\ndef test_interrupt_is_also_safe(tmp_path):\n    p = tmp_path \u002F \"a.txt\"\n    p.write_text(\"keep me\")\n    with pytest.raises(KeyboardInterrupt):\n        with atomic_open(p) as fh:\n            fh.write(\"partial\")\n            raise KeyboardInterrupt\n    assert p.read_text() == \"keep me\"\n\n\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"POSIX permission bits\")\ndef test_permissions_are_preserved(tmp_path):\n    p = tmp_path \u002F \"secret.toml\"\n    p.write_text(\"token = 'x'\")\n    p.chmod(0o600)\n    write_text_atomic(p, \"token = 'y'\")\n    assert stat.S_IMODE(p.stat().st_mode) == 0o600\n\n\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"symlinks need privileges\")\ndef test_symlink_target_is_updated(tmp_path):\n    real = tmp_path \u002F \"dotfiles\" \u002F \"config.toml\"\n    real.parent.mkdir()\n    real.write_text(\"a = 1\")\n    link = tmp_path \u002F \"config.toml\"\n    link.symlink_to(real)\n    write_text_atomic(link, \"a = 2\")\n    assert link.is_symlink()\n    assert real.read_text() == \"a = 2\"\n",[14,1708,1709,1714,1720,1726,1733,1740,1744,1751,1755,1766,1770,1774,1784,1799,1817,1827,1848,1852,1856,1865,1878,1895,1907,1925,1946,1967,1971,1975,1984,1996,2005,2015,2026,2036,2044,2056,2060,2064,2089,2098,2111,2120,2132,2141,2155,2159,2163,2184,2193,2211,2216,2226,2239,2244,2254,2261],{"__ignoreMap":110},[114,1710,1711],{"class":116,"line":117},[114,1712,1713],{"class":120},"# tests\u002Ftest_files.py\n",[114,1715,1716,1718],{"class":116,"line":124},[114,1717,152],{"class":127},[114,1719,155],{"class":138},[114,1721,1722,1724],{"class":116,"line":142},[114,1723,152],{"class":127},[114,1725,163],{"class":138},[114,1727,1728,1730],{"class":116,"line":149},[114,1729,152],{"class":127},[114,1731,1732],{"class":138}," stat\n",[114,1734,1735,1737],{"class":116,"line":158},[114,1736,152],{"class":127},[114,1738,1739],{"class":138}," sys\n",[114,1741,1742],{"class":116,"line":166},[114,1743,146],{"emptyLinePlaceholder":145},[114,1745,1746,1748],{"class":116,"line":174},[114,1747,152],{"class":127},[114,1749,1750],{"class":138}," pytest\n",[114,1752,1753],{"class":116,"line":187},[114,1754,146],{"emptyLinePlaceholder":145},[114,1756,1757,1759,1761,1763],{"class":116,"line":200},[114,1758,128],{"class":127},[114,1760,1326],{"class":138},[114,1762,152],{"class":127},[114,1764,1765],{"class":138}," atomic_open, write_json_atomic, write_text_atomic\n",[114,1767,1768],{"class":116,"line":213},[114,1769,146],{"emptyLinePlaceholder":145},[114,1771,1772],{"class":116,"line":229},[114,1773,146],{"emptyLinePlaceholder":145},[114,1775,1776,1778,1781],{"class":116,"line":234},[114,1777,249],{"class":127},[114,1779,1780],{"class":242}," test_replaces_content",[114,1782,1783],{"class":138},"(tmp_path):\n",[114,1785,1786,1789,1791,1794,1796],{"class":116,"line":239},[114,1787,1788],{"class":138},"    p ",[114,1790,405],{"class":127},[114,1792,1793],{"class":138}," tmp_path ",[114,1795,1358],{"class":127},[114,1797,1798],{"class":277}," \"a.txt\"\n",[114,1800,1801,1804,1807,1809,1811,1813,1815],{"class":116,"line":246},[114,1802,1803],{"class":138},"    p.write_text(",[114,1805,1806],{"class":277},"\"old\"",[114,1808,281],{"class":138},[114,1810,557],{"class":420},[114,1812,405],{"class":127},[114,1814,1477],{"class":277},[114,1816,396],{"class":138},[114,1818,1819,1822,1825],{"class":116,"line":290},[114,1820,1821],{"class":138},"    write_text_atomic(p, ",[114,1823,1824],{"class":277},"\"new\"",[114,1826,396],{"class":138},[114,1828,1829,1832,1835,1837,1839,1841,1843,1845],{"class":116,"line":324},[114,1830,1831],{"class":127},"    assert",[114,1833,1834],{"class":138}," p.read_text(",[114,1836,557],{"class":420},[114,1838,405],{"class":127},[114,1840,1477],{"class":277},[114,1842,946],{"class":138},[114,1844,696],{"class":127},[114,1846,1847],{"class":277}," \"new\"\n",[114,1849,1850],{"class":116,"line":347},[114,1851,146],{"emptyLinePlaceholder":145},[114,1853,1854],{"class":116,"line":353},[114,1855,146],{"emptyLinePlaceholder":145},[114,1857,1858,1860,1863],{"class":116,"line":381},[114,1859,249],{"class":127},[114,1861,1862],{"class":242}," test_failure_keeps_old_file_and_cleans_up",[114,1864,1783],{"class":138},[114,1866,1867,1869,1871,1873,1875],{"class":116,"line":399},[114,1868,1788],{"class":138},[114,1870,405],{"class":127},[114,1872,1793],{"class":138},[114,1874,1358],{"class":127},[114,1876,1877],{"class":277}," \"state.json\"\n",[114,1879,1880,1883,1886,1889,1892],{"class":116,"line":414},[114,1881,1882],{"class":138},"    write_json_atomic(p, {",[114,1884,1885],{"class":277},"\"n\"",[114,1887,1888],{"class":138},": ",[114,1890,1891],{"class":131},"1",[114,1893,1894],{"class":138},"})\n",[114,1896,1897,1899,1902,1905],{"class":116,"line":440},[114,1898,791],{"class":127},[114,1900,1901],{"class":138}," pytest.raises(",[114,1903,1904],{"class":131},"TypeError",[114,1906,378],{"class":138},[114,1908,1909,1912,1914,1916,1919,1922],{"class":116,"line":494},[114,1910,1911],{"class":138},"        write_json_atomic(p, {",[114,1913,1885],{"class":277},[114,1915,1888],{"class":138},[114,1917,1918],{"class":131},"object",[114,1920,1921],{"class":138},"()})   ",[114,1923,1924],{"class":120},"# not serialisable, fails mid-dump\n",[114,1926,1927,1929,1932,1934,1937,1939,1941,1943],{"class":116,"line":505},[114,1928,1831],{"class":127},[114,1930,1931],{"class":138}," json.loads(p.read_text()) ",[114,1933,696],{"class":127},[114,1935,1936],{"class":138}," {",[114,1938,1885],{"class":277},[114,1940,1888],{"class":138},[114,1942,1891],{"class":131},[114,1944,1945],{"class":138},"}\n",[114,1947,1948,1950,1953,1956,1958,1961,1964],{"class":116,"line":514},[114,1949,1831],{"class":127},[114,1951,1952],{"class":131}," sorted",[114,1954,1955],{"class":138},"(os.listdir(tmp_path)) ",[114,1957,696],{"class":127},[114,1959,1960],{"class":138}," [",[114,1962,1963],{"class":277},"\"state.json\"",[114,1965,1966],{"class":138},"]\n",[114,1968,1969],{"class":116,"line":528},[114,1970,146],{"emptyLinePlaceholder":145},[114,1972,1973],{"class":116,"line":539},[114,1974,146],{"emptyLinePlaceholder":145},[114,1976,1977,1979,1982],{"class":116,"line":547},[114,1978,249],{"class":127},[114,1980,1981],{"class":242}," test_interrupt_is_also_safe",[114,1983,1783],{"class":138},[114,1985,1986,1988,1990,1992,1994],{"class":116,"line":573},[114,1987,1788],{"class":138},[114,1989,405],{"class":127},[114,1991,1793],{"class":138},[114,1993,1358],{"class":127},[114,1995,1798],{"class":277},[114,1997,1998,2000,2003],{"class":116,"line":582},[114,1999,1803],{"class":138},[114,2001,2002],{"class":277},"\"keep me\"",[114,2004,396],{"class":138},[114,2006,2007,2009,2011,2013],{"class":116,"line":591},[114,2008,791],{"class":127},[114,2010,1901],{"class":138},[114,2012,79],{"class":131},[114,2014,378],{"class":138},[114,2016,2017,2019,2022,2024],{"class":116,"line":597},[114,2018,576],{"class":127},[114,2020,2021],{"class":138}," atomic_open(p) ",[114,2023,806],{"class":127},[114,2025,579],{"class":138},[114,2027,2028,2031,2034],{"class":116,"line":606},[114,2029,2030],{"class":138},"            fh.write(",[114,2032,2033],{"class":277},"\"partial\"",[114,2035,396],{"class":138},[114,2037,2038,2041],{"class":116,"line":612},[114,2039,2040],{"class":127},"            raise",[114,2042,2043],{"class":131}," KeyboardInterrupt\n",[114,2045,2046,2048,2051,2053],{"class":116,"line":620},[114,2047,1831],{"class":127},[114,2049,2050],{"class":138}," p.read_text() ",[114,2052,696],{"class":127},[114,2054,2055],{"class":277}," \"keep me\"\n",[114,2057,2058],{"class":116,"line":641},[114,2059,146],{"emptyLinePlaceholder":145},[114,2061,2062],{"class":116,"line":652},[114,2063,146],{"emptyLinePlaceholder":145},[114,2065,2066,2069,2072,2074,2077,2079,2082,2084,2087],{"class":116,"line":676},[114,2067,2068],{"class":242},"@pytest.mark.skipif",[114,2070,2071],{"class":138},"(sys.platform ",[114,2073,696],{"class":127},[114,2075,2076],{"class":277}," \"win32\"",[114,2078,281],{"class":138},[114,2080,2081],{"class":420},"reason",[114,2083,405],{"class":127},[114,2085,2086],{"class":277},"\"POSIX permission bits\"",[114,2088,396],{"class":138},[114,2090,2091,2093,2096],{"class":116,"line":682},[114,2092,249],{"class":127},[114,2094,2095],{"class":242}," test_permissions_are_preserved",[114,2097,1783],{"class":138},[114,2099,2100,2102,2104,2106,2108],{"class":116,"line":704},[114,2101,1788],{"class":138},[114,2103,405],{"class":127},[114,2105,1793],{"class":138},[114,2107,1358],{"class":127},[114,2109,2110],{"class":277}," \"secret.toml\"\n",[114,2112,2113,2115,2118],{"class":116,"line":710},[114,2114,1803],{"class":138},[114,2116,2117],{"class":277},"\"token = 'x'\"",[114,2119,396],{"class":138},[114,2121,2122,2125,2127,2130],{"class":116,"line":721},[114,2123,2124],{"class":138},"    p.chmod(",[114,2126,658],{"class":127},[114,2128,2129],{"class":131},"600",[114,2131,396],{"class":138},[114,2133,2134,2136,2139],{"class":116,"line":736},[114,2135,1821],{"class":138},[114,2137,2138],{"class":277},"\"token = 'y'\"",[114,2140,396],{"class":138},[114,2142,2143,2145,2148,2150,2152],{"class":116,"line":742},[114,2144,1831],{"class":127},[114,2146,2147],{"class":138}," stat.S_IMODE(p.stat().st_mode) ",[114,2149,696],{"class":127},[114,2151,629],{"class":127},[114,2153,2154],{"class":131},"600\n",[114,2156,2157],{"class":116,"line":747},[114,2158,146],{"emptyLinePlaceholder":145},[114,2160,2161],{"class":116,"line":752},[114,2162,146],{"emptyLinePlaceholder":145},[114,2164,2165,2167,2169,2171,2173,2175,2177,2179,2182],{"class":116,"line":788},[114,2166,2068],{"class":242},[114,2168,2071],{"class":138},[114,2170,696],{"class":127},[114,2172,2076],{"class":277},[114,2174,281],{"class":138},[114,2176,2081],{"class":420},[114,2178,405],{"class":127},[114,2180,2181],{"class":277},"\"symlinks need privileges\"",[114,2183,396],{"class":138},[114,2185,2186,2188,2191],{"class":116,"line":811},[114,2187,249],{"class":127},[114,2189,2190],{"class":242}," test_symlink_target_is_updated",[114,2192,1783],{"class":138},[114,2194,2195,2198,2200,2202,2204,2207,2209],{"class":116,"line":817},[114,2196,2197],{"class":138},"    real ",[114,2199,405],{"class":127},[114,2201,1793],{"class":138},[114,2203,1358],{"class":127},[114,2205,2206],{"class":277}," \"dotfiles\"",[114,2208,1364],{"class":127},[114,2210,1372],{"class":277},[114,2212,2213],{"class":116,"line":822},[114,2214,2215],{"class":138},"    real.parent.mkdir()\n",[114,2217,2218,2221,2224],{"class":116,"line":827},[114,2219,2220],{"class":138},"    real.write_text(",[114,2222,2223],{"class":277},"\"a = 1\"",[114,2225,396],{"class":138},[114,2227,2228,2231,2233,2235,2237],{"class":116,"line":861},[114,2229,2230],{"class":138},"    link ",[114,2232,405],{"class":127},[114,2234,1793],{"class":138},[114,2236,1358],{"class":127},[114,2238,1372],{"class":277},[114,2240,2241],{"class":116,"line":896},[114,2242,2243],{"class":138},"    link.symlink_to(real)\n",[114,2245,2246,2249,2252],{"class":116,"line":902},[114,2247,2248],{"class":138},"    write_text_atomic(link, ",[114,2250,2251],{"class":277},"\"a = 2\"",[114,2253,396],{"class":138},[114,2255,2256,2258],{"class":116,"line":907},[114,2257,1831],{"class":127},[114,2259,2260],{"class":138}," link.is_symlink()\n",[114,2262,2263,2265,2268,2270],{"class":116,"line":912},[114,2264,1831],{"class":127},[114,2266,2267],{"class":138}," real.read_text() ",[114,2269,696],{"class":127},[114,2271,2272],{"class":277}," \"a = 2\"\n",[10,2274,2275,2276,2278],{},"You cannot easily simulate a power cut in a unit test, and you do not need to: the ",[14,2277,1204],{}," calls are standard-library behaviour. Test your code's logic — cleanup, permissions, symlinks — and trust the operating system for the rest.",[37,2280,2282],{"id":2281},"conclusion","Conclusion",[10,2284,2285,2286,2288,2289,2291,2292,2295,2296,2299],{},"Every file your CLI writes that someone would miss should be written atomically: temporary file in the same directory, flush and ",[14,2287,1204],{},", preserve permissions, ",[14,2290,1176],{},", and clean up on any exception. Wrapped in a context manager, it costs nothing at the call site — ",[14,2293,2294],{},"with atomic_open(path) as fh:"," instead of ",[14,2297,2298],{},"with open(path, \"w\") as fh:"," — and it turns a class of \"my config vanished\" bug reports into something that simply cannot happen short of hardware failure.",[37,2301,2303],{"id":2302},"frequently-asked-questions","Frequently asked questions",[1152,2305,2307,2308,2311],{"id":2306},"should-i-use-the-atomicwrites-package-instead","Should I use the ",[14,2309,2310],{},"atomicwrites"," package instead?",[10,2313,2314,2315,2318],{},"It is archived and no longer maintained. The standard library has everything needed, and the helper above is short enough to own. If you would rather depend on something, ",[14,2316,2317],{},"safer"," provides a similar API, but check its maintenance status before adding it.",[1152,2320,2322],{"id":2321},"is-this-slower-than-a-normal-write","Is this slower than a normal write?",[10,2324,2325,2326,2328,2329,2332],{},"The ",[14,2327,1204],{}," calls cost a few milliseconds on an SSD — noticeable only if you write thousands of files in a loop. For bulk outputs that can be regenerated, pass ",[14,2330,2331],{},"fsync=False",": you keep atomicity against crashes of your own process and give up only protection against power loss.",[1152,2334,2336],{"id":2335},"does-atomic-replacement-work-on-network-filesystems","Does atomic replacement work on network filesystems?",[10,2338,2339,2340,2342],{},"Renames on NFS and SMB are generally atomic within one directory, but durability guarantees are weaker and ",[14,2341,1204],{}," semantics vary. For network home directories, the pattern is still far better than an in-place write; just do not rely on it for database-grade durability.",[1152,2344,2346],{"id":2345},"what-about-appending-to-a-log-file","What about appending to a log file?",[10,2348,2349,2350,2353,2354,2357,2358,2362],{},"Appends are a different problem: rewriting the whole file to add a line would be absurd. Open with mode ",[14,2351,2352],{},"\"a\"",", write complete lines in a single ",[14,2355,2356],{},"write()"," call, and let a rotating handler manage size. ",[31,2359,2361],{"href":2360},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli\u002F","Writing rotating log files from a CLI"," covers it.",[37,2364,2366],{"id":2365},"related","Related",[42,2368,2369,2375,2379,2385,2390],{},[45,2370,2371,2372],{},"Up: ",[31,2373,2374],{"href":33},"Filesystem paths and atomic writes",[45,2376,2377],{},[31,2378,1635],{"href":1634},[45,2380,2381],{},[31,2382,2384],{"href":2383},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories\u002F","Safe temporary files and directories",[45,2386,2387],{},[31,2388,2389],{"href":1625},"Storing app data with platformdirs",[45,2391,2392],{},[31,2393,2395],{"href":2394},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps\u002F","Loading YAML configs safely in CLI apps",[2397,2398,2399],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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":110,"searchDepth":124,"depth":124,"links":2401},[2402,2403,2404,2407,2408,2409,2410,2411,2418],{"id":39,"depth":124,"text":40},{"id":56,"depth":124,"text":57},{"id":102,"depth":124,"text":103,"children":2405},[2406],{"id":1154,"depth":142,"text":1155},{"id":1266,"depth":124,"text":1267},{"id":1642,"depth":124,"text":1643},{"id":1695,"depth":124,"text":1696},{"id":2281,"depth":124,"text":2282},{"id":2302,"depth":124,"text":2303,"children":2412},[2413,2415,2416,2417],{"id":2306,"depth":142,"text":2414},"Should I use the atomicwrites package instead?",{"id":2321,"depth":142,"text":2322},{"id":2335,"depth":142,"text":2336},{"id":2345,"depth":142,"text":2346},{"id":2365,"depth":124,"text":2366},"2026-09-18","Stop crashes and Ctrl+C from leaving empty or half-written files: a tested atomic write helper for text, bytes and JSON using mkstemp, fsync and os.replace.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis",{"title":5,"description":2420},"cli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002Findex",[2429,2430,2431,2432],"filesystem","atomic-writes","reliability","json","FwqAJWQccCVux8mjBQiLqD75seyQrDN8QUYR1mSgapw",[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,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572,2575,2578,2581,2584,2587,2590,2593,2596,2599,2602,2605,2608,2611,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2707,2710,2713,2716,2719,2722,2725,2728,2731,2734,2737,2740,2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2794,2797,2800,2803,2806,2809,2812,2815,2818,2821,2824,2827,2830,2833,2836,2839,2842,2845,2848,2851,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977],{"path":2436,"title":2437},"\u002Fabout","About Python CLI Toolcraft",{"path":2439,"title":2440},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2442,"title":2443},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2448,"title":2449},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2451,"title":2452},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2454,"title":2455},"\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":2457,"title":2458},"\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":2460,"title":2461},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2463,"title":2464},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2466,"title":2467},"\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":2469,"title":2470},"\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":2472,"title":2473},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2475,"title":2476},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2478,"title":2479},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2481,"title":2482},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2484,"title":2485},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2487,"title":2488},"\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":2490,"title":2491},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2493,"title":2494},"\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":2496,"title":2497},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2499,"title":2500},"\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":2502,"title":2503},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2505,"title":2506},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2508,"title":2509},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2511,"title":2512},"\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":2514,"title":2515},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2517,"title":2518},"\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":2520,"title":2521},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2523,"title":2395},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps",{"path":2525,"title":2526},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2528,"title":2529},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2531,"title":2532},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2534,"title":2535},"\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":2537,"title":2538},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2540,"title":2541},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2543,"title":2544},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2546,"title":2547},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2549,"title":2550},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2552,"title":2553},"\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":2555,"title":2556},"\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":2558,"title":2559},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2561,"title":2562},"\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":2564,"title":2565},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2567,"title":2568},"\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":2570,"title":2571},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2573,"title":2574},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2576,"title":2577},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2579,"title":2580},"\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":2582,"title":2583},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2585,"title":2586},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2588,"title":2589},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2591,"title":2592},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2594,"title":2595},"\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":2597,"title":2598},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2600,"title":2601},"\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":2603,"title":2604},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2606,"title":2607},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2609,"title":2610},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2612,"title":2613},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2615,"title":2616},"\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":2618,"title":2619},"\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":2621,"title":2622},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2624,"title":2625},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2627,"title":2628},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2630,"title":2631},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2633,"title":2634},"\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":2636,"title":2637},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2639,"title":2640},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2642,"title":2643},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2645,"title":2646},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2648,"title":2649},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2425,"title":5},{"path":2652,"title":2653},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2655,"title":2656},"\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":2658,"title":2659},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2661,"title":2662},"\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":2664,"title":2665},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2667,"title":2668},"\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":2670,"title":2671},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2673,"title":2674},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2676,"title":2677},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2679,"title":2680},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2682,"title":2683},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2685,"title":2686},"\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":2688,"title":2689},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2691,"title":2692},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2694,"title":2695},"\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":2697,"title":2698},"\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":2700,"title":2701},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2703,"title":2704},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1358,"title":2706},"Python CLI Toolcraft",{"path":2708,"title":2709},"\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":2711,"title":2712},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2714,"title":2715},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2717,"title":2718},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2720,"title":2721},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2723,"title":2724},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2726,"title":2727},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2729,"title":2730},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2732,"title":2733},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2735,"title":2736},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2738,"title":2739},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2741,"title":2742},"\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":2744,"title":2745},"\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":2747,"title":2748},"\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":2750,"title":2751},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2753,"title":2754},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2756,"title":2757},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2759,"title":2760},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2762,"title":2763},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2765,"title":2766},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2768,"title":2769},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2771,"title":2772},"\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":2774,"title":2775},"\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":2777,"title":2778},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2780,"title":2781},"\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":2783,"title":2784},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2786,"title":2787},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2789,"title":2790},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2792,"title":2793},"\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":2795,"title":2796},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2798,"title":2799},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2801,"title":2802},"\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":2804,"title":2805},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2807,"title":2808},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2810,"title":2811},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2813,"title":2814},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2816,"title":2817},"\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":2819,"title":2820},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2822,"title":2823},"\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":2825,"title":2826},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2828,"title":2829},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2831,"title":2832},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2834,"title":2835},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2837,"title":2838},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2840,"title":2841},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2843,"title":2844},"\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":2846,"title":2847},"\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":2849,"title":2850},"\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":2852,"title":2853},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2855,"title":2856},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2858,"title":2859},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2861,"title":2862},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2864,"title":2865},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2867,"title":2868},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2870,"title":2871},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2873,"title":2874},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2876,"title":2877},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2879,"title":2880},"\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":2882,"title":2883},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2885,"title":2886},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2888,"title":2889},"\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":2891,"title":2892},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2894,"title":2895},"\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":2897,"title":2898},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2900,"title":2901},"\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":2903,"title":2904},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2906,"title":2907},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2909,"title":2910},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2912,"title":2913},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2915,"title":2916},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2918,"title":2919},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2921,"title":2922},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2924,"title":2925},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2927,"title":2928},"\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":2930,"title":2931},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2933,"title":2934},"\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":2936,"title":2937},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2939,"title":2940},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2942,"title":2943},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2945,"title":2946},"\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":2948,"title":2949},"\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":2951,"title":2952},"\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":2954,"title":2955},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2957,"title":2958},"\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":2960,"title":2961},"\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":2963,"title":2964},"\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":2966,"title":2967},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2969,"title":2970},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2972,"title":2973},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2975,"title":2976},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2978,"title":2979},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905049]