[{"data":1,"prerenderedAt":2476},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002F":3,"content-directory":1929},{"id":4,"title":5,"body":6,"date":1914,"description":1915,"difficulty":1916,"draft":1917,"extension":1918,"meta":1919,"navigation":190,"path":1920,"seo":1921,"stem":1922,"tags":1923,"updated":1914,"__hash__":1928},"content\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002Findex.md","File Locking for Concurrent CLI Runs in Python",{"type":7,"value":8,"toc":1894},"minimark",[9,19,24,50,54,57,61,75,79,86,89,138,142,149,631,638,1193,1198,1213,1217,1225,1229,1232,1290,1294,1309,1777,1788,1792,1798,1802,1809,1818,1822,1833,1837,1849,1853,1856,1860,1890],[10,11,12,13,18],"p",{},"A CLI that keeps state between runs — a sync marker, a download index, a queue, a counter — works perfectly until two copies run at once. Cron starts a job while the previous one is still going. A developer runs the same command in two terminals. A CI matrix fans out six jobs on one runner that share a cache. Each copy reads the state, does its work and writes the state back, and whichever writes last silently discards the other's changes. Nothing crashes and nothing is corrupted; data just goes missing. This guide shows how to put a lock around the dangerous section using the operating system's own file locking, how to decide between waiting and failing, and how to test that the lock actually works. It is part of the ",[14,15,17],"a",{"href":16},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","filesystem topic",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,27,40,47],"ul",{},[28,29,30,31,35,36,39],"li",{},"Python 3.10+ and the ",[32,33,34],"code",{},"filelock"," package (",[32,37,38],{},"uv add filelock","), which works on Linux, macOS and Windows.",[28,41,42,43,18],{},"A CLI with some shared state on disk — ideally already written with ",[14,44,46],{"href":45},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","atomic writes",[28,48,49],{},"Familiarity with the idea of a critical section: the stretch of code that must not run in two processes at once.",[20,51,53],{"id":52},"the-problem-lost-updates","The problem: lost updates",[10,55,56],{},"Atomic writes make every write complete. They say nothing about whether a write was based on current data. The classic failure is the read-modify-write race:",[58,59],"inline-diagram",{"name":60},"fs-lock-race",[10,62,63,64,67,68,71,72,74],{},"Both runs read ",[32,65,66],{},"{count: 5}",", both compute ",[32,69,70],{},"6",", both write ",[32,73,70],{},". The file is valid JSON, and one increment has vanished. Replace \"count\" with \"list of files already uploaded\" and the second run re-uploads everything; replace it with \"last processed ID\" and records are skipped. The only fix is to make the whole cycle — read, decide, write — exclusive.",[20,76,78],{"id":77},"choosing-a-locking-mechanism","Choosing a locking mechanism",[10,80,81,82],{},"There are three families of approach, and one important property separates them: ",[83,84,85],"strong",{},"what happens to the lock when the process holding it dies?",[58,87],{"name":88},"fs-lock-options",[25,90,91,108,122],{},[28,92,93,99,100,103,104,107],{},[83,94,95,96,18],{},"Lock files created with ",[32,97,98],{},"O_EXCL"," \"If ",[32,101,102],{},"sync.lock"," exists, someone is running; otherwise create it and delete it at the end.\" Portable and simple, and broken by any crash, ",[32,105,106],{},"SIGKILL"," or power cut: the file stays, and every later run believes a phantom process holds the lock. You end up writing stale-lock detection with PIDs and timestamps, which has its own races.",[28,109,110,113,114,117,118,121],{},[83,111,112],{},"Advisory OS locks"," — ",[32,115,116],{},"fcntl.flock()"," on POSIX, ",[32,119,120],{},"msvcrt.locking()"," on Windows. The kernel tracks the lock against an open file descriptor and releases it automatically when the process exits for any reason. This is the property you want.",[28,123,124,130,131,134,135,137],{},[83,125,126,127,129],{},"The ",[32,128,34],{}," package"," wraps the OS locks behind one cross-platform API with timeouts and a context manager, which makes it the pragmatic default. It also offers ",[32,132,133],{},"SoftFileLock",", an ",[32,136,98],{},"-style lock for filesystems where OS locks do not work (some network filesystems).",[20,139,141],{"id":140},"the-recipe","The recipe",[10,143,144,145,148],{},"Lock the ",[83,146,147],{},"resource",", not the program. Put the lock file next to the state it protects, and hold it for exactly the read-modify-write cycle:",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fstate.py\nfrom __future__ import annotations\n\nimport json\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Any\n\nfrom filelock import FileLock, Timeout\n\nfrom mytool.files import write_json_atomic   # from the atomic-writes guide\n\n\nclass Busy(Exception):\n    \"\"\"Another process holds the lock.\"\"\"\n\n    def __init__(self, lock_path: Path) -> None:\n        super().__init__(f\"another process is using {lock_path.parent}\")\n        self.lock_path = lock_path\n\n\n@contextmanager\ndef locked_json(path: Path, *, timeout: float = 30.0) -> Iterator[dict[str, Any]]:\n    \"\"\"Load JSON under an exclusive lock; save atomically if the block succeeds.\"\"\"\n    path.parent.mkdir(parents=True, exist_ok=True)\n    lock_path = path.with_name(path.name + \".lock\")\n    lock = FileLock(lock_path)\n    try:\n        lock.acquire(timeout=timeout)\n    except Timeout:\n        raise Busy(lock_path) from None\n    try:\n        data = json.loads(path.read_text(encoding=\"utf-8\")) if path.exists() else {}\n        yield data\n        write_json_atomic(path, data)\n    finally:\n        lock.release()\n","python","",[32,157,158,167,185,192,201,214,227,240,253,258,271,276,292,297,302,321,328,333,351,386,401,406,411,417,453,459,486,505,516,524,538,547,561,568,602,611,617,625],{"__ignoreMap":155},[159,160,163],"span",{"class":161,"line":162},"line",1,[159,164,166],{"class":165},"sJ8bj","# src\u002Fmytool\u002Fstate.py\n",[159,168,170,174,178,181],{"class":161,"line":169},2,[159,171,173],{"class":172},"szBVR","from",[159,175,177],{"class":176},"sj4cs"," __future__",[159,179,180],{"class":172}," import",[159,182,184],{"class":183},"sVt8B"," annotations\n",[159,186,188],{"class":161,"line":187},3,[159,189,191],{"emptyLinePlaceholder":190},true,"\n",[159,193,195,198],{"class":161,"line":194},4,[159,196,197],{"class":172},"import",[159,199,200],{"class":183}," json\n",[159,202,204,206,209,211],{"class":161,"line":203},5,[159,205,173],{"class":172},[159,207,208],{"class":183}," collections.abc ",[159,210,197],{"class":172},[159,212,213],{"class":183}," Iterator\n",[159,215,217,219,222,224],{"class":161,"line":216},6,[159,218,173],{"class":172},[159,220,221],{"class":183}," contextlib ",[159,223,197],{"class":172},[159,225,226],{"class":183}," contextmanager\n",[159,228,230,232,235,237],{"class":161,"line":229},7,[159,231,173],{"class":172},[159,233,234],{"class":183}," pathlib ",[159,236,197],{"class":172},[159,238,239],{"class":183}," Path\n",[159,241,243,245,248,250],{"class":161,"line":242},8,[159,244,173],{"class":172},[159,246,247],{"class":183}," typing ",[159,249,197],{"class":172},[159,251,252],{"class":183}," Any\n",[159,254,256],{"class":161,"line":255},9,[159,257,191],{"emptyLinePlaceholder":190},[159,259,261,263,266,268],{"class":161,"line":260},10,[159,262,173],{"class":172},[159,264,265],{"class":183}," filelock ",[159,267,197],{"class":172},[159,269,270],{"class":183}," FileLock, Timeout\n",[159,272,274],{"class":161,"line":273},11,[159,275,191],{"emptyLinePlaceholder":190},[159,277,279,281,284,286,289],{"class":161,"line":278},12,[159,280,173],{"class":172},[159,282,283],{"class":183}," mytool.files ",[159,285,197],{"class":172},[159,287,288],{"class":183}," write_json_atomic   ",[159,290,291],{"class":165},"# from the atomic-writes guide\n",[159,293,295],{"class":161,"line":294},13,[159,296,191],{"emptyLinePlaceholder":190},[159,298,300],{"class":161,"line":299},14,[159,301,191],{"emptyLinePlaceholder":190},[159,303,305,308,312,315,318],{"class":161,"line":304},15,[159,306,307],{"class":172},"class",[159,309,311],{"class":310},"sScJk"," Busy",[159,313,314],{"class":183},"(",[159,316,317],{"class":176},"Exception",[159,319,320],{"class":183},"):\n",[159,322,324],{"class":161,"line":323},16,[159,325,327],{"class":326},"sZZnC","    \"\"\"Another process holds the lock.\"\"\"\n",[159,329,331],{"class":161,"line":330},17,[159,332,191],{"emptyLinePlaceholder":190},[159,334,336,339,342,345,348],{"class":161,"line":335},18,[159,337,338],{"class":172},"    def",[159,340,341],{"class":176}," __init__",[159,343,344],{"class":183},"(self, lock_path: Path) -> ",[159,346,347],{"class":176},"None",[159,349,350],{"class":183},":\n",[159,352,354,357,360,363,365,368,371,374,377,380,383],{"class":161,"line":353},19,[159,355,356],{"class":176},"        super",[159,358,359],{"class":183},"().",[159,361,362],{"class":176},"__init__",[159,364,314],{"class":183},[159,366,367],{"class":172},"f",[159,369,370],{"class":326},"\"another process is using ",[159,372,373],{"class":176},"{",[159,375,376],{"class":183},"lock_path.parent",[159,378,379],{"class":176},"}",[159,381,382],{"class":326},"\"",[159,384,385],{"class":183},")\n",[159,387,389,392,395,398],{"class":161,"line":388},20,[159,390,391],{"class":176},"        self",[159,393,394],{"class":183},".lock_path ",[159,396,397],{"class":172},"=",[159,399,400],{"class":183}," lock_path\n",[159,402,404],{"class":161,"line":403},21,[159,405,191],{"emptyLinePlaceholder":190},[159,407,409],{"class":161,"line":408},22,[159,410,191],{"emptyLinePlaceholder":190},[159,412,414],{"class":161,"line":413},23,[159,415,416],{"class":310},"@contextmanager\n",[159,418,420,423,426,429,432,435,438,441,444,447,450],{"class":161,"line":419},24,[159,421,422],{"class":172},"def",[159,424,425],{"class":310}," locked_json",[159,427,428],{"class":183},"(path: Path, ",[159,430,431],{"class":172},"*",[159,433,434],{"class":183},", timeout: ",[159,436,437],{"class":176},"float",[159,439,440],{"class":172}," =",[159,442,443],{"class":176}," 30.0",[159,445,446],{"class":183},") -> Iterator[dict[",[159,448,449],{"class":176},"str",[159,451,452],{"class":183},", Any]]:\n",[159,454,456],{"class":161,"line":455},25,[159,457,458],{"class":326},"    \"\"\"Load JSON under an exclusive lock; save atomically if the block succeeds.\"\"\"\n",[159,460,462,465,469,471,474,477,480,482,484],{"class":161,"line":461},26,[159,463,464],{"class":183},"    path.parent.mkdir(",[159,466,468],{"class":467},"s4XuR","parents",[159,470,397],{"class":172},[159,472,473],{"class":176},"True",[159,475,476],{"class":183},", ",[159,478,479],{"class":467},"exist_ok",[159,481,397],{"class":172},[159,483,473],{"class":176},[159,485,385],{"class":183},[159,487,489,492,494,497,500,503],{"class":161,"line":488},27,[159,490,491],{"class":183},"    lock_path ",[159,493,397],{"class":172},[159,495,496],{"class":183}," path.with_name(path.name ",[159,498,499],{"class":172},"+",[159,501,502],{"class":326}," \".lock\"",[159,504,385],{"class":183},[159,506,508,511,513],{"class":161,"line":507},28,[159,509,510],{"class":183},"    lock ",[159,512,397],{"class":172},[159,514,515],{"class":183}," FileLock(lock_path)\n",[159,517,519,522],{"class":161,"line":518},29,[159,520,521],{"class":172},"    try",[159,523,350],{"class":183},[159,525,527,530,533,535],{"class":161,"line":526},30,[159,528,529],{"class":183},"        lock.acquire(",[159,531,532],{"class":467},"timeout",[159,534,397],{"class":172},[159,536,537],{"class":183},"timeout)\n",[159,539,541,544],{"class":161,"line":540},31,[159,542,543],{"class":172},"    except",[159,545,546],{"class":183}," Timeout:\n",[159,548,550,553,556,558],{"class":161,"line":549},32,[159,551,552],{"class":172},"        raise",[159,554,555],{"class":183}," Busy(lock_path) ",[159,557,173],{"class":172},[159,559,560],{"class":176}," None\n",[159,562,564,566],{"class":161,"line":563},33,[159,565,521],{"class":172},[159,567,350],{"class":183},[159,569,571,574,576,579,582,584,587,590,593,596,599],{"class":161,"line":570},34,[159,572,573],{"class":183},"        data ",[159,575,397],{"class":172},[159,577,578],{"class":183}," json.loads(path.read_text(",[159,580,581],{"class":467},"encoding",[159,583,397],{"class":172},[159,585,586],{"class":326},"\"utf-8\"",[159,588,589],{"class":183},")) ",[159,591,592],{"class":172},"if",[159,594,595],{"class":183}," path.exists() ",[159,597,598],{"class":172},"else",[159,600,601],{"class":183}," {}\n",[159,603,605,608],{"class":161,"line":604},35,[159,606,607],{"class":172},"        yield",[159,609,610],{"class":183}," data\n",[159,612,614],{"class":161,"line":613},36,[159,615,616],{"class":183},"        write_json_atomic(path, data)\n",[159,618,620,623],{"class":161,"line":619},37,[159,621,622],{"class":172},"    finally",[159,624,350],{"class":183},[159,626,628],{"class":161,"line":627},38,[159,629,630],{"class":183},"        lock.release()\n",[10,632,633,634,637],{},"A timeout of ",[32,635,636],{},"0"," means \"try once and fail immediately\", a positive number means \"wait up to this long\", and a negative one means \"wait forever\". The command layer maps those onto flags users understand:",[150,639,641],{"className":152,"code":640,"language":154,"meta":155,"style":155},"# src\u002Fmytool\u002Fcli.py\nimport time\nfrom pathlib import Path\n\nimport typer\n\nfrom mytool.state import Busy, locked_json\n\napp = typer.Typer()\nSTATE = Path.home() \u002F \".local\" \u002F \"state\" \u002F \"mytool\" \u002F \"sync.json\"\nEX_TEMPFAIL = 75\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Sync tool.\"\"\"\n\n\n@app.command()\ndef sync(\n    wait: float = typer.Option(30.0, help=\"Seconds to wait for another run to finish.\"),\n    no_wait: bool = typer.Option(False, \"--no-wait\", help=\"Fail at once if another run is active.\"),\n) -> None:\n    \"\"\"Sync new items and record progress.\"\"\"\n    timeout = 0 if no_wait else wait\n    try:\n        with locked_json(STATE, timeout=timeout) as state:\n            last = state.get(\"last_id\", 0)\n            new_items = list(range(last + 1, last + 4))   # stand-in for real work\n            time.sleep(0.2)\n            state[\"last_id\"] = new_items[-1]\n            state[\"runs\"] = state.get(\"runs\", 0) + 1\n    except Busy as exc:\n        typer.echo(f\"error: {exc} (lock: {exc.lock_path})\", err=True)\n        raise typer.Exit(EX_TEMPFAIL)\n    typer.echo(f\"synced items {new_items[0]}..{new_items[-1]}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[32,642,643,648,655,665,669,676,680,692,696,706,738,748,752,756,764,778,783,787,791,798,808,836,867,876,881,902,908,933,952,989,999,1023,1050,1062,1103,1114,1163,1167,1171,1187],{"__ignoreMap":155},[159,644,645],{"class":161,"line":162},[159,646,647],{"class":165},"# src\u002Fmytool\u002Fcli.py\n",[159,649,650,652],{"class":161,"line":169},[159,651,197],{"class":172},[159,653,654],{"class":183}," time\n",[159,656,657,659,661,663],{"class":161,"line":187},[159,658,173],{"class":172},[159,660,234],{"class":183},[159,662,197],{"class":172},[159,664,239],{"class":183},[159,666,667],{"class":161,"line":194},[159,668,191],{"emptyLinePlaceholder":190},[159,670,671,673],{"class":161,"line":203},[159,672,197],{"class":172},[159,674,675],{"class":183}," typer\n",[159,677,678],{"class":161,"line":216},[159,679,191],{"emptyLinePlaceholder":190},[159,681,682,684,687,689],{"class":161,"line":229},[159,683,173],{"class":172},[159,685,686],{"class":183}," mytool.state ",[159,688,197],{"class":172},[159,690,691],{"class":183}," Busy, locked_json\n",[159,693,694],{"class":161,"line":242},[159,695,191],{"emptyLinePlaceholder":190},[159,697,698,701,703],{"class":161,"line":255},[159,699,700],{"class":183},"app ",[159,702,397],{"class":172},[159,704,705],{"class":183}," typer.Typer()\n",[159,707,708,711,713,716,719,722,725,728,730,733,735],{"class":161,"line":260},[159,709,710],{"class":176},"STATE",[159,712,440],{"class":172},[159,714,715],{"class":183}," Path.home() ",[159,717,718],{"class":172},"\u002F",[159,720,721],{"class":326}," \".local\"",[159,723,724],{"class":172}," \u002F",[159,726,727],{"class":326}," \"state\"",[159,729,724],{"class":172},[159,731,732],{"class":326}," \"mytool\"",[159,734,724],{"class":172},[159,736,737],{"class":326}," \"sync.json\"\n",[159,739,740,743,745],{"class":161,"line":273},[159,741,742],{"class":176},"EX_TEMPFAIL",[159,744,440],{"class":172},[159,746,747],{"class":176}," 75\n",[159,749,750],{"class":161,"line":278},[159,751,191],{"emptyLinePlaceholder":190},[159,753,754],{"class":161,"line":294},[159,755,191],{"emptyLinePlaceholder":190},[159,757,758,761],{"class":161,"line":299},[159,759,760],{"class":310},"@app.callback",[159,762,763],{"class":183},"()\n",[159,765,766,768,771,774,776],{"class":161,"line":304},[159,767,422],{"class":172},[159,769,770],{"class":310}," main",[159,772,773],{"class":183},"() -> ",[159,775,347],{"class":176},[159,777,350],{"class":183},[159,779,780],{"class":161,"line":323},[159,781,782],{"class":326},"    \"\"\"Sync tool.\"\"\"\n",[159,784,785],{"class":161,"line":330},[159,786,191],{"emptyLinePlaceholder":190},[159,788,789],{"class":161,"line":335},[159,790,191],{"emptyLinePlaceholder":190},[159,792,793,796],{"class":161,"line":353},[159,794,795],{"class":310},"@app.command",[159,797,763],{"class":183},[159,799,800,802,805],{"class":161,"line":388},[159,801,422],{"class":172},[159,803,804],{"class":310}," sync",[159,806,807],{"class":183},"(\n",[159,809,810,813,815,817,820,823,825,828,830,833],{"class":161,"line":403},[159,811,812],{"class":183},"    wait: ",[159,814,437],{"class":176},[159,816,440],{"class":172},[159,818,819],{"class":183}," typer.Option(",[159,821,822],{"class":176},"30.0",[159,824,476],{"class":183},[159,826,827],{"class":467},"help",[159,829,397],{"class":172},[159,831,832],{"class":326},"\"Seconds to wait for another run to finish.\"",[159,834,835],{"class":183},"),\n",[159,837,838,841,844,846,848,851,853,856,858,860,862,865],{"class":161,"line":408},[159,839,840],{"class":183},"    no_wait: ",[159,842,843],{"class":176},"bool",[159,845,440],{"class":172},[159,847,819],{"class":183},[159,849,850],{"class":176},"False",[159,852,476],{"class":183},[159,854,855],{"class":326},"\"--no-wait\"",[159,857,476],{"class":183},[159,859,827],{"class":467},[159,861,397],{"class":172},[159,863,864],{"class":326},"\"Fail at once if another run is active.\"",[159,866,835],{"class":183},[159,868,869,872,874],{"class":161,"line":413},[159,870,871],{"class":183},") -> ",[159,873,347],{"class":176},[159,875,350],{"class":183},[159,877,878],{"class":161,"line":419},[159,879,880],{"class":326},"    \"\"\"Sync new items and record progress.\"\"\"\n",[159,882,883,886,888,891,894,897,899],{"class":161,"line":455},[159,884,885],{"class":183},"    timeout ",[159,887,397],{"class":172},[159,889,890],{"class":176}," 0",[159,892,893],{"class":172}," if",[159,895,896],{"class":183}," no_wait ",[159,898,598],{"class":172},[159,900,901],{"class":183}," wait\n",[159,903,904,906],{"class":161,"line":461},[159,905,521],{"class":172},[159,907,350],{"class":183},[159,909,910,913,916,918,920,922,924,927,930],{"class":161,"line":488},[159,911,912],{"class":172},"        with",[159,914,915],{"class":183}," locked_json(",[159,917,710],{"class":176},[159,919,476],{"class":183},[159,921,532],{"class":467},[159,923,397],{"class":172},[159,925,926],{"class":183},"timeout) ",[159,928,929],{"class":172},"as",[159,931,932],{"class":183}," state:\n",[159,934,935,938,940,943,946,948,950],{"class":161,"line":507},[159,936,937],{"class":183},"            last ",[159,939,397],{"class":172},[159,941,942],{"class":183}," state.get(",[159,944,945],{"class":326},"\"last_id\"",[159,947,476],{"class":183},[159,949,636],{"class":176},[159,951,385],{"class":183},[159,953,954,957,959,962,964,967,970,972,975,978,980,983,986],{"class":161,"line":518},[159,955,956],{"class":183},"            new_items ",[159,958,397],{"class":172},[159,960,961],{"class":176}," list",[159,963,314],{"class":183},[159,965,966],{"class":176},"range",[159,968,969],{"class":183},"(last ",[159,971,499],{"class":172},[159,973,974],{"class":176}," 1",[159,976,977],{"class":183},", last ",[159,979,499],{"class":172},[159,981,982],{"class":176}," 4",[159,984,985],{"class":183},"))   ",[159,987,988],{"class":165},"# stand-in for real work\n",[159,990,991,994,997],{"class":161,"line":526},[159,992,993],{"class":183},"            time.sleep(",[159,995,996],{"class":176},"0.2",[159,998,385],{"class":183},[159,1000,1001,1004,1006,1009,1011,1014,1017,1020],{"class":161,"line":540},[159,1002,1003],{"class":183},"            state[",[159,1005,945],{"class":326},[159,1007,1008],{"class":183},"] ",[159,1010,397],{"class":172},[159,1012,1013],{"class":183}," new_items[",[159,1015,1016],{"class":172},"-",[159,1018,1019],{"class":176},"1",[159,1021,1022],{"class":183},"]\n",[159,1024,1025,1027,1030,1032,1034,1036,1038,1040,1042,1045,1047],{"class":161,"line":549},[159,1026,1003],{"class":183},[159,1028,1029],{"class":326},"\"runs\"",[159,1031,1008],{"class":183},[159,1033,397],{"class":172},[159,1035,942],{"class":183},[159,1037,1029],{"class":326},[159,1039,476],{"class":183},[159,1041,636],{"class":176},[159,1043,1044],{"class":183},") ",[159,1046,499],{"class":172},[159,1048,1049],{"class":176}," 1\n",[159,1051,1052,1054,1057,1059],{"class":161,"line":563},[159,1053,543],{"class":172},[159,1055,1056],{"class":183}," Busy ",[159,1058,929],{"class":172},[159,1060,1061],{"class":183}," exc:\n",[159,1063,1064,1067,1069,1072,1074,1077,1079,1082,1084,1087,1089,1092,1094,1097,1099,1101],{"class":161,"line":570},[159,1065,1066],{"class":183},"        typer.echo(",[159,1068,367],{"class":172},[159,1070,1071],{"class":326},"\"error: ",[159,1073,373],{"class":176},[159,1075,1076],{"class":183},"exc",[159,1078,379],{"class":176},[159,1080,1081],{"class":326}," (lock: ",[159,1083,373],{"class":176},[159,1085,1086],{"class":183},"exc.lock_path",[159,1088,379],{"class":176},[159,1090,1091],{"class":326},")\"",[159,1093,476],{"class":183},[159,1095,1096],{"class":467},"err",[159,1098,397],{"class":172},[159,1100,473],{"class":176},[159,1102,385],{"class":183},[159,1104,1105,1107,1110,1112],{"class":161,"line":604},[159,1106,552],{"class":172},[159,1108,1109],{"class":183}," typer.Exit(",[159,1111,742],{"class":176},[159,1113,385],{"class":183},[159,1115,1116,1119,1121,1124,1126,1129,1131,1134,1136,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161],{"class":161,"line":613},[159,1117,1118],{"class":183},"    typer.echo(",[159,1120,367],{"class":172},[159,1122,1123],{"class":326},"\"synced items ",[159,1125,373],{"class":176},[159,1127,1128],{"class":183},"new_items[",[159,1130,636],{"class":176},[159,1132,1133],{"class":183},"]",[159,1135,379],{"class":176},[159,1137,1138],{"class":326},"..",[159,1140,373],{"class":176},[159,1142,1128],{"class":183},[159,1144,1016],{"class":172},[159,1146,1019],{"class":176},[159,1148,1133],{"class":183},[159,1150,379],{"class":176},[159,1152,382],{"class":326},[159,1154,476],{"class":183},[159,1156,1096],{"class":467},[159,1158,397],{"class":172},[159,1160,473],{"class":176},[159,1162,385],{"class":183},[159,1164,1165],{"class":161,"line":619},[159,1166,191],{"emptyLinePlaceholder":190},[159,1168,1169],{"class":161,"line":627},[159,1170,191],{"emptyLinePlaceholder":190},[159,1172,1174,1176,1179,1182,1185],{"class":161,"line":1173},39,[159,1175,592],{"class":172},[159,1177,1178],{"class":176}," __name__",[159,1180,1181],{"class":172}," ==",[159,1183,1184],{"class":326}," \"__main__\"",[159,1186,350],{"class":183},[159,1188,1190],{"class":161,"line":1189},40,[159,1191,1192],{"class":183},"    app()\n",[1194,1195,1197],"h3",{"id":1196},"locking-the-resource-versus-locking-the-program","Locking the resource versus locking the program",[10,1199,1200,1201,1204,1205,1208,1209,1212],{},"A \"single instance\" guard — one lock for the whole program, taken at startup — is sometimes what you want: a daemon-like ",[32,1202,1203],{},"watch"," command that must never run twice. More often it is too coarse. Two ",[32,1206,1207],{},"mytool sync --project a"," and ",[32,1210,1211],{},"--project b"," runs touch different state and should be free to run in parallel; a program-wide lock serialises them for no reason. Prefer one lock per resource you mutate, and add a program-wide lock only for commands that genuinely own the whole tool.",[1194,1214,1216],{"id":1215},"where-the-lock-file-lives","Where the lock file lives",[10,1218,1219,1220,1224],{},"Next to the data it protects, on the same filesystem, in a directory your tool owns — the state directory from ",[14,1221,1223],{"href":1222},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs"," is ideal. Do not delete lock files after releasing them; with OS locks, an existing unlocked file is harmless, and deleting it opens a race in which two processes lock two different inodes with the same name.",[20,1226,1228],{"id":1227},"ux-considerations","UX considerations",[58,1230],{"name":1231},"fs-lock-wait-terminal",[25,1233,1234,1244,1257,1278,1284],{},[28,1235,1236,1239,1240,1243],{},[83,1237,1238],{},"Tell the user you are waiting."," A command that silently blocks for thirty seconds looks hung. Print one line when the lock is not immediately available — try ",[32,1241,1242],{},"acquire(timeout=0)"," first, and only print before a blocking retry.",[28,1245,1246,1249,1250,1208,1253,1256],{},[83,1247,1248],{},"Offer both behaviours."," Interactive users usually prefer to wait a little; cron jobs and CI usually prefer to fail fast and try again on the next schedule. ",[32,1251,1252],{},"--wait SECONDS",[32,1254,1255],{},"--no-wait"," cover both.",[28,1258,1259,1262,1263,1266,1267,1269,1270,1273,1274,18],{},[83,1260,1261],{},"Exit with a \"try again\" code."," ",[32,1264,1265],{},"75"," (",[32,1268,742],{}," from ",[32,1271,1272],{},"sysexits.h",") tells a scheduler that the failure is transient. It is more useful than a generic 1 — see ",[14,1275,1277],{"href":1276},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",[28,1279,1280,1283],{},[83,1281,1282],{},"Name the lock in the error."," Printing the lock path lets an operator see which resource is contended and, if a network filesystem is misbehaving, find it.",[28,1285,1286,1289],{},[83,1287,1288],{},"Keep critical sections short."," Hold the lock only around the read-modify-write, not around a ten-minute download. If the work is long, read state, release, work, then re-lock and merge results — or accept that runs serialise, and say so in the help text.",[20,1291,1293],{"id":1292},"testing-the-behaviour","Testing the behaviour",[10,1295,1296,1297,1299,1300,1304,1305,1308],{},"A lock test must use real concurrency — two processes, or at least two threads with separate lock objects — or it proves nothing. ",[32,1298,34],{}," locks are re-entrant per object ",[1301,1302,1303],"em",{},"within"," a process, so use separate ",[32,1306,1307],{},"FileLock"," instances, or better, separate processes:",[150,1310,1312],{"className":152,"code":1311,"language":154,"meta":155,"style":155},"# tests\u002Ftest_state.py\nimport json\nfrom concurrent.futures import ProcessPoolExecutor\nfrom pathlib import Path\n\nimport pytest\nfrom filelock import FileLock\n\nfrom mytool.state import Busy, locked_json\n\n\ndef bump(path_str: str) -> None:\n    path = Path(path_str)\n    for _ in range(50):\n        with locked_json(path, timeout=10) as state:\n            state[\"n\"] = state.get(\"n\", 0) + 1\n\n\ndef test_no_lost_updates_across_processes(tmp_path):\n    path = tmp_path \u002F \"state.json\"\n    with ProcessPoolExecutor(max_workers=4) as pool:\n        list(pool.map(bump, [str(path)] * 4))\n    assert json.loads(path.read_text())[\"n\"] == 200\n\n\ndef test_busy_when_locked_elsewhere(tmp_path):\n    path = tmp_path \u002F \"state.json\"\n    other = FileLock(path.with_name(\"state.json.lock\"))\n    with other:\n        with pytest.raises(Busy):\n            with locked_json(path, timeout=0):\n                pass\n\n\ndef test_error_in_block_writes_nothing(tmp_path):\n    path = tmp_path \u002F \"state.json\"\n    with locked_json(path) as state:\n        state[\"n\"] = 1\n    with pytest.raises(RuntimeError):\n        with locked_json(path) as state:\n            state[\"n\"] = 99\n            raise RuntimeError\n    assert json.loads(path.read_text()) == {\"n\": 1}\n",[32,1313,1314,1319,1325,1337,1347,1351,1358,1369,1373,1383,1387,1391,1409,1419,1440,1460,1485,1489,1493,1503,1517,1540,1560,1578,1582,1586,1595,1607,1622,1629,1636,1651,1656,1660,1664,1673,1685,1696,1709,1721,1731,1745,1754],{"__ignoreMap":155},[159,1315,1316],{"class":161,"line":162},[159,1317,1318],{"class":165},"# tests\u002Ftest_state.py\n",[159,1320,1321,1323],{"class":161,"line":169},[159,1322,197],{"class":172},[159,1324,200],{"class":183},[159,1326,1327,1329,1332,1334],{"class":161,"line":187},[159,1328,173],{"class":172},[159,1330,1331],{"class":183}," concurrent.futures ",[159,1333,197],{"class":172},[159,1335,1336],{"class":183}," ProcessPoolExecutor\n",[159,1338,1339,1341,1343,1345],{"class":161,"line":194},[159,1340,173],{"class":172},[159,1342,234],{"class":183},[159,1344,197],{"class":172},[159,1346,239],{"class":183},[159,1348,1349],{"class":161,"line":203},[159,1350,191],{"emptyLinePlaceholder":190},[159,1352,1353,1355],{"class":161,"line":216},[159,1354,197],{"class":172},[159,1356,1357],{"class":183}," pytest\n",[159,1359,1360,1362,1364,1366],{"class":161,"line":229},[159,1361,173],{"class":172},[159,1363,265],{"class":183},[159,1365,197],{"class":172},[159,1367,1368],{"class":183}," FileLock\n",[159,1370,1371],{"class":161,"line":242},[159,1372,191],{"emptyLinePlaceholder":190},[159,1374,1375,1377,1379,1381],{"class":161,"line":255},[159,1376,173],{"class":172},[159,1378,686],{"class":183},[159,1380,197],{"class":172},[159,1382,691],{"class":183},[159,1384,1385],{"class":161,"line":260},[159,1386,191],{"emptyLinePlaceholder":190},[159,1388,1389],{"class":161,"line":273},[159,1390,191],{"emptyLinePlaceholder":190},[159,1392,1393,1395,1398,1401,1403,1405,1407],{"class":161,"line":278},[159,1394,422],{"class":172},[159,1396,1397],{"class":310}," bump",[159,1399,1400],{"class":183},"(path_str: ",[159,1402,449],{"class":176},[159,1404,871],{"class":183},[159,1406,347],{"class":176},[159,1408,350],{"class":183},[159,1410,1411,1414,1416],{"class":161,"line":294},[159,1412,1413],{"class":183},"    path ",[159,1415,397],{"class":172},[159,1417,1418],{"class":183}," Path(path_str)\n",[159,1420,1421,1424,1427,1430,1433,1435,1438],{"class":161,"line":299},[159,1422,1423],{"class":172},"    for",[159,1425,1426],{"class":183}," _ ",[159,1428,1429],{"class":172},"in",[159,1431,1432],{"class":176}," range",[159,1434,314],{"class":183},[159,1436,1437],{"class":176},"50",[159,1439,320],{"class":183},[159,1441,1442,1444,1447,1449,1451,1454,1456,1458],{"class":161,"line":304},[159,1443,912],{"class":172},[159,1445,1446],{"class":183}," locked_json(path, ",[159,1448,532],{"class":467},[159,1450,397],{"class":172},[159,1452,1453],{"class":176},"10",[159,1455,1044],{"class":183},[159,1457,929],{"class":172},[159,1459,932],{"class":183},[159,1461,1462,1464,1467,1469,1471,1473,1475,1477,1479,1481,1483],{"class":161,"line":323},[159,1463,1003],{"class":183},[159,1465,1466],{"class":326},"\"n\"",[159,1468,1008],{"class":183},[159,1470,397],{"class":172},[159,1472,942],{"class":183},[159,1474,1466],{"class":326},[159,1476,476],{"class":183},[159,1478,636],{"class":176},[159,1480,1044],{"class":183},[159,1482,499],{"class":172},[159,1484,1049],{"class":176},[159,1486,1487],{"class":161,"line":330},[159,1488,191],{"emptyLinePlaceholder":190},[159,1490,1491],{"class":161,"line":335},[159,1492,191],{"emptyLinePlaceholder":190},[159,1494,1495,1497,1500],{"class":161,"line":353},[159,1496,422],{"class":172},[159,1498,1499],{"class":310}," test_no_lost_updates_across_processes",[159,1501,1502],{"class":183},"(tmp_path):\n",[159,1504,1505,1507,1509,1512,1514],{"class":161,"line":388},[159,1506,1413],{"class":183},[159,1508,397],{"class":172},[159,1510,1511],{"class":183}," tmp_path ",[159,1513,718],{"class":172},[159,1515,1516],{"class":326}," \"state.json\"\n",[159,1518,1519,1522,1525,1528,1530,1533,1535,1537],{"class":161,"line":403},[159,1520,1521],{"class":172},"    with",[159,1523,1524],{"class":183}," ProcessPoolExecutor(",[159,1526,1527],{"class":467},"max_workers",[159,1529,397],{"class":172},[159,1531,1532],{"class":176},"4",[159,1534,1044],{"class":183},[159,1536,929],{"class":172},[159,1538,1539],{"class":183}," pool:\n",[159,1541,1542,1545,1548,1550,1553,1555,1557],{"class":161,"line":408},[159,1543,1544],{"class":176},"        list",[159,1546,1547],{"class":183},"(pool.map(bump, [",[159,1549,449],{"class":176},[159,1551,1552],{"class":183},"(path)] ",[159,1554,431],{"class":172},[159,1556,982],{"class":176},[159,1558,1559],{"class":183},"))\n",[159,1561,1562,1565,1568,1570,1572,1575],{"class":161,"line":413},[159,1563,1564],{"class":172},"    assert",[159,1566,1567],{"class":183}," json.loads(path.read_text())[",[159,1569,1466],{"class":326},[159,1571,1008],{"class":183},[159,1573,1574],{"class":172},"==",[159,1576,1577],{"class":176}," 200\n",[159,1579,1580],{"class":161,"line":419},[159,1581,191],{"emptyLinePlaceholder":190},[159,1583,1584],{"class":161,"line":455},[159,1585,191],{"emptyLinePlaceholder":190},[159,1587,1588,1590,1593],{"class":161,"line":461},[159,1589,422],{"class":172},[159,1591,1592],{"class":310}," test_busy_when_locked_elsewhere",[159,1594,1502],{"class":183},[159,1596,1597,1599,1601,1603,1605],{"class":161,"line":488},[159,1598,1413],{"class":183},[159,1600,397],{"class":172},[159,1602,1511],{"class":183},[159,1604,718],{"class":172},[159,1606,1516],{"class":326},[159,1608,1609,1612,1614,1617,1620],{"class":161,"line":507},[159,1610,1611],{"class":183},"    other ",[159,1613,397],{"class":172},[159,1615,1616],{"class":183}," FileLock(path.with_name(",[159,1618,1619],{"class":326},"\"state.json.lock\"",[159,1621,1559],{"class":183},[159,1623,1624,1626],{"class":161,"line":518},[159,1625,1521],{"class":172},[159,1627,1628],{"class":183}," other:\n",[159,1630,1631,1633],{"class":161,"line":526},[159,1632,912],{"class":172},[159,1634,1635],{"class":183}," pytest.raises(Busy):\n",[159,1637,1638,1641,1643,1645,1647,1649],{"class":161,"line":540},[159,1639,1640],{"class":172},"            with",[159,1642,1446],{"class":183},[159,1644,532],{"class":467},[159,1646,397],{"class":172},[159,1648,636],{"class":176},[159,1650,320],{"class":183},[159,1652,1653],{"class":161,"line":549},[159,1654,1655],{"class":172},"                pass\n",[159,1657,1658],{"class":161,"line":563},[159,1659,191],{"emptyLinePlaceholder":190},[159,1661,1662],{"class":161,"line":570},[159,1663,191],{"emptyLinePlaceholder":190},[159,1665,1666,1668,1671],{"class":161,"line":604},[159,1667,422],{"class":172},[159,1669,1670],{"class":310}," test_error_in_block_writes_nothing",[159,1672,1502],{"class":183},[159,1674,1675,1677,1679,1681,1683],{"class":161,"line":613},[159,1676,1413],{"class":183},[159,1678,397],{"class":172},[159,1680,1511],{"class":183},[159,1682,718],{"class":172},[159,1684,1516],{"class":326},[159,1686,1687,1689,1692,1694],{"class":161,"line":619},[159,1688,1521],{"class":172},[159,1690,1691],{"class":183}," locked_json(path) ",[159,1693,929],{"class":172},[159,1695,932],{"class":183},[159,1697,1698,1701,1703,1705,1707],{"class":161,"line":627},[159,1699,1700],{"class":183},"        state[",[159,1702,1466],{"class":326},[159,1704,1008],{"class":183},[159,1706,397],{"class":172},[159,1708,1049],{"class":176},[159,1710,1711,1713,1716,1719],{"class":161,"line":1173},[159,1712,1521],{"class":172},[159,1714,1715],{"class":183}," pytest.raises(",[159,1717,1718],{"class":176},"RuntimeError",[159,1720,320],{"class":183},[159,1722,1723,1725,1727,1729],{"class":161,"line":1189},[159,1724,912],{"class":172},[159,1726,1691],{"class":183},[159,1728,929],{"class":172},[159,1730,932],{"class":183},[159,1732,1734,1736,1738,1740,1742],{"class":161,"line":1733},41,[159,1735,1003],{"class":183},[159,1737,1466],{"class":326},[159,1739,1008],{"class":183},[159,1741,397],{"class":172},[159,1743,1744],{"class":176}," 99\n",[159,1746,1748,1751],{"class":161,"line":1747},42,[159,1749,1750],{"class":172},"            raise",[159,1752,1753],{"class":176}," RuntimeError\n",[159,1755,1757,1759,1762,1764,1767,1769,1772,1774],{"class":161,"line":1756},43,[159,1758,1564],{"class":172},[159,1760,1761],{"class":183}," json.loads(path.read_text()) ",[159,1763,1574],{"class":172},[159,1765,1766],{"class":183}," {",[159,1768,1466],{"class":326},[159,1770,1771],{"class":183},": ",[159,1773,1019],{"class":176},[159,1775,1776],{"class":183},"}\n",[10,1778,1779,1780,1783,1784,1787],{},"Remove the lock from ",[32,1781,1782],{},"locked_json"," and the first test fails with a count well under 200 on almost every run — which is the demonstration that the lock is doing real work. Keep the worker function at module level so ",[32,1785,1786],{},"ProcessPoolExecutor"," can pickle it.",[20,1789,1791],{"id":1790},"conclusion","Conclusion",[10,1793,1794,1795,1797],{},"Atomic writes protect a file from crashes; locks protect it from your own tool running twice. Use OS-level locks — through ",[32,1796,34],{}," for portability — because they vanish with the process that held them, and scope each lock to the resource and the read-modify-write cycle that needs it. Let users choose between waiting and failing fast, exit with a retryable code when the lock is busy, and prove it all with a multi-process test.",[20,1799,1801],{"id":1800},"frequently-asked-questions","Frequently asked questions",[1194,1803,1805,1806,1808],{"id":1804},"does-filelock-work-on-nfs-or-smb-shares","Does ",[32,1807,34],{}," work on NFS or SMB shares?",[10,1810,1811,1812,1814,1815,1817],{},"OS locks over network filesystems depend on the server and client configuration and are not reliable everywhere. If your state lives on a network share, use ",[32,1813,133],{}," (a lock file created with ",[32,1816,98],{},") and accept that a crashed process leaves a stale lock to be removed by hand — or better, keep state on local disk.",[1194,1819,1821],{"id":1820},"can-i-use-a-lock-to-stop-two-copies-of-a-long-running-command","Can I use a lock to stop two copies of a long-running command?",[10,1823,1824,1825,1828,1829,18],{},"Yes: acquire a program-wide lock with ",[32,1826,1827],{},"timeout=0"," at the start of the command and hold it for the whole run. That is the right design for a watcher or scheduler loop; see ",[14,1830,1832],{"href":1831},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles\u002F","building a watch mode with watchfiles",[1194,1834,1836],{"id":1835},"what-about-threads-inside-one-process","What about threads inside one process?",[10,1838,1839,1841,1842,1844,1845,1848],{},[32,1840,34],{}," locks are re-entrant for the same ",[32,1843,1307],{}," object, which means threads sharing one object do not exclude each other. Use ",[32,1846,1847],{},"threading.Lock"," for coordination between threads, and a file lock only for coordination between processes.",[1194,1850,1852],{"id":1851},"is-sqlite-a-better-answer-than-json-plus-a-lock","Is SQLite a better answer than JSON plus a lock?",[10,1854,1855],{},"For state that is updated often or queried, frequently yes. SQLite handles locking and atomic transactions itself, works across processes, and ships with Python. JSON plus a lock remains simpler for a handful of values that change once per run.",[20,1857,1859],{"id":1858},"related","Related",[25,1861,1862,1868,1873,1878,1884],{},[28,1863,1864,1865],{},"Up: ",[14,1866,1867],{"href":16},"Filesystem paths and atomic writes",[28,1869,1870],{},[14,1871,1872],{"href":45},"Writing files atomically in Python CLIs",[28,1874,1875],{},[14,1876,1877],{"href":1222},"Storing app data with platformdirs",[28,1879,1880],{},[14,1881,1883],{"href":1882},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd\u002F","Running a CLI on a schedule with cron and systemd",[28,1885,1886],{},[14,1887,1889],{"href":1888},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","Parallelising CLI work with thread pools",[1891,1892,1893],"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":155,"searchDepth":169,"depth":169,"links":1895},[1896,1897,1898,1899,1903,1904,1905,1906,1913],{"id":22,"depth":169,"text":23},{"id":52,"depth":169,"text":53},{"id":77,"depth":169,"text":78},{"id":140,"depth":169,"text":141,"children":1900},[1901,1902],{"id":1196,"depth":187,"text":1197},{"id":1215,"depth":187,"text":1216},{"id":1227,"depth":169,"text":1228},{"id":1292,"depth":169,"text":1293},{"id":1790,"depth":169,"text":1791},{"id":1800,"depth":169,"text":1801,"children":1907},[1908,1910,1911,1912],{"id":1804,"depth":187,"text":1909},"Does filelock work on NFS or SMB shares?",{"id":1820,"depth":187,"text":1821},{"id":1835,"depth":187,"text":1836},{"id":1851,"depth":187,"text":1852},{"id":1858,"depth":169,"text":1859},"2026-09-18","Stop two runs of a Python CLI from corrupting shared state: OS-level locks with filelock or fcntl, wait-or-fail options, single-instance guards and tests.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs",{"title":5,"description":1915},"cli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs\u002Findex",[1924,1925,1926,1927],"locking","concurrency","filesystem","reliability","XtVvMtZjsmvt-4eFqehLeXPg6lTHQGi66h0w_-3D5ZM",[1930,1933,1936,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978,1981,1984,1987,1990,1993,1996,1999,2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2092,2095,2098,2101,2104,2107,2110,2113,2116,2119,2122,2125,2128,2131,2134,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195,2198,2201,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2233,2236,2239,2242,2245,2248,2251,2254,2257,2260,2263,2266,2269,2272,2275,2278,2281,2284,2287,2290,2293,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398,2401,2404,2407,2410,2413,2416,2419,2422,2425,2428,2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2461,2464,2467,2470,2473],{"path":1931,"title":1932},"\u002Fabout","About Python CLI Toolcraft",{"path":1934,"title":1935},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1937,"title":1938},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1943,"title":1944},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1946,"title":1947},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1949,"title":1950},"\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":1952,"title":1953},"\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":1955,"title":1956},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1958,"title":1959},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1961,"title":1962},"\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":1964,"title":1965},"\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":1967,"title":1968},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1970,"title":1971},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1973,"title":1974},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1976,"title":1977},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1979,"title":1980},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1982,"title":1983},"\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":1985,"title":1986},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1988,"title":1989},"\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":1991,"title":1992},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1994,"title":1995},"\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":1997,"title":1998},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2000,"title":2001},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2003,"title":2004},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2006,"title":2007},"\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":2009,"title":2010},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2012,"title":2013},"\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":2015,"title":2016},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2018,"title":2019},"\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":2021,"title":2022},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2024,"title":2025},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2027,"title":2028},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2030,"title":2031},"\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":2033,"title":2034},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2036,"title":2037},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2039,"title":2040},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2042,"title":2043},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2045,"title":2046},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2048,"title":2049},"\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":2051,"title":2052},"\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":2054,"title":2055},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2057,"title":2058},"\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":2060,"title":2061},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2063,"title":2064},"\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":2066,"title":2067},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2069,"title":2070},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2072,"title":2073},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2075,"title":2076},"\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":2078,"title":2079},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2081,"title":2082},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2084,"title":2085},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2087,"title":2088},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2090,"title":2091},"\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":2093,"title":2094},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2096,"title":2097},"\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":2099,"title":2100},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2102,"title":2103},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2105,"title":2106},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2108,"title":2109},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2111,"title":2112},"\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":2114,"title":2115},"\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":2117,"title":2118},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2120,"title":2121},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2123,"title":2124},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2126,"title":2127},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2129,"title":2130},"\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":2132,"title":2133},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":1920,"title":5},{"path":2136,"title":2137},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2139,"title":2140},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2142,"title":2143},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2145,"title":2146},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2148,"title":2149},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2151,"title":2152},"\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":2154,"title":2155},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2157,"title":2158},"\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":2160,"title":2161},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2163,"title":2164},"\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":2166,"title":2167},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2169,"title":2170},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2172,"title":2173},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2175,"title":2176},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2178,"title":2179},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2181,"title":2182},"\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":2184,"title":2185},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2187,"title":2188},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2190,"title":2191},"\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":2193,"title":2194},"\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":2196,"title":2197},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2199,"title":2200},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":718,"title":2202},"Python CLI Toolcraft",{"path":2204,"title":2205},"\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":2207,"title":2208},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2210,"title":2211},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2213,"title":2214},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2216,"title":2217},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2219,"title":2220},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2222,"title":2223},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2225,"title":2226},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2228,"title":2229},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2231,"title":2232},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2234,"title":2235},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2237,"title":2238},"\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":2240,"title":2241},"\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":2243,"title":2244},"\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":2246,"title":2247},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2249,"title":2250},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2252,"title":2253},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2255,"title":2256},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2258,"title":2259},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2261,"title":2262},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2264,"title":2265},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2267,"title":2268},"\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":2270,"title":2271},"\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":2273,"title":2274},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2276,"title":2277},"\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":2279,"title":2280},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2282,"title":2283},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2285,"title":2286},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2288,"title":2289},"\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":2291,"title":2292},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2294,"title":2295},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2297,"title":2298},"\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":2300,"title":2301},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2303,"title":2304},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2306,"title":2307},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2309,"title":2310},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2312,"title":2313},"\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":2315,"title":2316},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2318,"title":2319},"\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":2321,"title":2322},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2324,"title":2325},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2327,"title":2328},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2330,"title":2331},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2333,"title":2334},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2339,"title":2340},"\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":2342,"title":2343},"\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":2345,"title":2346},"\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":2348,"title":2349},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2354,"title":2355},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2357,"title":2358},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2363,"title":2364},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2366,"title":2367},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2369,"title":2370},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2375,"title":2376},"\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":2378,"title":2379},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2381,"title":2382},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2384,"title":2385},"\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":2387,"title":2388},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2390,"title":2391},"\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":2393,"title":2394},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2396,"title":2397},"\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":2399,"title":2400},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2402,"title":2403},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2405,"title":2406},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2408,"title":2409},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2411,"title":2412},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2414,"title":2415},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2417,"title":2418},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2420,"title":2421},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2423,"title":2424},"\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":2426,"title":2427},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2429,"title":2430},"\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":2432,"title":2433},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2435,"title":2436},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2438,"title":2439},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2441,"title":2442},"\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":2444,"title":2445},"\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":2447,"title":2448},"\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":2450,"title":2451},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2453,"title":2454},"\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":2456,"title":2457},"\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":2459,"title":2460},"\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":2462,"title":2463},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2465,"title":2466},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2468,"title":2469},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2471,"title":2472},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2474,"title":2475},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905049]