[{"data":1,"prerenderedAt":2416},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F":3,"content-directory":1869},{"id":4,"title":5,"body":6,"date":1855,"description":1856,"difficulty":1857,"draft":1858,"extension":1859,"meta":1860,"navigation":118,"path":1861,"seo":1862,"stem":1863,"tags":1864,"updated":1855,"__hash__":1868},"content\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002Findex.md","Cross-Platform Paths with pathlib in CLIs",{"type":7,"value":8,"toc":1834},"minimark",[9,40,45,63,67,81,85,254,262,266,276,1004,1007,1085,1088,1093,1098,1178,1182,1189,1192,1245,1249,1257,1654,1657,1661,1679,1683,1692,1709,1717,1728,1732,1753,1759,1775,1779,1795,1799,1830],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"A CLI written on macOS works for everyone on the team until the first Windows user runs it. Then ",[14,15,16],"code",{},"path.split(\"\u002F\")[-1]"," returns the whole path, ",[14,19,20],{},"\"~\u002Freports\""," creates a directory literally named ",[14,23,24],{},"~",", ",[14,27,28],{},"os.path.join(root, \"data\u002Fraw\")"," produces a mixed-separator path that some tools reject, and a manifest file written with backslashes breaks the Linux CI job that reads it. None of this is exotic; it is the everyday cost of treating paths as strings. This guide shows how to use ",[14,31,32],{},"pathlib"," consistently in a CLI — from argument parsing to output — so the same code behaves correctly on every platform. It is part of the ",[35,36,38],"a",{"href":37},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","filesystem topic",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,48,52,55],"ul",{},[49,50,51],"li",{},"Python 3.10+ (a couple of methods noted below need 3.12).",[49,53,54],{},"A Typer or Click CLI.",[49,56,57,58,62],{},"Ideally, access to a Windows machine or a Windows CI runner to confirm the results. ",[35,59,61],{"href":60},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","Testing a CLI across Python versions with GitHub Actions"," shows how to add one.",[41,64,66],{"id":65},"paths-are-objects-not-strings","Paths are objects, not strings",[10,68,69,72,73,76,77,80],{},[14,70,71],{},"pathlib.Path"," represents a path as a structured object. On Windows it is a ",[14,74,75],{},"WindowsPath","; elsewhere a ",[14,78,79],{},"PosixPath",". Both expose the same interface, and the operations that trip people up with strings — joining, splitting off the name, changing an extension, finding a parent — are properties and methods that know the platform's rules.",[82,83],"inline-diagram",{"name":84},"fs-path-anatomy",[86,87,92],"pre",{"className":88,"code":89,"language":90,"meta":91,"style":91},"language-python shiki shiki-themes github-light github-dark","from pathlib import Path\n\np = Path(\"reports\") \u002F \"2026\" \u002F \"q3.tar.gz\"\nprint(p.parent)       # reports\u002F2026   (reports\\2026 on Windows)\nprint(p.name)         # q3.tar.gz\nprint(p.stem)         # q3.tar\nprint(p.suffix)       # .gz\nprint(p.suffixes)     # ['.tar', '.gz']\nprint(p.with_suffix(\".zip\"))        # reports\u002F2026\u002Fq3.tar.zip\nprint(p.with_name(\"q4.tar.gz\"))     # reports\u002F2026\u002Fq4.tar.gz\nprint(p.as_posix())   # reports\u002F2026\u002Fq3.tar.gz on every platform\n","python","",[14,93,94,113,120,151,165,176,187,198,209,226,243],{"__ignoreMap":91},[95,96,99,103,107,110],"span",{"class":97,"line":98},"line",1,[95,100,102],{"class":101},"szBVR","from",[95,104,106],{"class":105},"sVt8B"," pathlib ",[95,108,109],{"class":101},"import",[95,111,112],{"class":105}," Path\n",[95,114,116],{"class":97,"line":115},2,[95,117,119],{"emptyLinePlaceholder":118},true,"\n",[95,121,123,126,129,132,136,139,142,145,148],{"class":97,"line":122},3,[95,124,125],{"class":105},"p ",[95,127,128],{"class":101},"=",[95,130,131],{"class":105}," Path(",[95,133,135],{"class":134},"sZZnC","\"reports\"",[95,137,138],{"class":105},") ",[95,140,141],{"class":101},"\u002F",[95,143,144],{"class":134}," \"2026\"",[95,146,147],{"class":101}," \u002F",[95,149,150],{"class":134}," \"q3.tar.gz\"\n",[95,152,154,158,161],{"class":97,"line":153},4,[95,155,157],{"class":156},"sj4cs","print",[95,159,160],{"class":105},"(p.parent)       ",[95,162,164],{"class":163},"sJ8bj","# reports\u002F2026   (reports\\2026 on Windows)\n",[95,166,168,170,173],{"class":97,"line":167},5,[95,169,157],{"class":156},[95,171,172],{"class":105},"(p.name)         ",[95,174,175],{"class":163},"# q3.tar.gz\n",[95,177,179,181,184],{"class":97,"line":178},6,[95,180,157],{"class":156},[95,182,183],{"class":105},"(p.stem)         ",[95,185,186],{"class":163},"# q3.tar\n",[95,188,190,192,195],{"class":97,"line":189},7,[95,191,157],{"class":156},[95,193,194],{"class":105},"(p.suffix)       ",[95,196,197],{"class":163},"# .gz\n",[95,199,201,203,206],{"class":97,"line":200},8,[95,202,157],{"class":156},[95,204,205],{"class":105},"(p.suffixes)     ",[95,207,208],{"class":163},"# ['.tar', '.gz']\n",[95,210,212,214,217,220,223],{"class":97,"line":211},9,[95,213,157],{"class":156},[95,215,216],{"class":105},"(p.with_suffix(",[95,218,219],{"class":134},"\".zip\"",[95,221,222],{"class":105},"))        ",[95,224,225],{"class":163},"# reports\u002F2026\u002Fq3.tar.zip\n",[95,227,229,231,234,237,240],{"class":97,"line":228},10,[95,230,157],{"class":156},[95,232,233],{"class":105},"(p.with_name(",[95,235,236],{"class":134},"\"q4.tar.gz\"",[95,238,239],{"class":105},"))     ",[95,241,242],{"class":163},"# reports\u002F2026\u002Fq4.tar.gz\n",[95,244,246,248,251],{"class":97,"line":245},11,[95,247,157],{"class":156},[95,249,250],{"class":105},"(p.as_posix())   ",[95,252,253],{"class":163},"# reports\u002F2026\u002Fq3.tar.gz on every platform\n",[10,255,256,257,261],{},"These are ",[258,259,260],"em",{},"pure"," operations: none of them touch the disk. That makes them cheap and safe to use in validation code and tests.",[41,263,265],{"id":264},"the-recipe-pathlib-from-edge-to-edge","The recipe: pathlib from edge to edge",[10,267,268,269,272,273,275],{},"The rule for a CLI is simple: convert to ",[14,270,271],{},"Path"," at the edge where input arrives, keep it as a ",[14,274,271],{}," throughout, and convert to a string only at the edge where something outside Python demands one. Here is a small but complete command that follows it — it collects log files under a directory into a gzip archive:",[86,277,279],{"className":88,"code":278,"language":90,"meta":91,"style":91},"# src\u002Fmytool\u002Fcli.py\nfrom __future__ import annotations\n\nimport gzip\nimport shutil\nfrom pathlib import Path\n\nimport typer\n\napp = typer.Typer()\n\n\ndef display(p: Path, base: Path | None = None) -> str:\n    \"\"\"Short, recognisable form of a path for humans.\"\"\"\n    base = base or Path.cwd()\n    try:\n        return str(p.relative_to(base))\n    except ValueError:\n        pass\n    try:\n        return str(Path(\"~\") \u002F p.relative_to(Path.home()))\n    except ValueError:\n        return str(p)\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Log utilities.\"\"\"\n\n\n@app.command()\ndef bundle(\n    root: Path = typer.Argument(..., exists=True, file_okay=False, resolve_path=True),\n    out: Path = typer.Option(Path(\"logs.txt.gz\"), \"--out\", \"-o\", dir_okay=False),\n    pattern: str = typer.Option(\"*.log\", help=\"Glob, relative to ROOT.\"),\n) -> None:\n    \"\"\"Concatenate every file matching PATTERN under ROOT into one gzip file.\"\"\"\n    out = out.expanduser().resolve()\n    files = sorted(p for p in root.rglob(pattern) if p.is_file() and p != out)\n    if not files:\n        typer.echo(f\"no files matching {pattern!r} under {display(root)}\", err=True)\n        raise typer.Exit(1)\n    out.parent.mkdir(parents=True, exist_ok=True)\n    with gzip.open(out, \"wb\") as dst:\n        for f in files:\n            dst.write(f\"# {f.relative_to(root).as_posix()}\\n\".encode())\n            with f.open(\"rb\") as src:\n                shutil.copyfileobj(src, dst)\n    typer.echo(f\"bundled {len(files)} files into {display(out)}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,280,281,286,299,303,310,317,327,331,338,342,352,356,361,394,400,417,425,437,448,454,461,481,490,500,505,510,519,535,541,546,551,559,570,617,653,681,690,696,707,750,762,811,825,849,869,882,906,925,931,972,977,982,998],{"__ignoreMap":91},[95,282,283],{"class":97,"line":98},[95,284,285],{"class":163},"# src\u002Fmytool\u002Fcli.py\n",[95,287,288,290,293,296],{"class":97,"line":115},[95,289,102],{"class":101},[95,291,292],{"class":156}," __future__",[95,294,295],{"class":101}," import",[95,297,298],{"class":105}," annotations\n",[95,300,301],{"class":97,"line":122},[95,302,119],{"emptyLinePlaceholder":118},[95,304,305,307],{"class":97,"line":153},[95,306,109],{"class":101},[95,308,309],{"class":105}," gzip\n",[95,311,312,314],{"class":97,"line":167},[95,313,109],{"class":101},[95,315,316],{"class":105}," shutil\n",[95,318,319,321,323,325],{"class":97,"line":178},[95,320,102],{"class":101},[95,322,106],{"class":105},[95,324,109],{"class":101},[95,326,112],{"class":105},[95,328,329],{"class":97,"line":189},[95,330,119],{"emptyLinePlaceholder":118},[95,332,333,335],{"class":97,"line":200},[95,334,109],{"class":101},[95,336,337],{"class":105}," typer\n",[95,339,340],{"class":97,"line":211},[95,341,119],{"emptyLinePlaceholder":118},[95,343,344,347,349],{"class":97,"line":228},[95,345,346],{"class":105},"app ",[95,348,128],{"class":101},[95,350,351],{"class":105}," typer.Typer()\n",[95,353,354],{"class":97,"line":245},[95,355,119],{"emptyLinePlaceholder":118},[95,357,359],{"class":97,"line":358},12,[95,360,119],{"emptyLinePlaceholder":118},[95,362,364,367,371,374,377,380,383,385,388,391],{"class":97,"line":363},13,[95,365,366],{"class":101},"def",[95,368,370],{"class":369},"sScJk"," display",[95,372,373],{"class":105},"(p: Path, base: Path ",[95,375,376],{"class":101},"|",[95,378,379],{"class":156}," None",[95,381,382],{"class":101}," =",[95,384,379],{"class":156},[95,386,387],{"class":105},") -> ",[95,389,390],{"class":156},"str",[95,392,393],{"class":105},":\n",[95,395,397],{"class":97,"line":396},14,[95,398,399],{"class":134},"    \"\"\"Short, recognisable form of a path for humans.\"\"\"\n",[95,401,403,406,408,411,414],{"class":97,"line":402},15,[95,404,405],{"class":105},"    base ",[95,407,128],{"class":101},[95,409,410],{"class":105}," base ",[95,412,413],{"class":101},"or",[95,415,416],{"class":105}," Path.cwd()\n",[95,418,420,423],{"class":97,"line":419},16,[95,421,422],{"class":101},"    try",[95,424,393],{"class":105},[95,426,428,431,434],{"class":97,"line":427},17,[95,429,430],{"class":101},"        return",[95,432,433],{"class":156}," str",[95,435,436],{"class":105},"(p.relative_to(base))\n",[95,438,440,443,446],{"class":97,"line":439},18,[95,441,442],{"class":101},"    except",[95,444,445],{"class":156}," ValueError",[95,447,393],{"class":105},[95,449,451],{"class":97,"line":450},19,[95,452,453],{"class":101},"        pass\n",[95,455,457,459],{"class":97,"line":456},20,[95,458,422],{"class":101},[95,460,393],{"class":105},[95,462,464,466,468,471,474,476,478],{"class":97,"line":463},21,[95,465,430],{"class":101},[95,467,433],{"class":156},[95,469,470],{"class":105},"(Path(",[95,472,473],{"class":134},"\"~\"",[95,475,138],{"class":105},[95,477,141],{"class":101},[95,479,480],{"class":105}," p.relative_to(Path.home()))\n",[95,482,484,486,488],{"class":97,"line":483},22,[95,485,442],{"class":101},[95,487,445],{"class":156},[95,489,393],{"class":105},[95,491,493,495,497],{"class":97,"line":492},23,[95,494,430],{"class":101},[95,496,433],{"class":156},[95,498,499],{"class":105},"(p)\n",[95,501,503],{"class":97,"line":502},24,[95,504,119],{"emptyLinePlaceholder":118},[95,506,508],{"class":97,"line":507},25,[95,509,119],{"emptyLinePlaceholder":118},[95,511,513,516],{"class":97,"line":512},26,[95,514,515],{"class":369},"@app.callback",[95,517,518],{"class":105},"()\n",[95,520,522,524,527,530,533],{"class":97,"line":521},27,[95,523,366],{"class":101},[95,525,526],{"class":369}," main",[95,528,529],{"class":105},"() -> ",[95,531,532],{"class":156},"None",[95,534,393],{"class":105},[95,536,538],{"class":97,"line":537},28,[95,539,540],{"class":134},"    \"\"\"Log utilities.\"\"\"\n",[95,542,544],{"class":97,"line":543},29,[95,545,119],{"emptyLinePlaceholder":118},[95,547,549],{"class":97,"line":548},30,[95,550,119],{"emptyLinePlaceholder":118},[95,552,554,557],{"class":97,"line":553},31,[95,555,556],{"class":369},"@app.command",[95,558,518],{"class":105},[95,560,562,564,567],{"class":97,"line":561},32,[95,563,366],{"class":101},[95,565,566],{"class":369}," bundle",[95,568,569],{"class":105},"(\n",[95,571,573,576,578,581,584,586,590,592,595,597,600,602,605,607,610,612,614],{"class":97,"line":572},33,[95,574,575],{"class":105},"    root: Path ",[95,577,128],{"class":101},[95,579,580],{"class":105}," typer.Argument(",[95,582,583],{"class":156},"...",[95,585,25],{"class":105},[95,587,589],{"class":588},"s4XuR","exists",[95,591,128],{"class":101},[95,593,594],{"class":156},"True",[95,596,25],{"class":105},[95,598,599],{"class":588},"file_okay",[95,601,128],{"class":101},[95,603,604],{"class":156},"False",[95,606,25],{"class":105},[95,608,609],{"class":588},"resolve_path",[95,611,128],{"class":101},[95,613,594],{"class":156},[95,615,616],{"class":105},"),\n",[95,618,620,623,625,628,631,634,637,639,642,644,647,649,651],{"class":97,"line":619},34,[95,621,622],{"class":105},"    out: Path ",[95,624,128],{"class":101},[95,626,627],{"class":105}," typer.Option(Path(",[95,629,630],{"class":134},"\"logs.txt.gz\"",[95,632,633],{"class":105},"), ",[95,635,636],{"class":134},"\"--out\"",[95,638,25],{"class":105},[95,640,641],{"class":134},"\"-o\"",[95,643,25],{"class":105},[95,645,646],{"class":588},"dir_okay",[95,648,128],{"class":101},[95,650,604],{"class":156},[95,652,616],{"class":105},[95,654,656,659,661,663,666,669,671,674,676,679],{"class":97,"line":655},35,[95,657,658],{"class":105},"    pattern: ",[95,660,390],{"class":156},[95,662,382],{"class":101},[95,664,665],{"class":105}," typer.Option(",[95,667,668],{"class":134},"\"*.log\"",[95,670,25],{"class":105},[95,672,673],{"class":588},"help",[95,675,128],{"class":101},[95,677,678],{"class":134},"\"Glob, relative to ROOT.\"",[95,680,616],{"class":105},[95,682,684,686,688],{"class":97,"line":683},36,[95,685,387],{"class":105},[95,687,532],{"class":156},[95,689,393],{"class":105},[95,691,693],{"class":97,"line":692},37,[95,694,695],{"class":134},"    \"\"\"Concatenate every file matching PATTERN under ROOT into one gzip file.\"\"\"\n",[95,697,699,702,704],{"class":97,"line":698},38,[95,700,701],{"class":105},"    out ",[95,703,128],{"class":101},[95,705,706],{"class":105}," out.expanduser().resolve()\n",[95,708,710,713,715,718,721,724,727,730,733,736,739,742,744,747],{"class":97,"line":709},39,[95,711,712],{"class":105},"    files ",[95,714,128],{"class":101},[95,716,717],{"class":156}," sorted",[95,719,720],{"class":105},"(p ",[95,722,723],{"class":101},"for",[95,725,726],{"class":105}," p ",[95,728,729],{"class":101},"in",[95,731,732],{"class":105}," root.rglob(pattern) ",[95,734,735],{"class":101},"if",[95,737,738],{"class":105}," p.is_file() ",[95,740,741],{"class":101},"and",[95,743,726],{"class":105},[95,745,746],{"class":101},"!=",[95,748,749],{"class":105}," out)\n",[95,751,753,756,759],{"class":97,"line":752},40,[95,754,755],{"class":101},"    if",[95,757,758],{"class":101}," not",[95,760,761],{"class":105}," files:\n",[95,763,765,768,771,774,777,780,783,786,789,791,794,796,799,801,804,806,808],{"class":97,"line":764},41,[95,766,767],{"class":105},"        typer.echo(",[95,769,770],{"class":101},"f",[95,772,773],{"class":134},"\"no files matching ",[95,775,776],{"class":156},"{",[95,778,779],{"class":105},"pattern",[95,781,782],{"class":101},"!r",[95,784,785],{"class":156},"}",[95,787,788],{"class":134}," under ",[95,790,776],{"class":156},[95,792,793],{"class":105},"display(root)",[95,795,785],{"class":156},[95,797,798],{"class":134},"\"",[95,800,25],{"class":105},[95,802,803],{"class":588},"err",[95,805,128],{"class":101},[95,807,594],{"class":156},[95,809,810],{"class":105},")\n",[95,812,814,817,820,823],{"class":97,"line":813},42,[95,815,816],{"class":101},"        raise",[95,818,819],{"class":105}," typer.Exit(",[95,821,822],{"class":156},"1",[95,824,810],{"class":105},[95,826,828,831,834,836,838,840,843,845,847],{"class":97,"line":827},43,[95,829,830],{"class":105},"    out.parent.mkdir(",[95,832,833],{"class":588},"parents",[95,835,128],{"class":101},[95,837,594],{"class":156},[95,839,25],{"class":105},[95,841,842],{"class":588},"exist_ok",[95,844,128],{"class":101},[95,846,594],{"class":156},[95,848,810],{"class":105},[95,850,852,855,858,861,863,866],{"class":97,"line":851},44,[95,853,854],{"class":101},"    with",[95,856,857],{"class":105}," gzip.open(out, ",[95,859,860],{"class":134},"\"wb\"",[95,862,138],{"class":105},[95,864,865],{"class":101},"as",[95,867,868],{"class":105}," dst:\n",[95,870,872,875,878,880],{"class":97,"line":871},45,[95,873,874],{"class":101},"        for",[95,876,877],{"class":105}," f ",[95,879,729],{"class":101},[95,881,761],{"class":105},[95,883,885,888,890,893,895,898,901,903],{"class":97,"line":884},46,[95,886,887],{"class":105},"            dst.write(",[95,889,770],{"class":101},[95,891,892],{"class":134},"\"# ",[95,894,776],{"class":156},[95,896,897],{"class":105},"f.relative_to(root).as_posix()",[95,899,900],{"class":156},"}\\n",[95,902,798],{"class":134},[95,904,905],{"class":105},".encode())\n",[95,907,909,912,915,918,920,922],{"class":97,"line":908},47,[95,910,911],{"class":101},"            with",[95,913,914],{"class":105}," f.open(",[95,916,917],{"class":134},"\"rb\"",[95,919,138],{"class":105},[95,921,865],{"class":101},[95,923,924],{"class":105}," src:\n",[95,926,928],{"class":97,"line":927},48,[95,929,930],{"class":105},"                shutil.copyfileobj(src, dst)\n",[95,932,934,937,939,942,945,948,950,953,955,958,960,962,964,966,968,970],{"class":97,"line":933},49,[95,935,936],{"class":105},"    typer.echo(",[95,938,770],{"class":101},[95,940,941],{"class":134},"\"bundled ",[95,943,944],{"class":156},"{len",[95,946,947],{"class":105},"(files)",[95,949,785],{"class":156},[95,951,952],{"class":134}," files into ",[95,954,776],{"class":156},[95,956,957],{"class":105},"display(out)",[95,959,785],{"class":156},[95,961,798],{"class":134},[95,963,25],{"class":105},[95,965,803],{"class":588},[95,967,128],{"class":101},[95,969,594],{"class":156},[95,971,810],{"class":105},[95,973,975],{"class":97,"line":974},50,[95,976,119],{"emptyLinePlaceholder":118},[95,978,980],{"class":97,"line":979},51,[95,981,119],{"emptyLinePlaceholder":118},[95,983,985,987,990,993,996],{"class":97,"line":984},52,[95,986,735],{"class":101},[95,988,989],{"class":156}," __name__",[95,991,992],{"class":101}," ==",[95,994,995],{"class":134}," \"__main__\"",[95,997,393],{"class":105},[95,999,1001],{"class":97,"line":1000},53,[95,1002,1003],{"class":105},"    app()\n",[10,1005,1006],{},"Walk through where the conversions happen:",[46,1008,1009,1033,1066,1076],{},[49,1010,1011,1015,1016,1018,1019,1022,1023,1025,1026,1029,1030,1032],{},[1012,1013,1014],"strong",{},"In:"," Typer converts the argument to ",[14,1017,271],{}," because of the annotation, validates that it exists and is a directory, and ",[14,1020,1021],{},"resolve_path=True"," makes it absolute. For options that may contain ",[14,1024,24],{},", call ",[14,1027,1028],{},".expanduser()"," yourself — shells expand an unquoted ",[14,1031,24],{},", but a value from a config file or a quoted argument arrives with the tilde intact.",[49,1034,1035,1038,1039,25,1042,25,1045,25,1048,25,1051,1054,1055,1058,1059,1061,1062,1065],{},[1012,1036,1037],{},"Throughout:"," ",[14,1040,1041],{},"rglob",[14,1043,1044],{},"is_file",[14,1046,1047],{},"relative_to",[14,1049,1050],{},"open",[14,1052,1053],{},"mkdir"," and ",[14,1056,1057],{},"parent"," are all ",[14,1060,271],{}," methods. There is no ",[14,1063,1064],{},"os.path"," import and no string slicing.",[49,1067,1068,1071,1072,1075],{},[1012,1069,1070],{},"Out, for machines:"," the header line uses ",[14,1073,1074],{},".as_posix()",", so the archive's contents are identical whether it was built on Windows or Linux.",[49,1077,1078,1038,1081,1084],{},[1012,1079,1080],{},"Out, for humans:",[14,1082,1083],{},"display()"," shows a path relative to the current directory when possible, otherwise abbreviates the home directory.",[82,1086],{"name":1087},"fs-os-to-pathlib",[1089,1090,1092],"h3",{"id":1091},"traps-that-pathlib-does-not-remove","Traps that pathlib does not remove",[10,1094,1095,1097],{},[14,1096,32],{}," fixes string handling, but a few platform differences are about the filesystem itself:",[46,1099,1100,1113,1142,1148,1169],{},[49,1101,1102,1105,1106,1054,1109,1112],{},[1012,1103,1104],{},"Case sensitivity."," macOS (APFS by default) and Windows are case-insensitive; Linux is not. Two files called ",[14,1107,1108],{},"README.md",[14,1110,1111],{},"readme.md"," can coexist only on Linux. When your tool compares paths, compare resolved paths, and do not assume a lookup by name is case-sensitive.",[49,1114,1115,1118,1119,25,1122,1125,1126,1129,1130,1133,1134,1137,1138,1141],{},[1012,1116,1117],{},"Reserved names and characters."," Windows refuses filenames such as ",[14,1120,1121],{},"CON",[14,1123,1124],{},"NUL"," or ",[14,1127,1128],{},"COM1",", and characters including ",[14,1131,1132],{},"\u003C>:\"|?*",". If your CLI generates filenames from data — titles, URLs, dates with colons — sanitise them. A timestamp like ",[14,1135,1136],{},"2026-09-18T10:00:00"," is a valid filename on Linux and invalid on Windows; use ",[14,1139,1140],{},"2026-09-18T100000"," instead.",[49,1143,1144,1147],{},[1012,1145,1146],{},"Path length."," Older Windows configurations limit paths to 260 characters. Deeply nested output directories can hit it; keep generated structures shallow.",[49,1149,1150,1038,1153,1156,1157,1160,1161,1164,1165,1168],{},[1012,1151,1152],{},"Absolute paths.",[14,1154,1155],{},"Path(\"C:foo\")"," on Windows is relative to the current directory on drive C, and ",[14,1158,1159],{},"Path(\"\u002Ffoo\")"," is relative to the current drive. ",[14,1162,1163],{},"is_absolute()"," handles these correctly; hand-written checks like ",[14,1166,1167],{},"s.startswith(\"\u002F\")"," do not.",[49,1170,1171,1038,1174,1177],{},[1012,1172,1173],{},"Globbing and hidden files.",[14,1175,1176],{},"Path.glob(\"*\")"," includes dot-files on POSIX (unlike the shell). Filter explicitly if you mean to skip them.",[41,1179,1181],{"id":1180},"ux-considerations","UX considerations",[10,1183,1184,1185,1188],{},"How paths ",[258,1186,1187],{},"look"," matters as much as how they are handled. Users scan output for the files they care about, and a 90-character absolute path buries the part they recognise.",[82,1190],{"name":1191},"fs-path-display",[46,1193,1194,1206,1221,1230],{},[49,1195,1196,1199,1200,1202,1203,1205],{},[1012,1197,1198],{},"Show short paths to people."," Relative to the working directory where possible, ",[14,1201,24],{},"-abbreviated otherwise — that is what ",[14,1204,1083],{}," above does.",[49,1207,1208,1211,1212,1215,1216,1220],{},[1012,1209,1210],{},"Emit absolute or POSIX paths to machines."," In ",[14,1213,1214],{},"--json"," output, prefer absolute paths (unambiguous regardless of where the consumer runs) or root-relative POSIX paths (portable across machines). Pick one and document it; ",[35,1217,1219],{"href":1218},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","emitting JSON output for scripting"," covers keeping that contract stable.",[49,1222,1223,1038,1226,1229],{},[1012,1224,1225],{},"Accept both separators on Windows.",[14,1227,1228],{},"Path(\"a\u002Fb\")"," already works on Windows, so users can paste either form. Do not reject forward slashes.",[49,1231,1232,1038,1235,1238,1239,1125,1242,1244],{},[1012,1233,1234],{},"Quote paths in messages when they may contain spaces.",[14,1236,1237],{},"error: cannot read 'My Documents\u002Freport.csv'"," is clearer than the unquoted version, and ",[14,1240,1241],{},"repr()",[14,1243,782],{}," in an f-string does it for you.",[41,1246,1248],{"id":1247},"testing-the-behaviour","Testing the behaviour",[10,1250,1251,1253,1254,1256],{},[14,1252,32],{}," provides ",[1012,1255,260],{}," path classes you can instantiate on any platform, which lets a Linux CI job test Windows path logic without a Windows machine:",[86,1258,1260],{"className":88,"code":1259,"language":90,"meta":91,"style":91},"# tests\u002Ftest_paths.py\nfrom pathlib import Path, PurePosixPath, PureWindowsPath\n\nimport pytest\n\nfrom mytool.cli import display\n\n\n@pytest.mark.parametrize(\"cls\", [PurePosixPath, PureWindowsPath])\ndef test_suffix_logic_is_platform_neutral(cls):\n    p = cls(\"reports\") \u002F \"2026\" \u002F \"q3.tar.gz\"\n    assert p.name == \"q3.tar.gz\"\n    assert p.with_suffix(\".zip\").name == \"q3.tar.zip\"\n\n\ndef test_windows_absolute_rules():\n    assert PureWindowsPath(r\"C:\\data\").is_absolute()\n    assert not PureWindowsPath(\"C:data\").is_absolute()\n    assert not PureWindowsPath(r\"\\data\").is_absolute()\n\n\ndef test_display_prefers_relative(tmp_path, monkeypatch):\n    monkeypatch.chdir(tmp_path)\n    target = tmp_path \u002F \"sub\" \u002F \"x.log\"\n    assert display(target) == str(Path(\"sub\") \u002F \"x.log\")\n\n\ndef test_display_abbreviates_home(tmp_path, monkeypatch):\n    home, elsewhere = tmp_path \u002F \"home\", tmp_path \u002F \"elsewhere\"\n    home.mkdir()\n    elsewhere.mkdir()\n    monkeypatch.setattr(Path, \"home\", classmethod(lambda cls: home))\n    monkeypatch.chdir(elsewhere)   # the target is not under the working directory\n    assert display(home \u002F \".cache\" \u002F \"x\") == str(Path(\"~\") \u002F \".cache\" \u002F \"x\")\n",[14,1261,1262,1267,1278,1282,1289,1293,1305,1309,1313,1327,1337,1361,1374,1391,1395,1399,1409,1436,1449,1469,1473,1477,1487,1492,1512,1537,1541,1545,1554,1576,1581,1586,1607,1615],{"__ignoreMap":91},[95,1263,1264],{"class":97,"line":98},[95,1265,1266],{"class":163},"# tests\u002Ftest_paths.py\n",[95,1268,1269,1271,1273,1275],{"class":97,"line":115},[95,1270,102],{"class":101},[95,1272,106],{"class":105},[95,1274,109],{"class":101},[95,1276,1277],{"class":105}," Path, PurePosixPath, PureWindowsPath\n",[95,1279,1280],{"class":97,"line":122},[95,1281,119],{"emptyLinePlaceholder":118},[95,1283,1284,1286],{"class":97,"line":153},[95,1285,109],{"class":101},[95,1287,1288],{"class":105}," pytest\n",[95,1290,1291],{"class":97,"line":167},[95,1292,119],{"emptyLinePlaceholder":118},[95,1294,1295,1297,1300,1302],{"class":97,"line":178},[95,1296,102],{"class":101},[95,1298,1299],{"class":105}," mytool.cli ",[95,1301,109],{"class":101},[95,1303,1304],{"class":105}," display\n",[95,1306,1307],{"class":97,"line":189},[95,1308,119],{"emptyLinePlaceholder":118},[95,1310,1311],{"class":97,"line":200},[95,1312,119],{"emptyLinePlaceholder":118},[95,1314,1315,1318,1321,1324],{"class":97,"line":211},[95,1316,1317],{"class":369},"@pytest.mark.parametrize",[95,1319,1320],{"class":105},"(",[95,1322,1323],{"class":134},"\"cls\"",[95,1325,1326],{"class":105},", [PurePosixPath, PureWindowsPath])\n",[95,1328,1329,1331,1334],{"class":97,"line":228},[95,1330,366],{"class":101},[95,1332,1333],{"class":369}," test_suffix_logic_is_platform_neutral",[95,1335,1336],{"class":105},"(cls):\n",[95,1338,1339,1342,1344,1347,1349,1351,1353,1355,1357,1359],{"class":97,"line":245},[95,1340,1341],{"class":105},"    p ",[95,1343,128],{"class":101},[95,1345,1346],{"class":156}," cls",[95,1348,1320],{"class":105},[95,1350,135],{"class":134},[95,1352,138],{"class":105},[95,1354,141],{"class":101},[95,1356,144],{"class":134},[95,1358,147],{"class":101},[95,1360,150],{"class":134},[95,1362,1363,1366,1369,1372],{"class":97,"line":358},[95,1364,1365],{"class":101},"    assert",[95,1367,1368],{"class":105}," p.name ",[95,1370,1371],{"class":101},"==",[95,1373,150],{"class":134},[95,1375,1376,1378,1381,1383,1386,1388],{"class":97,"line":363},[95,1377,1365],{"class":101},[95,1379,1380],{"class":105}," p.with_suffix(",[95,1382,219],{"class":134},[95,1384,1385],{"class":105},").name ",[95,1387,1371],{"class":101},[95,1389,1390],{"class":134}," \"q3.tar.zip\"\n",[95,1392,1393],{"class":97,"line":396},[95,1394,119],{"emptyLinePlaceholder":118},[95,1396,1397],{"class":97,"line":402},[95,1398,119],{"emptyLinePlaceholder":118},[95,1400,1401,1403,1406],{"class":97,"line":419},[95,1402,366],{"class":101},[95,1404,1405],{"class":369}," test_windows_absolute_rules",[95,1407,1408],{"class":105},"():\n",[95,1410,1411,1413,1416,1419,1421,1425,1428,1431,1433],{"class":97,"line":427},[95,1412,1365],{"class":101},[95,1414,1415],{"class":105}," PureWindowsPath(",[95,1417,1418],{"class":101},"r",[95,1420,798],{"class":134},[95,1422,1424],{"class":1423},"sA_wV","C:",[95,1426,1427],{"class":156},"\\d",[95,1429,1430],{"class":1423},"ata",[95,1432,798],{"class":134},[95,1434,1435],{"class":105},").is_absolute()\n",[95,1437,1438,1440,1442,1444,1447],{"class":97,"line":439},[95,1439,1365],{"class":101},[95,1441,758],{"class":101},[95,1443,1415],{"class":105},[95,1445,1446],{"class":134},"\"C:data\"",[95,1448,1435],{"class":105},[95,1450,1451,1453,1455,1457,1459,1461,1463,1465,1467],{"class":97,"line":450},[95,1452,1365],{"class":101},[95,1454,758],{"class":101},[95,1456,1415],{"class":105},[95,1458,1418],{"class":101},[95,1460,798],{"class":134},[95,1462,1427],{"class":156},[95,1464,1430],{"class":1423},[95,1466,798],{"class":134},[95,1468,1435],{"class":105},[95,1470,1471],{"class":97,"line":456},[95,1472,119],{"emptyLinePlaceholder":118},[95,1474,1475],{"class":97,"line":463},[95,1476,119],{"emptyLinePlaceholder":118},[95,1478,1479,1481,1484],{"class":97,"line":483},[95,1480,366],{"class":101},[95,1482,1483],{"class":369}," test_display_prefers_relative",[95,1485,1486],{"class":105},"(tmp_path, monkeypatch):\n",[95,1488,1489],{"class":97,"line":492},[95,1490,1491],{"class":105},"    monkeypatch.chdir(tmp_path)\n",[95,1493,1494,1497,1499,1502,1504,1507,1509],{"class":97,"line":502},[95,1495,1496],{"class":105},"    target ",[95,1498,128],{"class":101},[95,1500,1501],{"class":105}," tmp_path ",[95,1503,141],{"class":101},[95,1505,1506],{"class":134}," \"sub\"",[95,1508,147],{"class":101},[95,1510,1511],{"class":134}," \"x.log\"\n",[95,1513,1514,1516,1519,1521,1523,1525,1528,1530,1532,1535],{"class":97,"line":507},[95,1515,1365],{"class":101},[95,1517,1518],{"class":105}," display(target) ",[95,1520,1371],{"class":101},[95,1522,433],{"class":156},[95,1524,470],{"class":105},[95,1526,1527],{"class":134},"\"sub\"",[95,1529,138],{"class":105},[95,1531,141],{"class":101},[95,1533,1534],{"class":134}," \"x.log\"",[95,1536,810],{"class":105},[95,1538,1539],{"class":97,"line":512},[95,1540,119],{"emptyLinePlaceholder":118},[95,1542,1543],{"class":97,"line":521},[95,1544,119],{"emptyLinePlaceholder":118},[95,1546,1547,1549,1552],{"class":97,"line":537},[95,1548,366],{"class":101},[95,1550,1551],{"class":369}," test_display_abbreviates_home",[95,1553,1486],{"class":105},[95,1555,1556,1559,1561,1563,1565,1568,1571,1573],{"class":97,"line":543},[95,1557,1558],{"class":105},"    home, elsewhere ",[95,1560,128],{"class":101},[95,1562,1501],{"class":105},[95,1564,141],{"class":101},[95,1566,1567],{"class":134}," \"home\"",[95,1569,1570],{"class":105},", tmp_path ",[95,1572,141],{"class":101},[95,1574,1575],{"class":134}," \"elsewhere\"\n",[95,1577,1578],{"class":97,"line":548},[95,1579,1580],{"class":105},"    home.mkdir()\n",[95,1582,1583],{"class":97,"line":553},[95,1584,1585],{"class":105},"    elsewhere.mkdir()\n",[95,1587,1588,1591,1594,1596,1599,1601,1604],{"class":97,"line":561},[95,1589,1590],{"class":105},"    monkeypatch.setattr(Path, ",[95,1592,1593],{"class":134},"\"home\"",[95,1595,25],{"class":105},[95,1597,1598],{"class":156},"classmethod",[95,1600,1320],{"class":105},[95,1602,1603],{"class":101},"lambda",[95,1605,1606],{"class":105}," cls: home))\n",[95,1608,1609,1612],{"class":97,"line":572},[95,1610,1611],{"class":105},"    monkeypatch.chdir(elsewhere)   ",[95,1613,1614],{"class":163},"# the target is not under the working directory\n",[95,1616,1617,1619,1622,1624,1627,1629,1632,1634,1636,1638,1640,1642,1644,1646,1648,1650,1652],{"class":97,"line":619},[95,1618,1365],{"class":101},[95,1620,1621],{"class":105}," display(home ",[95,1623,141],{"class":101},[95,1625,1626],{"class":134}," \".cache\"",[95,1628,147],{"class":101},[95,1630,1631],{"class":134}," \"x\"",[95,1633,138],{"class":105},[95,1635,1371],{"class":101},[95,1637,433],{"class":156},[95,1639,470],{"class":105},[95,1641,473],{"class":134},[95,1643,138],{"class":105},[95,1645,141],{"class":101},[95,1647,1626],{"class":134},[95,1649,147],{"class":101},[95,1651,1631],{"class":134},[95,1653,810],{"class":105},[10,1655,1656],{},"For behaviour that depends on the real filesystem — case sensitivity, reserved names, symlinks — run the suite on each operating system in CI. That is the only reliable test, and it catches the rest of the platform differences at the same time.",[41,1658,1660],{"id":1659},"conclusion","Conclusion",[10,1662,1663,1664,1666,1667,1669,1670,1673,1674,1678],{},"Paths are one of the few areas where a small, mechanical discipline removes an entire category of bug reports. Convert to ",[14,1665,271],{}," at the edge, stay with ",[14,1668,271],{}," inside, use ",[14,1671,1672],{},"as_posix()"," for anything another machine will read, show short paths to people, and remember the handful of filesystem differences no API can hide. Pair it with ",[35,1675,1677],{"href":1676},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs"," for your tool's own files and your CLI will behave the same on every laptop your team owns.",[41,1680,1682],{"id":1681},"frequently-asked-questions","Frequently asked questions",[1089,1684,1686,1687,1125,1689,1691],{"id":1685},"should-functions-accept-str-or-path","Should functions accept ",[14,1688,390],{},[14,1690,271],{},"?",[10,1693,1694,1695,1698,1699,1702,1703,1705,1706,1708],{},"Accept ",[14,1696,1697],{},"str | os.PathLike[str]"," in public helpers and convert with ",[14,1700,1701],{},"Path(value)"," on the first line; return ",[14,1704,271],{},". That lets callers pass either, while everything inside works with one type. Inside a single application, annotating parameters as ",[14,1707,271],{}," is simpler and perfectly fine.",[1089,1710,1712,1713,1716],{"id":1711},"is-pathresolve-safe-to-call-on-paths-that-do-not-exist","Is ",[14,1714,1715],{},"Path.resolve()"," safe to call on paths that do not exist?",[10,1718,1719,1720,1723,1724,1727],{},"Yes; since Python 3.6 it resolves as much as exists and appends the rest. Pass ",[14,1721,1722],{},"strict=True"," when you want a ",[14,1725,1726],{},"FileNotFoundError"," for a missing path. Note that it follows symlinks, which you may not want when displaying paths back to the user.",[1089,1729,1731],{"id":1730},"how-do-i-walk-a-directory-tree-efficiently","How do I walk a directory tree efficiently?",[10,1733,1734,1737,1738,1125,1741,1744,1745,1748,1749,1752],{},[14,1735,1736],{},"Path.rglob()"," is convenient for simple patterns. For large trees where you want to prune directories — skipping ",[14,1739,1740],{},".git",[14,1742,1743],{},"node_modules"," — use ",[14,1746,1747],{},"Path.walk()"," (Python 3.12+) or ",[14,1750,1751],{},"os.walk()",", and remove names from the directory list in place to stop descent.",[1089,1754,1756,1757,1691],{"id":1755},"why-did-my-tool-create-a-directory-literally-named","Why did my tool create a directory literally named ",[14,1758,24],{},[10,1760,1761,1762,1764,1765,1768,1769,1771,1772,1774],{},"Tilde expansion is a shell feature, not a filesystem one. When a path reaches Python without passing through an unquoted shell word — from a config file, an environment variable, a quoted argument or a Windows terminal — ",[14,1763,24],{}," is just a character, and ",[14,1766,1767],{},"Path(\"~\u002Fout\").mkdir(parents=True)"," creates a folder called ",[14,1770,24],{}," in the current directory. Call ",[14,1773,1028],{}," on every path that a person might have typed, wherever it came from. It is a no-op for paths without a leading tilde, so applying it unconditionally is safe.",[1089,1776,1778],{"id":1777},"what-about-paths-inside-archives-or-on-remote-storage","What about paths inside archives or on remote storage?",[10,1780,1781,1784,1785,1125,1788,1791,1792,1794],{},[14,1782,1783],{},"zipfile.Path"," gives a pathlib-like interface into zip files, and libraries such as ",[14,1786,1787],{},"fsspec",[14,1789,1790],{},"universal-pathlib"," extend the idea to S3 and other stores. Keep local-path code on ",[14,1793,71],{}," and put remote access behind its own small module.",[41,1796,1798],{"id":1797},"related","Related",[46,1800,1801,1807,1812,1818,1824],{},[49,1802,1803,1804],{},"Up: ",[35,1805,1806],{"href":37},"Filesystem paths and atomic writes",[49,1808,1809],{},[35,1810,1811],{"href":1676},"Storing app data with platformdirs",[49,1813,1814],{},[35,1815,1817],{"href":1816},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","Writing files atomically in Python CLIs",[49,1819,1820],{},[35,1821,1823],{"href":1822},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis\u002F","Validating file and directory paths in CLIs",[49,1825,1826],{},[35,1827,1829],{"href":1828},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis\u002F","Managing virtual environments for cross-platform CLIs",[1831,1832,1833],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":91,"searchDepth":115,"depth":115,"links":1835},[1836,1837,1838,1841,1842,1843,1844,1854],{"id":43,"depth":115,"text":44},{"id":65,"depth":115,"text":66},{"id":264,"depth":115,"text":265,"children":1839},[1840],{"id":1091,"depth":122,"text":1092},{"id":1180,"depth":115,"text":1181},{"id":1247,"depth":115,"text":1248},{"id":1659,"depth":115,"text":1660},{"id":1681,"depth":115,"text":1682,"children":1845},[1846,1848,1850,1851,1853],{"id":1685,"depth":122,"text":1847},"Should functions accept str or Path?",{"id":1711,"depth":122,"text":1849},"Is Path.resolve() safe to call on paths that do not exist?",{"id":1730,"depth":122,"text":1731},{"id":1755,"depth":122,"text":1852},"Why did my tool create a directory literally named ~?",{"id":1777,"depth":122,"text":1778},{"id":1797,"depth":115,"text":1798},"2026-09-18","Use pathlib to make Python CLI path handling work on Windows, macOS and Linux: parsing arguments, joining, globbing, showing paths and writing portable output.","beginner",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib",{"title":5,"description":1856},"cli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002Findex",[32,1865,1866,1867],"filesystem","windows","portability","BYgHZwI0SXMtLPuVdQpLarNsN8LqMYud4t4PRsqYs-0",[1870,1873,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1918,1921,1924,1927,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,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2143,2146,2149,2152,2155,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2194,2197,2200,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],{"path":1871,"title":1872},"\u002Fabout","About Python CLI Toolcraft",{"path":1874,"title":1875},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1877,"title":1878},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1880,"title":1881},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1883,"title":1884},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1886,"title":1887},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1889,"title":1890},"\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":1892,"title":1893},"\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":1895,"title":1896},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1898,"title":1899},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1901,"title":1902},"\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":1904,"title":1905},"\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":1907,"title":1908},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1910,"title":1911},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1913,"title":1914},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1916,"title":1917},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1919,"title":1920},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1922,"title":1923},"\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":1925,"title":1926},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1928,"title":1929},"\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":1931,"title":1932},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1934,"title":1935},"\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":1937,"title":1938},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1943,"title":1944},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1946,"title":1947},"\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":1949,"title":1950},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1952,"title":1953},"\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":1955,"title":1956},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1958,"title":1959},"\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":1961,"title":1962},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1964,"title":1965},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1967,"title":1968},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1970,"title":1971},"\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":1973,"title":1974},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1976,"title":1977},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1979,"title":1980},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1982,"title":1983},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1985,"title":1986},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1988,"title":1989},"\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":1991,"title":1992},"\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":1994,"title":1995},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1997,"title":1998},"\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":2000,"title":2001},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2003,"title":2004},"\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":2006,"title":2007},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2009,"title":2010},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2012,"title":2013},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2015,"title":2016},"\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":2018,"title":2019},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2021,"title":2022},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2024,"title":2025},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2027,"title":2028},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2030,"title":2031},"\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":2033,"title":2034},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2036,"title":2037},"\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":2039,"title":2040},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2042,"title":2043},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2045,"title":2046},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2048,"title":2049},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2051,"title":2052},"\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":2054,"title":2055},"\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":2057,"title":2058},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2060,"title":2061},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2063,"title":2064},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2066,"title":2067},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2069,"title":2070},"\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":1861,"title":5},{"path":2073,"title":2074},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2076,"title":2077},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2079,"title":2080},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2082,"title":2083},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2085,"title":2086},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2088,"title":2089},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2091,"title":2092},"\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":2094,"title":2095},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2097,"title":2098},"\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":2100,"title":2101},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2103,"title":2104},"\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":2106,"title":2107},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2109,"title":2110},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2112,"title":2113},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2115,"title":2116},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2118,"title":2119},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2121,"title":2122},"\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":2124,"title":2125},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2127,"title":2128},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2130,"title":2131},"\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":2133,"title":2134},"\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":2136,"title":2137},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2139,"title":2140},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":141,"title":2142},"Python CLI Toolcraft",{"path":2144,"title":2145},"\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":2147,"title":2148},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2150,"title":2151},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2153,"title":2154},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2156,"title":2157},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2159,"title":2160},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2162,"title":2163},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2165,"title":2166},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2168,"title":2169},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2171,"title":2172},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2174,"title":2175},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2177,"title":2178},"\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":2180,"title":2181},"\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":2183,"title":2184},"\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":2186,"title":2187},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2189,"title":2190},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2192,"title":2193},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2195,"title":2196},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2198,"title":2199},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2201,"title":2202},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2204,"title":2205},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2207,"title":2208},"\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":2210,"title":2211},"\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":2213,"title":2214},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2216,"title":2217},"\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":2219,"title":2220},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2222,"title":2223},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2225,"title":2226},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2228,"title":2229},"\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":2231,"title":2232},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2234,"title":2235},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2237,"title":2238},"\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":2240,"title":2241},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2243,"title":2244},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2246,"title":2247},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2249,"title":2250},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2252,"title":2253},"\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":2255,"title":2256},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2258,"title":2259},"\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":2261,"title":2262},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2264,"title":2265},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2267,"title":2268},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2270,"title":2271},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2273,"title":2274},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2276,"title":2277},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2279,"title":2280},"\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":2282,"title":2283},"\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":2285,"title":2286},"\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":2288,"title":2289},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2291,"title":2292},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2294,"title":2295},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2297,"title":2298},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2300,"title":2301},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2303,"title":2304},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2306,"title":2307},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2309,"title":2310},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2312,"title":2313},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2315,"title":2316},"\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":2318,"title":2319},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2321,"title":2322},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2324,"title":2325},"\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":2327,"title":2328},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2330,"title":2331},"\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":2333,"title":2334},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2336,"title":2337},"\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":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2345,"title":2346},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2348,"title":2349},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2354,"title":2355},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2357,"title":2358},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2363,"title":2364},"\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":2366,"title":2367},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2369,"title":2370},"\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":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2375,"title":2376},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2378,"title":2379},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2381,"title":2382},"\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":2384,"title":2385},"\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":2387,"title":2388},"\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":2390,"title":2391},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2393,"title":2394},"\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":2396,"title":2397},"\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":2399,"title":2400},"\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":2402,"title":2403},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2405,"title":2406},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2408,"title":2409},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2411,"title":2412},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2414,"title":2415},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905049]