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