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