[{"data":1,"prerenderedAt":2344},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002F":3,"content-directory":1797},{"id":4,"title":5,"body":6,"date":1783,"description":1784,"difficulty":1785,"draft":1786,"extension":1787,"meta":1788,"navigation":160,"path":1789,"seo":1790,"stem":1791,"tags":1792,"updated":1783,"__hash__":1796},"content\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002Findex.md","Bundling Data Files with importlib.resources in CLIs",{"type":7,"value":8,"toc":1763},"minimark",[9,39,44,66,73,77,90,96,100,108,118,121,661,664,698,701,1107,1111,1114,1117,1139,1183,1189,1194,1222,1226,1264,1268,1275,1613,1631,1635,1648,1652,1660,1666,1670,1692,1696,1709,1713,1724,1728,1759],[10,11,12,13,17,18,21,22,25,26,29,30,32,33,38],"p",{},"Most CLIs carry files that are not Python: project templates for an ",[14,15,16],"code",{},"init"," command, a JSON schema for validating config, a default configuration, SQL migrations, shell completion scripts. In the source tree they sit next to the code and ",[14,19,20],{},"Path(__file__).parent \u002F \"templates\""," finds them. After packaging, that approach breaks in several ways — the files were never included in the wheel, or the package runs from a zip file where ",[14,23,24],{},"__file__"," points inside an archive, or a frozen binary lays files out differently. ",[14,27,28],{},"importlib.resources"," is the standard-library API designed for exactly this problem: it finds files that belong to a package, wherever and however that package is installed. This guide shows how to lay out data files, load them with ",[14,31,28],{},", make sure the build includes them, and test that the installed package really contains them. It belongs to the ",[34,35,37],"a",{"href":36},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002F","packaging Python CLIs for distribution topic",".",[40,41,43],"h2",{"id":42},"prerequisites","Prerequisites",[45,46,47,59],"ul",{},[48,49,50,51,54,55,58],"li",{},"Python 3.10+ (the ",[14,52,53],{},"files()"," API used here is available from 3.9 and complete from 3.12; ",[14,56,57],{},"importlib_resources"," backports newer behaviour).",[48,60,61,62,65],{},"A CLI in a ",[14,63,64],{},"src\u002F"," layout with a build backend such as hatchling or uv_build.",[40,67,69,70,72],{"id":68},"why-__file__-is-the-wrong-tool","Why ",[14,71,24],{}," is the wrong tool",[74,75],"inline-diagram",{"name":76},"res-right-wrong",[10,78,79,81,82,85,86,89],{},[14,80,20],{}," assumes the package is a directory of real files on disk. That is true for a normal installation and for your checkout, which is why the approach survives until it meets one of the other ways Python code is run: a zipapp built with shiv or zipapp, where modules live inside an archive; a PyInstaller or Nuitka binary, where the layout is decided by the freezer; or an unusual importer. ",[14,83,84],{},"os.getcwd()","-relative paths are worse still — they depend on where the user happens to be standing. And the old ",[14,87,88],{},"pkg_resources"," API is deprecated and slow to import.",[10,91,92,95],{},[14,93,94],{},"importlib.resources.files(package)"," asks the package's own loader for its resources. Normal packages, zip imports and well-behaved freezers all answer correctly.",[40,97,99],{"id":98},"the-recipe-layout-and-loading","The recipe: layout and loading",[10,101,102,103,107],{},"Put data files ",[104,105,106],"strong",{},"inside"," the importable package, so they travel with it:",[109,110,116],"pre",{"className":111,"code":113,"language":114,"meta":115},[112],"language-text","src\u002Fmytool\u002F\n├── __init__.py\n├── cli.py\n├── resources.py\n├── schema.json\n└── templates\u002F\n    ├── __init__.py          # optional since 3.10; keeps older tooling happy\n    ├── default.toml\n    └── ci.yml.j2\n","text","",[14,117,113],{"__ignoreMap":115},[10,119,120],{},"Then load them through a small module, so the rest of the code never deals with paths:",[109,122,126],{"className":123,"code":124,"language":125,"meta":115,"style":115},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fresources.py\nfrom __future__ import annotations\n\nimport json\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\nfrom importlib.resources import as_file, files\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nif TYPE_CHECKING:                     # the abc module moved in 3.11; only needed for typing\n    from importlib.resources.abc import Traversable\n\nPACKAGE = \"mytool\"\n\n\ndef _root() -> Traversable:\n    return files(PACKAGE)\n\n\ndef template_names() -> list[str]:\n    return sorted(t.name for t in (_root() \u002F \"templates\").iterdir()\n                  if t.is_file() and not t.name.startswith(\"__\"))\n\n\ndef read_template(name: str) -> str:\n    resource = _root() \u002F \"templates\" \u002F name\n    if not resource.is_file():\n        raise FileNotFoundError(f\"no template named {name!r}; available: {', '.join(template_names())}\")\n    return resource.read_text(encoding=\"utf-8\")\n\n\ndef load_schema() -> dict[str, Any]:\n    return json.loads((_root() \u002F \"schema.json\").read_text(encoding=\"utf-8\"))\n\n\n@contextmanager\ndef template_path(name: str) -> Iterator[Path]:\n    \"\"\"A real filesystem path to a template, for tools that insist on one.\"\"\"\n    with as_file(_root() \u002F \"templates\" \u002F name) as path:\n        yield path\n","python",[14,127,128,137,155,162,171,184,197,210,223,239,244,258,272,277,290,295,300,313,327,332,337,354,386,410,415,420,441,463,474,522,541,546,551,567,591,596,601,607,622,628,652],{"__ignoreMap":115},[129,130,133],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"sJ8bj","# src\u002Fmytool\u002Fresources.py\n",[129,138,140,144,148,151],{"class":131,"line":139},2,[129,141,143],{"class":142},"szBVR","from",[129,145,147],{"class":146},"sj4cs"," __future__",[129,149,150],{"class":142}," import",[129,152,154],{"class":153},"sVt8B"," annotations\n",[129,156,158],{"class":131,"line":157},3,[129,159,161],{"emptyLinePlaceholder":160},true,"\n",[129,163,165,168],{"class":131,"line":164},4,[129,166,167],{"class":142},"import",[129,169,170],{"class":153}," json\n",[129,172,174,176,179,181],{"class":131,"line":173},5,[129,175,143],{"class":142},[129,177,178],{"class":153}," collections.abc ",[129,180,167],{"class":142},[129,182,183],{"class":153}," Iterator\n",[129,185,187,189,192,194],{"class":131,"line":186},6,[129,188,143],{"class":142},[129,190,191],{"class":153}," contextlib ",[129,193,167],{"class":142},[129,195,196],{"class":153}," contextmanager\n",[129,198,200,202,205,207],{"class":131,"line":199},7,[129,201,143],{"class":142},[129,203,204],{"class":153}," importlib.resources ",[129,206,167],{"class":142},[129,208,209],{"class":153}," as_file, files\n",[129,211,213,215,218,220],{"class":131,"line":212},8,[129,214,143],{"class":142},[129,216,217],{"class":153}," pathlib ",[129,219,167],{"class":142},[129,221,222],{"class":153}," Path\n",[129,224,226,228,231,233,236],{"class":131,"line":225},9,[129,227,143],{"class":142},[129,229,230],{"class":153}," typing ",[129,232,167],{"class":142},[129,234,235],{"class":146}," TYPE_CHECKING",[129,237,238],{"class":153},", Any\n",[129,240,242],{"class":131,"line":241},10,[129,243,161],{"emptyLinePlaceholder":160},[129,245,247,250,252,255],{"class":131,"line":246},11,[129,248,249],{"class":142},"if",[129,251,235],{"class":146},[129,253,254],{"class":153},":                     ",[129,256,257],{"class":135},"# the abc module moved in 3.11; only needed for typing\n",[129,259,261,264,267,269],{"class":131,"line":260},12,[129,262,263],{"class":142},"    from",[129,265,266],{"class":153}," importlib.resources.abc ",[129,268,167],{"class":142},[129,270,271],{"class":153}," Traversable\n",[129,273,275],{"class":131,"line":274},13,[129,276,161],{"emptyLinePlaceholder":160},[129,278,280,283,286],{"class":131,"line":279},14,[129,281,282],{"class":146},"PACKAGE",[129,284,285],{"class":142}," =",[129,287,289],{"class":288},"sZZnC"," \"mytool\"\n",[129,291,293],{"class":131,"line":292},15,[129,294,161],{"emptyLinePlaceholder":160},[129,296,298],{"class":131,"line":297},16,[129,299,161],{"emptyLinePlaceholder":160},[129,301,303,306,310],{"class":131,"line":302},17,[129,304,305],{"class":142},"def",[129,307,309],{"class":308},"sScJk"," _root",[129,311,312],{"class":153},"() -> Traversable:\n",[129,314,316,319,322,324],{"class":131,"line":315},18,[129,317,318],{"class":142},"    return",[129,320,321],{"class":153}," files(",[129,323,282],{"class":146},[129,325,326],{"class":153},")\n",[129,328,330],{"class":131,"line":329},19,[129,331,161],{"emptyLinePlaceholder":160},[129,333,335],{"class":131,"line":334},20,[129,336,161],{"emptyLinePlaceholder":160},[129,338,340,342,345,348,351],{"class":131,"line":339},21,[129,341,305],{"class":142},[129,343,344],{"class":308}," template_names",[129,346,347],{"class":153},"() -> list[",[129,349,350],{"class":146},"str",[129,352,353],{"class":153},"]:\n",[129,355,357,359,362,365,368,371,374,377,380,383],{"class":131,"line":356},22,[129,358,318],{"class":142},[129,360,361],{"class":146}," sorted",[129,363,364],{"class":153},"(t.name ",[129,366,367],{"class":142},"for",[129,369,370],{"class":153}," t ",[129,372,373],{"class":142},"in",[129,375,376],{"class":153}," (_root() ",[129,378,379],{"class":142},"\u002F",[129,381,382],{"class":288}," \"templates\"",[129,384,385],{"class":153},").iterdir()\n",[129,387,389,392,395,398,401,404,407],{"class":131,"line":388},23,[129,390,391],{"class":142},"                  if",[129,393,394],{"class":153}," t.is_file() ",[129,396,397],{"class":142},"and",[129,399,400],{"class":142}," not",[129,402,403],{"class":153}," t.name.startswith(",[129,405,406],{"class":288},"\"__\"",[129,408,409],{"class":153},"))\n",[129,411,413],{"class":131,"line":412},24,[129,414,161],{"emptyLinePlaceholder":160},[129,416,418],{"class":131,"line":417},25,[129,419,161],{"emptyLinePlaceholder":160},[129,421,423,425,428,431,433,436,438],{"class":131,"line":422},26,[129,424,305],{"class":142},[129,426,427],{"class":308}," read_template",[129,429,430],{"class":153},"(name: ",[129,432,350],{"class":146},[129,434,435],{"class":153},") -> ",[129,437,350],{"class":146},[129,439,440],{"class":153},":\n",[129,442,444,447,450,453,455,457,460],{"class":131,"line":443},27,[129,445,446],{"class":153},"    resource ",[129,448,449],{"class":142},"=",[129,451,452],{"class":153}," _root() ",[129,454,379],{"class":142},[129,456,382],{"class":288},[129,458,459],{"class":142}," \u002F",[129,461,462],{"class":153}," name\n",[129,464,466,469,471],{"class":131,"line":465},28,[129,467,468],{"class":142},"    if",[129,470,400],{"class":142},[129,472,473],{"class":153}," resource.is_file():\n",[129,475,477,480,483,486,489,492,495,498,501,504,507,509,512,515,517,520],{"class":131,"line":476},29,[129,478,479],{"class":142},"        raise",[129,481,482],{"class":146}," FileNotFoundError",[129,484,485],{"class":153},"(",[129,487,488],{"class":142},"f",[129,490,491],{"class":288},"\"no template named ",[129,493,494],{"class":146},"{",[129,496,497],{"class":153},"name",[129,499,500],{"class":142},"!r",[129,502,503],{"class":146},"}",[129,505,506],{"class":288},"; available: ",[129,508,494],{"class":146},[129,510,511],{"class":288},"', '",[129,513,514],{"class":153},".join(template_names())",[129,516,503],{"class":146},[129,518,519],{"class":288},"\"",[129,521,326],{"class":153},[129,523,525,527,530,534,536,539],{"class":131,"line":524},30,[129,526,318],{"class":142},[129,528,529],{"class":153}," resource.read_text(",[129,531,533],{"class":532},"s4XuR","encoding",[129,535,449],{"class":142},[129,537,538],{"class":288},"\"utf-8\"",[129,540,326],{"class":153},[129,542,544],{"class":131,"line":543},31,[129,545,161],{"emptyLinePlaceholder":160},[129,547,549],{"class":131,"line":548},32,[129,550,161],{"emptyLinePlaceholder":160},[129,552,554,556,559,562,564],{"class":131,"line":553},33,[129,555,305],{"class":142},[129,557,558],{"class":308}," load_schema",[129,560,561],{"class":153},"() -> dict[",[129,563,350],{"class":146},[129,565,566],{"class":153},", Any]:\n",[129,568,570,572,575,577,580,583,585,587,589],{"class":131,"line":569},34,[129,571,318],{"class":142},[129,573,574],{"class":153}," json.loads((_root() ",[129,576,379],{"class":142},[129,578,579],{"class":288}," \"schema.json\"",[129,581,582],{"class":153},").read_text(",[129,584,533],{"class":532},[129,586,449],{"class":142},[129,588,538],{"class":288},[129,590,409],{"class":153},[129,592,594],{"class":131,"line":593},35,[129,595,161],{"emptyLinePlaceholder":160},[129,597,599],{"class":131,"line":598},36,[129,600,161],{"emptyLinePlaceholder":160},[129,602,604],{"class":131,"line":603},37,[129,605,606],{"class":308},"@contextmanager\n",[129,608,610,612,615,617,619],{"class":131,"line":609},38,[129,611,305],{"class":142},[129,613,614],{"class":308}," template_path",[129,616,430],{"class":153},[129,618,350],{"class":146},[129,620,621],{"class":153},") -> Iterator[Path]:\n",[129,623,625],{"class":131,"line":624},39,[129,626,627],{"class":288},"    \"\"\"A real filesystem path to a template, for tools that insist on one.\"\"\"\n",[129,629,631,634,637,639,641,643,646,649],{"class":131,"line":630},40,[129,632,633],{"class":142},"    with",[129,635,636],{"class":153}," as_file(_root() ",[129,638,379],{"class":142},[129,640,382],{"class":288},[129,642,459],{"class":142},[129,644,645],{"class":153}," name) ",[129,647,648],{"class":142},"as",[129,650,651],{"class":153}," path:\n",[129,653,655,658],{"class":131,"line":654},41,[129,656,657],{"class":142},"        yield",[129,659,660],{"class":153}," path\n",[74,662],{"name":663},"res-flow",[10,665,666,668,669,672,673,675,676,675,679,675,682,685,686,689,690,693,694,697],{},[14,667,53],{}," returns a ",[14,670,671],{},"Traversable"," — an object with ",[14,674,379],{},", ",[14,677,678],{},"iterdir()",[14,680,681],{},"is_file()",[14,683,684],{},"read_text()"," and ",[14,687,688],{},"read_bytes()",", which behaves like a ",[14,691,692],{},"Path"," without promising to be one. For most uses, reading the contents is all you need. When something outside Python requires a real path — passing a template to an external program, for example — ",[14,695,696],{},"as_file()"," gives you one: for a normal install it is simply the existing file, and for a package inside a zip it extracts to a temporary file and cleans up when the block exits.",[10,699,700],{},"Using it from a command:",[109,702,704],{"className":123,"code":703,"language":125,"meta":115,"style":115},"# src\u002Fmytool\u002Fcli.py\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool import resources\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Project scaffolding.\"\"\"\n\n\n@app.command()\ndef init(\n    template: Annotated[str, typer.Option(help=\"Template to use.\")] = \"default.toml\",\n    dest: Annotated[Path, typer.Option(dir_okay=False)] = Path(\"mytool.toml\"),\n) -> None:\n    \"\"\"Write a starter configuration file.\"\"\"\n    if dest.exists():\n        typer.echo(f\"error: {dest} already exists\", err=True)\n        raise typer.Exit(1)\n    try:\n        dest.write_text(resources.read_template(template), encoding=\"utf-8\")\n    except FileNotFoundError as exc:\n        typer.echo(f\"error: {exc}\", err=True)\n        raise typer.Exit(2)\n    typer.echo(f\"wrote {dest}\", err=True)\n\n\n@app.command(\"templates\")\ndef list_templates() -> None:\n    \"\"\"List bundled templates.\"\"\"\n    for name in resources.template_names():\n        typer.echo(name)\n",[14,705,706,711,721,732,736,743,747,759,763,773,777,781,789,804,809,813,817,824,834,863,889,897,902,909,941,953,960,973,986,1013,1024,1052,1056,1060,1071,1084,1089,1102],{"__ignoreMap":115},[129,707,708],{"class":131,"line":132},[129,709,710],{"class":135},"# src\u002Fmytool\u002Fcli.py\n",[129,712,713,715,717,719],{"class":131,"line":139},[129,714,143],{"class":142},[129,716,217],{"class":153},[129,718,167],{"class":142},[129,720,222],{"class":153},[129,722,723,725,727,729],{"class":131,"line":157},[129,724,143],{"class":142},[129,726,230],{"class":153},[129,728,167],{"class":142},[129,730,731],{"class":153}," Annotated\n",[129,733,734],{"class":131,"line":164},[129,735,161],{"emptyLinePlaceholder":160},[129,737,738,740],{"class":131,"line":173},[129,739,167],{"class":142},[129,741,742],{"class":153}," typer\n",[129,744,745],{"class":131,"line":186},[129,746,161],{"emptyLinePlaceholder":160},[129,748,749,751,754,756],{"class":131,"line":199},[129,750,143],{"class":142},[129,752,753],{"class":153}," mytool ",[129,755,167],{"class":142},[129,757,758],{"class":153}," resources\n",[129,760,761],{"class":131,"line":212},[129,762,161],{"emptyLinePlaceholder":160},[129,764,765,768,770],{"class":131,"line":225},[129,766,767],{"class":153},"app ",[129,769,449],{"class":142},[129,771,772],{"class":153}," typer.Typer()\n",[129,774,775],{"class":131,"line":241},[129,776,161],{"emptyLinePlaceholder":160},[129,778,779],{"class":131,"line":246},[129,780,161],{"emptyLinePlaceholder":160},[129,782,783,786],{"class":131,"line":260},[129,784,785],{"class":308},"@app.callback",[129,787,788],{"class":153},"()\n",[129,790,791,793,796,799,802],{"class":131,"line":274},[129,792,305],{"class":142},[129,794,795],{"class":308}," main",[129,797,798],{"class":153},"() -> ",[129,800,801],{"class":146},"None",[129,803,440],{"class":153},[129,805,806],{"class":131,"line":279},[129,807,808],{"class":288},"    \"\"\"Project scaffolding.\"\"\"\n",[129,810,811],{"class":131,"line":292},[129,812,161],{"emptyLinePlaceholder":160},[129,814,815],{"class":131,"line":297},[129,816,161],{"emptyLinePlaceholder":160},[129,818,819,822],{"class":131,"line":302},[129,820,821],{"class":308},"@app.command",[129,823,788],{"class":153},[129,825,826,828,831],{"class":131,"line":315},[129,827,305],{"class":142},[129,829,830],{"class":308}," init",[129,832,833],{"class":153},"(\n",[129,835,836,839,841,844,847,849,852,855,857,860],{"class":131,"line":329},[129,837,838],{"class":153},"    template: Annotated[",[129,840,350],{"class":146},[129,842,843],{"class":153},", typer.Option(",[129,845,846],{"class":532},"help",[129,848,449],{"class":142},[129,850,851],{"class":288},"\"Template to use.\"",[129,853,854],{"class":153},")] ",[129,856,449],{"class":142},[129,858,859],{"class":288}," \"default.toml\"",[129,861,862],{"class":153},",\n",[129,864,865,868,871,873,876,878,880,883,886],{"class":131,"line":334},[129,866,867],{"class":153},"    dest: Annotated[Path, typer.Option(",[129,869,870],{"class":532},"dir_okay",[129,872,449],{"class":142},[129,874,875],{"class":146},"False",[129,877,854],{"class":153},[129,879,449],{"class":142},[129,881,882],{"class":153}," Path(",[129,884,885],{"class":288},"\"mytool.toml\"",[129,887,888],{"class":153},"),\n",[129,890,891,893,895],{"class":131,"line":339},[129,892,435],{"class":153},[129,894,801],{"class":146},[129,896,440],{"class":153},[129,898,899],{"class":131,"line":356},[129,900,901],{"class":288},"    \"\"\"Write a starter configuration file.\"\"\"\n",[129,903,904,906],{"class":131,"line":388},[129,905,468],{"class":142},[129,907,908],{"class":153}," dest.exists():\n",[129,910,911,914,916,919,921,924,926,929,931,934,936,939],{"class":131,"line":412},[129,912,913],{"class":153},"        typer.echo(",[129,915,488],{"class":142},[129,917,918],{"class":288},"\"error: ",[129,920,494],{"class":146},[129,922,923],{"class":153},"dest",[129,925,503],{"class":146},[129,927,928],{"class":288}," already exists\"",[129,930,675],{"class":153},[129,932,933],{"class":532},"err",[129,935,449],{"class":142},[129,937,938],{"class":146},"True",[129,940,326],{"class":153},[129,942,943,945,948,951],{"class":131,"line":417},[129,944,479],{"class":142},[129,946,947],{"class":153}," typer.Exit(",[129,949,950],{"class":146},"1",[129,952,326],{"class":153},[129,954,955,958],{"class":131,"line":422},[129,956,957],{"class":142},"    try",[129,959,440],{"class":153},[129,961,962,965,967,969,971],{"class":131,"line":443},[129,963,964],{"class":153},"        dest.write_text(resources.read_template(template), ",[129,966,533],{"class":532},[129,968,449],{"class":142},[129,970,538],{"class":288},[129,972,326],{"class":153},[129,974,975,978,980,983],{"class":131,"line":465},[129,976,977],{"class":142},"    except",[129,979,482],{"class":146},[129,981,982],{"class":142}," as",[129,984,985],{"class":153}," exc:\n",[129,987,988,990,992,994,996,999,1001,1003,1005,1007,1009,1011],{"class":131,"line":476},[129,989,913],{"class":153},[129,991,488],{"class":142},[129,993,918],{"class":288},[129,995,494],{"class":146},[129,997,998],{"class":153},"exc",[129,1000,503],{"class":146},[129,1002,519],{"class":288},[129,1004,675],{"class":153},[129,1006,933],{"class":532},[129,1008,449],{"class":142},[129,1010,938],{"class":146},[129,1012,326],{"class":153},[129,1014,1015,1017,1019,1022],{"class":131,"line":524},[129,1016,479],{"class":142},[129,1018,947],{"class":153},[129,1020,1021],{"class":146},"2",[129,1023,326],{"class":153},[129,1025,1026,1029,1031,1034,1036,1038,1040,1042,1044,1046,1048,1050],{"class":131,"line":543},[129,1027,1028],{"class":153},"    typer.echo(",[129,1030,488],{"class":142},[129,1032,1033],{"class":288},"\"wrote ",[129,1035,494],{"class":146},[129,1037,923],{"class":153},[129,1039,503],{"class":146},[129,1041,519],{"class":288},[129,1043,675],{"class":153},[129,1045,933],{"class":532},[129,1047,449],{"class":142},[129,1049,938],{"class":146},[129,1051,326],{"class":153},[129,1053,1054],{"class":131,"line":548},[129,1055,161],{"emptyLinePlaceholder":160},[129,1057,1058],{"class":131,"line":553},[129,1059,161],{"emptyLinePlaceholder":160},[129,1061,1062,1064,1066,1069],{"class":131,"line":569},[129,1063,821],{"class":308},[129,1065,485],{"class":153},[129,1067,1068],{"class":288},"\"templates\"",[129,1070,326],{"class":153},[129,1072,1073,1075,1078,1080,1082],{"class":131,"line":593},[129,1074,305],{"class":142},[129,1076,1077],{"class":308}," list_templates",[129,1079,798],{"class":153},[129,1081,801],{"class":146},[129,1083,440],{"class":153},[129,1085,1086],{"class":131,"line":598},[129,1087,1088],{"class":288},"    \"\"\"List bundled templates.\"\"\"\n",[129,1090,1091,1094,1097,1099],{"class":131,"line":603},[129,1092,1093],{"class":142},"    for",[129,1095,1096],{"class":153}," name ",[129,1098,373],{"class":142},[129,1100,1101],{"class":153}," resources.template_names():\n",[129,1103,1104],{"class":131,"line":609},[129,1105,1106],{"class":153},"        typer.echo(name)\n",[40,1108,1110],{"id":1109},"making-sure-the-build-includes-the-files","Making sure the build includes the files",[10,1112,1113],{},"The loading code is only half the job; the files must also be in the wheel. Build backends differ in what they include by default:",[74,1115],{"name":1116},"res-backend-matrix",[10,1118,1119,1120,685,1123,1126,1127,1130,1131,1134,1135,1138],{},"With ",[104,1121,1122],{},"hatchling",[104,1124,1125],{},"uv_build",", files inside the package directory are included in the wheel by default, so the layout above just works. With ",[104,1128,1129],{},"setuptools",", package data inclusion depends on configuration (",[14,1132,1133],{},"[tool.setuptools.package-data]",") and whether files are tracked by version control with a plugin — the source of many \"worked locally, missing in the wheel\" bugs. Whatever the backend, ",[104,1136,1137],{},"verify rather than trust",":",[109,1140,1144],{"className":1141,"code":1142,"language":1143,"meta":115,"style":115},"language-bash shiki shiki-themes github-light github-dark","uv build\nunzip -l dist\u002Fmytool-*.whl | grep -E 'templates\u002F|schema.json'\n","bash",[14,1145,1146,1154],{"__ignoreMap":115},[129,1147,1148,1151],{"class":131,"line":132},[129,1149,1150],{"class":308},"uv",[129,1152,1153],{"class":288}," build\n",[129,1155,1156,1159,1162,1165,1168,1171,1174,1177,1180],{"class":131,"line":139},[129,1157,1158],{"class":308},"unzip",[129,1160,1161],{"class":146}," -l",[129,1163,1164],{"class":288}," dist\u002Fmytool-",[129,1166,1167],{"class":146},"*",[129,1169,1170],{"class":288},".whl",[129,1172,1173],{"class":142}," |",[129,1175,1176],{"class":308}," grep",[129,1178,1179],{"class":146}," -E",[129,1181,1182],{"class":288}," 'templates\u002F|schema.json'\n",[10,1184,1185,1186,38],{},"If you need to exclude files — test fixtures, editor backups — use the backend's exclude settings, such as ",[14,1187,1188],{},"[tool.hatch.build.targets.wheel] exclude = [\"**\u002F*.orig\"]",[1190,1191,1193],"h3",{"id":1192},"resources-that-change-with-the-tool","Resources that change with the tool",[10,1195,1196,1197,1200,1201,1204,1205,1208,1209,1212,1213,1217,1218,1221],{},"Bundled files are versioned with the code, which is usually exactly what you want: the schema that validates a config matches the code that reads it, and a template generated by version 2.4 is the one version 2.4 was tested with. Two practices keep that property useful. ",[104,1198,1199],{},"Stamp generated output with the tool version"," — a comment such as ",[14,1202,1203],{},"# generated by mytool 2.4.0"," at the top of a file written from a template — so that when a user reports a problem with a generated file, you know which template produced it. And ",[104,1206,1207],{},"treat bundled schemas as part of the public contract",": if users validate their own files against ",[14,1210,1211],{},"mytool schema --print",", changing the schema is a compatibility question, handled under the policy in ",[34,1214,1216],{"href":1215},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools\u002F","semantic versioning policy for CLI tools",". A small ",[14,1219,1220],{},"schema"," command that prints the bundled schema also lets editors and CI pipelines validate configuration without installing anything beyond the CLI itself.",[40,1223,1225],{"id":1224},"ux-considerations","UX considerations",[45,1227,1228,1242,1252,1258],{},[48,1229,1230,1233,1234,1237,1238,1241],{},[104,1231,1232],{},"List what is available."," A ",[14,1235,1236],{},"templates"," command (or listing names in an error message, as ",[14,1239,1240],{},"read_template"," does) turns \"no such template\" from a dead end into a choice.",[48,1243,1244,1247,1248,38],{},[104,1245,1246],{},"Let users override bundled defaults."," A common pattern is to look for a user template in the config directory first and fall back to the bundled one, so teams can customise without forking — see ",[34,1249,1251],{"href":1250},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs",[48,1253,1254,1257],{},[104,1255,1256],{},"Never write into the package."," Installed packages may be read-only, shared between users, or inside a zip. Generated or cached data belongs in the user's cache or state directory.",[48,1259,1260,1263],{},[104,1261,1262],{},"Keep resources small."," Everything bundled is downloaded by every user. Large datasets are better fetched on first use and cached.",[40,1265,1267],{"id":1266},"testing-the-behaviour","Testing the behaviour",[10,1269,1270,1271,1274],{},"Unit tests confirm the loader works from the source tree; a check against the ",[104,1272,1273],{},"built"," wheel confirms the files were packaged. The second is the one that catches real release bugs:",[109,1276,1278],{"className":123,"code":1277,"language":125,"meta":115,"style":115},"# tests\u002Ftest_resources.py\nimport zipfile\nfrom pathlib import Path\n\nimport pytest\n\nfrom mytool import resources\n\n\ndef test_templates_are_listed():\n    assert \"default.toml\" in resources.template_names()\n\n\ndef test_unknown_template_lists_alternatives():\n    with pytest.raises(FileNotFoundError, match=\"available: .*default.toml\"):\n        resources.read_template(\"nope.toml\")\n\n\ndef test_schema_loads():\n    assert resources.load_schema()[\"type\"] == \"object\"\n\n\ndef test_template_path_is_a_real_file():\n    with resources.template_path(\"default.toml\") as p:\n        assert p.is_file()\n\n\n@pytest.mark.slow\ndef test_wheel_contains_resources(tmp_path: Path):\n    import subprocess\n    subprocess.run([\"uv\", \"build\", \"--wheel\", \"--out-dir\", str(tmp_path)], check=True,\n                   capture_output=True)\n    names = zipfile.ZipFile(next(tmp_path.glob(\"*.whl\"))).namelist()\n    assert \"mytool\u002Fschema.json\" in names\n    assert \"mytool\u002Ftemplates\u002Fdefault.toml\" in names\n",[14,1279,1280,1285,1292,1302,1306,1313,1317,1327,1331,1335,1345,1358,1362,1366,1375,1398,1408,1412,1416,1425,1444,1448,1452,1461,1479,1487,1491,1495,1500,1510,1518,1557,1568,1590,1602],{"__ignoreMap":115},[129,1281,1282],{"class":131,"line":132},[129,1283,1284],{"class":135},"# tests\u002Ftest_resources.py\n",[129,1286,1287,1289],{"class":131,"line":139},[129,1288,167],{"class":142},[129,1290,1291],{"class":153}," zipfile\n",[129,1293,1294,1296,1298,1300],{"class":131,"line":157},[129,1295,143],{"class":142},[129,1297,217],{"class":153},[129,1299,167],{"class":142},[129,1301,222],{"class":153},[129,1303,1304],{"class":131,"line":164},[129,1305,161],{"emptyLinePlaceholder":160},[129,1307,1308,1310],{"class":131,"line":173},[129,1309,167],{"class":142},[129,1311,1312],{"class":153}," pytest\n",[129,1314,1315],{"class":131,"line":186},[129,1316,161],{"emptyLinePlaceholder":160},[129,1318,1319,1321,1323,1325],{"class":131,"line":199},[129,1320,143],{"class":142},[129,1322,753],{"class":153},[129,1324,167],{"class":142},[129,1326,758],{"class":153},[129,1328,1329],{"class":131,"line":212},[129,1330,161],{"emptyLinePlaceholder":160},[129,1332,1333],{"class":131,"line":225},[129,1334,161],{"emptyLinePlaceholder":160},[129,1336,1337,1339,1342],{"class":131,"line":241},[129,1338,305],{"class":142},[129,1340,1341],{"class":308}," test_templates_are_listed",[129,1343,1344],{"class":153},"():\n",[129,1346,1347,1350,1352,1355],{"class":131,"line":246},[129,1348,1349],{"class":142},"    assert",[129,1351,859],{"class":288},[129,1353,1354],{"class":142}," in",[129,1356,1357],{"class":153}," resources.template_names()\n",[129,1359,1360],{"class":131,"line":260},[129,1361,161],{"emptyLinePlaceholder":160},[129,1363,1364],{"class":131,"line":274},[129,1365,161],{"emptyLinePlaceholder":160},[129,1367,1368,1370,1373],{"class":131,"line":279},[129,1369,305],{"class":142},[129,1371,1372],{"class":308}," test_unknown_template_lists_alternatives",[129,1374,1344],{"class":153},[129,1376,1377,1379,1382,1385,1387,1390,1392,1395],{"class":131,"line":292},[129,1378,633],{"class":142},[129,1380,1381],{"class":153}," pytest.raises(",[129,1383,1384],{"class":146},"FileNotFoundError",[129,1386,675],{"class":153},[129,1388,1389],{"class":532},"match",[129,1391,449],{"class":142},[129,1393,1394],{"class":288},"\"available: .*default.toml\"",[129,1396,1397],{"class":153},"):\n",[129,1399,1400,1403,1406],{"class":131,"line":297},[129,1401,1402],{"class":153},"        resources.read_template(",[129,1404,1405],{"class":288},"\"nope.toml\"",[129,1407,326],{"class":153},[129,1409,1410],{"class":131,"line":302},[129,1411,161],{"emptyLinePlaceholder":160},[129,1413,1414],{"class":131,"line":315},[129,1415,161],{"emptyLinePlaceholder":160},[129,1417,1418,1420,1423],{"class":131,"line":329},[129,1419,305],{"class":142},[129,1421,1422],{"class":308}," test_schema_loads",[129,1424,1344],{"class":153},[129,1426,1427,1429,1432,1435,1438,1441],{"class":131,"line":334},[129,1428,1349],{"class":142},[129,1430,1431],{"class":153}," resources.load_schema()[",[129,1433,1434],{"class":288},"\"type\"",[129,1436,1437],{"class":153},"] ",[129,1439,1440],{"class":142},"==",[129,1442,1443],{"class":288}," \"object\"\n",[129,1445,1446],{"class":131,"line":339},[129,1447,161],{"emptyLinePlaceholder":160},[129,1449,1450],{"class":131,"line":356},[129,1451,161],{"emptyLinePlaceholder":160},[129,1453,1454,1456,1459],{"class":131,"line":388},[129,1455,305],{"class":142},[129,1457,1458],{"class":308}," test_template_path_is_a_real_file",[129,1460,1344],{"class":153},[129,1462,1463,1465,1468,1471,1474,1476],{"class":131,"line":412},[129,1464,633],{"class":142},[129,1466,1467],{"class":153}," resources.template_path(",[129,1469,1470],{"class":288},"\"default.toml\"",[129,1472,1473],{"class":153},") ",[129,1475,648],{"class":142},[129,1477,1478],{"class":153}," p:\n",[129,1480,1481,1484],{"class":131,"line":417},[129,1482,1483],{"class":142},"        assert",[129,1485,1486],{"class":153}," p.is_file()\n",[129,1488,1489],{"class":131,"line":422},[129,1490,161],{"emptyLinePlaceholder":160},[129,1492,1493],{"class":131,"line":443},[129,1494,161],{"emptyLinePlaceholder":160},[129,1496,1497],{"class":131,"line":465},[129,1498,1499],{"class":308},"@pytest.mark.slow\n",[129,1501,1502,1504,1507],{"class":131,"line":476},[129,1503,305],{"class":142},[129,1505,1506],{"class":308}," test_wheel_contains_resources",[129,1508,1509],{"class":153},"(tmp_path: Path):\n",[129,1511,1512,1515],{"class":131,"line":524},[129,1513,1514],{"class":142},"    import",[129,1516,1517],{"class":153}," subprocess\n",[129,1519,1520,1523,1526,1528,1531,1533,1536,1538,1541,1543,1545,1548,1551,1553,1555],{"class":131,"line":543},[129,1521,1522],{"class":153},"    subprocess.run([",[129,1524,1525],{"class":288},"\"uv\"",[129,1527,675],{"class":153},[129,1529,1530],{"class":288},"\"build\"",[129,1532,675],{"class":153},[129,1534,1535],{"class":288},"\"--wheel\"",[129,1537,675],{"class":153},[129,1539,1540],{"class":288},"\"--out-dir\"",[129,1542,675],{"class":153},[129,1544,350],{"class":146},[129,1546,1547],{"class":153},"(tmp_path)], ",[129,1549,1550],{"class":532},"check",[129,1552,449],{"class":142},[129,1554,938],{"class":146},[129,1556,862],{"class":153},[129,1558,1559,1562,1564,1566],{"class":131,"line":548},[129,1560,1561],{"class":532},"                   capture_output",[129,1563,449],{"class":142},[129,1565,938],{"class":146},[129,1567,326],{"class":153},[129,1569,1570,1573,1575,1578,1581,1584,1587],{"class":131,"line":553},[129,1571,1572],{"class":153},"    names ",[129,1574,449],{"class":142},[129,1576,1577],{"class":153}," zipfile.ZipFile(",[129,1579,1580],{"class":146},"next",[129,1582,1583],{"class":153},"(tmp_path.glob(",[129,1585,1586],{"class":288},"\"*.whl\"",[129,1588,1589],{"class":153},"))).namelist()\n",[129,1591,1592,1594,1597,1599],{"class":131,"line":569},[129,1593,1349],{"class":142},[129,1595,1596],{"class":288}," \"mytool\u002Fschema.json\"",[129,1598,1354],{"class":142},[129,1600,1601],{"class":153}," names\n",[129,1603,1604,1606,1609,1611],{"class":131,"line":593},[129,1605,1349],{"class":142},[129,1607,1608],{"class":288}," \"mytool\u002Ftemplates\u002Fdefault.toml\"",[129,1610,1354],{"class":142},[129,1612,1601],{"class":153},[10,1614,1615,1616,1619,1620,1623,1624,1626,1627,38],{},"To prove the loader works from a zip as well, add the built wheel (which is a zip) to ",[14,1617,1618],{},"sys.path"," in a subprocess and call ",[14,1621,1622],{},"template_names()"," — zipimport will load the package straight from the archive, exactly the situation ",[14,1625,24],{},"-based code fails in. The general artefact check is in ",[34,1628,1630],{"href":1629},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI",[40,1632,1634],{"id":1633},"conclusion","Conclusion",[10,1636,1637,1638,1641,1642,1644,1645,1647],{},"Data files belong inside your package, loaded through ",[14,1639,1640],{},"importlib.resources.files()"," rather than ",[14,1643,24],{}," or the working directory, with ",[14,1646,696],{}," for the rare case an external program needs a real path. Wrap access in one small module, list available resources in errors, never write into the package, and verify with a test against the built wheel that the files are actually there. The same code then works from a checkout, a wheel, a zipapp and a frozen binary.",[40,1649,1651],{"id":1650},"frequently-asked-questions","Frequently asked questions",[1190,1653,1655,1656,1659],{"id":1654},"do-resource-directories-need-an-__init__py","Do resource directories need an ",[14,1657,1658],{},"__init__.py","?",[10,1661,1662,1663,1665],{},"Not for ",[14,1664,53],{}," on Python 3.10+, which can traverse plain subdirectories of a package. Adding one does no harm and helps some older build tools include the directory.",[1190,1667,1669],{"id":1668},"how-do-pyinstaller-and-nuitka-handle-resources","How do PyInstaller and Nuitka handle resources?",[10,1671,1672,1673,1676,1677,1680,1681,1684,1685,1687,1688,38],{},"Both need to be told to include data files (",[14,1674,1675],{},"--add-data"," or ",[14,1678,1679],{},"--collect-data mytool"," for PyInstaller, ",[14,1682,1683],{},"--include-package-data=mytool"," for Nuitka). Once included, ",[14,1686,28],{}," finds them. See ",[34,1689,1691],{"href":1690},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F","bundling a Python CLI with PyInstaller",[1190,1693,1695],{"id":1694},"what-about-jinja-templates","What about Jinja templates?",[10,1697,1698,1699,1702,1703,1706,1707,38],{},"Jinja's ",[14,1700,1701],{},"PackageLoader(\"mytool\", \"templates\")"," uses the same resource machinery and works in wheels and zips. Prefer it over ",[14,1704,1705],{},"FileSystemLoader"," pointed at a path computed from ",[14,1708,24],{},[1190,1710,1712],{"id":1711},"can-i-ship-shell-completion-scripts-this-way","Can I ship shell completion scripts this way?",[10,1714,1715,1716,1719,1720,38],{},"Yes — bundle pre-generated completion scripts as resources and add a ",[14,1717,1718],{},"completion install"," command that copies them to the right place for each shell, as covered in ",[34,1721,1723],{"href":1722},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish\u002F","installing shell completion for bash, zsh and fish",[40,1725,1727],{"id":1726},"related","Related",[45,1729,1730,1736,1742,1748,1754],{},[48,1731,1732,1733],{},"Up: ",[34,1734,1735],{"href":36},"Packaging Python CLIs for distribution",[48,1737,1738],{},[34,1739,1741],{"href":1740},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli\u002F","Writing pyproject.toml metadata for a CLI",[48,1743,1744],{},[34,1745,1747],{"href":1746},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis\u002F","Building wheels and sdists for Python CLIs",[48,1749,1750],{},[34,1751,1753],{"href":1752},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002F","Shipping a CLI as a zipapp with shiv",[48,1755,1756],{},[34,1757,1758],{"href":1629},"Smoke-testing the built wheel in CI",[1760,1761,1762],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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":115,"searchDepth":139,"depth":139,"links":1764},[1765,1766,1768,1769,1772,1773,1774,1775,1782],{"id":42,"depth":139,"text":43},{"id":68,"depth":139,"text":1767},"Why __file__ is the wrong tool",{"id":98,"depth":139,"text":99},{"id":1109,"depth":139,"text":1110,"children":1770},[1771],{"id":1192,"depth":157,"text":1193},{"id":1224,"depth":139,"text":1225},{"id":1266,"depth":139,"text":1267},{"id":1633,"depth":139,"text":1634},{"id":1650,"depth":139,"text":1651,"children":1776},[1777,1779,1780,1781],{"id":1654,"depth":157,"text":1778},"Do resource directories need an __init__.py?",{"id":1668,"depth":157,"text":1669},{"id":1694,"depth":157,"text":1695},{"id":1711,"depth":157,"text":1712},{"id":1726,"depth":139,"text":1727},"2026-09-18","Ship templates, schemas and default configs inside a Python CLI package and load them reliably with importlib.resources, in wheels, zipapps and frozen binaries.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources",{"title":5,"description":1784},"project-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002Findex",[1793,1794,1795,1236],"packaging","importlib-resources","data-files","_h_QGrumSrNF0nOUWca8fo4tE9ySkjeBGG-uPf-wmzs",[1798,1801,1804,1807,1810,1813,1816,1819,1822,1825,1828,1831,1834,1837,1840,1843,1846,1849,1852,1855,1858,1861,1864,1867,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,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2281,2284,2287,2290,2293,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341],{"path":1799,"title":1800},"\u002Fabout","About Python CLI Toolcraft",{"path":1802,"title":1803},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1805,"title":1806},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1808,"title":1809},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1811,"title":1812},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1814,"title":1815},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1817,"title":1818},"\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":1820,"title":1821},"\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":1823,"title":1824},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1826,"title":1827},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1829,"title":1830},"\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":1832,"title":1833},"\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":1835,"title":1836},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1838,"title":1839},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1841,"title":1842},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1844,"title":1845},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1847,"title":1848},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1850,"title":1851},"\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":1853,"title":1854},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1856,"title":1857},"\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":1859,"title":1860},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1862,"title":1863},"\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":1865,"title":1866},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1868,"title":1869},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1871,"title":1872},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1874,"title":1875},"\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":1877,"title":1878},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1880,"title":1881},"\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":1883,"title":1884},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1886,"title":1887},"\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":1889,"title":1890},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1892,"title":1893},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1895,"title":1896},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1898,"title":1899},"\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":1901,"title":1902},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1904,"title":1905},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1907,"title":1908},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1910,"title":1911},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1913,"title":1914},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1916,"title":1917},"\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":1919,"title":1920},"\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":1922,"title":1923},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1925,"title":1926},"\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":1928,"title":1929},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1931,"title":1932},"\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":1934,"title":1935},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1937,"title":1938},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1940,"title":1941},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1943,"title":1944},"\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":1946,"title":1947},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1949,"title":1950},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1952,"title":1953},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1955,"title":1956},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1958,"title":1959},"\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":1961,"title":1962},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1964,"title":1965},"\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":1967,"title":1968},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":1970,"title":1971},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":1973,"title":1974},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":1976,"title":1977},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":1979,"title":1980},"\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":1982,"title":1983},"\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":1985,"title":1986},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":1988,"title":1989},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":1991,"title":1992},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":1994,"title":1995},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":1997,"title":1998},"\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":2000,"title":2001},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2003,"title":2004},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2006,"title":2007},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2009,"title":2010},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2012,"title":2013},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2015,"title":2016},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2018,"title":2019},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2021,"title":2022},"\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":2024,"title":2025},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2027,"title":2028},"\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":2030,"title":2031},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2033,"title":2034},"\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":2036,"title":2037},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2039,"title":2040},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2042,"title":2043},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2045,"title":2046},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2048,"title":2049},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2051,"title":2052},"\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":2054,"title":2055},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2057,"title":2058},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2060,"title":2061},"\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":2063,"title":2064},"\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":2066,"title":2067},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2069,"title":2070},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":379,"title":2072},"Python CLI Toolcraft",{"path":2074,"title":2075},"\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":2077,"title":2078},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2080,"title":2081},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2083,"title":2084},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2086,"title":2087},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2089,"title":2090},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2092,"title":2093},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2095,"title":2096},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2098,"title":2099},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2101,"title":2102},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2104,"title":2105},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2107,"title":2108},"\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":2110,"title":2111},"\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":2113,"title":2114},"\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":2116,"title":2117},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2119,"title":2120},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2122,"title":2123},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2125,"title":2126},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2128,"title":2129},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2131,"title":2132},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2134,"title":2135},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2137,"title":2138},"\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":2140,"title":2141},"\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":2143,"title":2144},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2146,"title":2147},"\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":2149,"title":2150},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2152,"title":2153},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2155,"title":2156},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2158,"title":2159},"\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":2161,"title":2162},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2164,"title":2165},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2167,"title":2168},"\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":2170,"title":2171},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2173,"title":2174},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2176,"title":2177},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2179,"title":2180},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2182,"title":2183},"\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":2185,"title":2186},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2188,"title":2189},"\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":2191,"title":2192},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2194,"title":2195},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2197,"title":2198},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2200,"title":2201},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2203,"title":2204},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2206,"title":2207},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2209,"title":2210},"\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":2212,"title":2213},"\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":2215,"title":2216},"\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":2218,"title":2219},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2221,"title":2222},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2224,"title":2225},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2227,"title":2228},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2230,"title":2231},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2233,"title":2234},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2236,"title":2237},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2239,"title":2240},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2242,"title":2243},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2245,"title":2246},"\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":2248,"title":2249},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2251,"title":2252},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2254,"title":2255},"\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":2257,"title":2258},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2260,"title":2261},"\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":2263,"title":2264},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2266,"title":2267},"\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":2269,"title":2270},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2272,"title":2273},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2275,"title":2276},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2278,"title":2279},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1789,"title":5},{"path":2282,"title":2283},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2285,"title":2286},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2288,"title":2289},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2291,"title":2292},"\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":2294,"title":2295},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2297,"title":2298},"\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":2300,"title":2301},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2303,"title":2304},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2306,"title":2307},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2309,"title":2310},"\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":2312,"title":2313},"\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":2315,"title":2316},"\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":2318,"title":2319},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2321,"title":2322},"\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":2324,"title":2325},"\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":2327,"title":2328},"\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":2330,"title":2331},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2333,"title":2334},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907503]