[{"data":1,"prerenderedAt":3894},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema\u002F":3,"content-directory":3051},{"id":4,"title":5,"body":6,"date":3036,"description":3037,"difficulty":3038,"draft":3039,"extension":3040,"meta":3041,"navigation":642,"path":3042,"seo":3043,"stem":3044,"tags":3045,"updated":3036,"__hash__":3050},"content\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema\u002Findex.md","Validating Config Files with JSON Schema in a Python CLI",{"type":7,"value":8,"toc":3014},"minimark",[9,53,58,76,80,84,100,103,107,112,119,559,606,610,1415,1435,1443,1454,1458,1775,1787,1790,1794,1797,1835,1859,1863,1875,1878,1882,1937,1941,1944,2592,2879,2882,2886,2908,2912,2916,2926,2930,2937,2941,2947,2951,2962,2966,2976,2980,3010],[10,11,12,13,17,18,21,22,26,27,30,31,34,35,38,39,42,43,46,47,52],"p",{},"Configuration errors are the most frustrating kind a CLI can produce, because the user did not type them a second ago — they are in a file written weeks earlier, maybe copied from a colleague. A loader that raises ",[14,15,16],"code",{},"KeyError: 'url'"," or silently ignores ",[14,19,20],{},"timout = 5"," leaves the user guessing. A good configuration check reports ",[23,24,25],"strong",{},"every"," problem in one pass, points at the exact key (",[14,28,29],{},"api.timeout","), says what was expected, and catches misspelt keys instead of ignoring them. ",[23,32,33],{},"JSON Schema"," is a standard way to express exactly those rules, and it brings a bonus no hand-written check can: the same schema file gives users autocompletion, hover documentation and inline errors in VS Code, JetBrains IDEs and any editor using the Taplo TOML language server. This guide writes a schema for a TOML config, validates it with the ",[14,36,37],{},"jsonschema"," library, turns raw validation errors into messages users understand, adds ",[14,40,41],{},"config validate"," and ",[14,44,45],{},"config schema"," commands, and tests the lot. It belongs to the ",[48,49,51],"a",{"href":50},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002F","configuration files and environment variables topic",".",[54,55,57],"h2",{"id":56},"prerequisites","Prerequisites",[59,60,61,68],"ul",{},[62,63,64,67],"li",{},[14,65,66],{},"uv add jsonschema typer"," (examples checked with jsonschema 4.26, Typer 0.27 and Python 3.13).",[62,69,70,71,75],{},"A CLI that loads TOML, as in ",[48,72,74],{"href":73},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib\u002F","reading TOML config with tomllib",". YAML works the same way once parsed.",[54,77,79],{"id":78},"why-a-schema-and-not-just-code","Why a schema, and not just code",[81,82],"inline-diagram",{"name":83},"jschema-one-source",[10,85,86,87,91,92,95,96,99],{},"JSON Schema validates ",[88,89,90],"em",{},"data",", not JSON text — any parsed TOML, YAML or JSON document is a tree of dictionaries, lists, strings and numbers, which is exactly what a schema describes. Writing the rules as data rather than ",[14,93,94],{},"if"," statements has three effects. The rules are complete and declarative, so “all problems at once” comes for free. They are portable: editors, CI linters such as ",[14,97,98],{},"check-jsonschema",", and documentation generators can all read the same file. And they are reviewable: a pull request that adds a setting shows the new key, its type, its range and its description in one place.",[10,101,102],{},"If your CLI already uses a typed settings model, you may not need to write the schema by hand — see the section on pydantic below. The validation and reporting code is the same either way.",[54,104,106],{"id":105},"the-recipe","The recipe",[108,109,111],"h3",{"id":110},"the-schema","The schema",[10,113,114,115,118],{},"The schema lives inside the package as ",[14,116,117],{},"src\u002Fmytool\u002Fconfig.schema.json",", next to the code that uses it, so it ships in the wheel:",[120,121,126],"pre",{"className":122,"code":123,"language":124,"meta":125,"style":125},"language-json shiki shiki-themes github-light github-dark","{\n  \"$schema\": \"https:\u002F\u002Fjson-schema.org\u002Fdraft\u002F2020-12\u002Fschema\",\n  \"$id\": \"https:\u002F\u002Fexample.com\u002Fmytool\u002Fconfig.schema.json\",\n  \"title\": \"mytool configuration\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"api\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"url\": {\"type\": \"string\", \"format\": \"uri\", \"pattern\": \"^https?:\u002F\u002F\"},\n        \"timeout\": {\"type\": \"number\", \"exclusiveMinimum\": 0, \"maximum\": 600}\n      },\n      \"required\": [\"url\"]\n    },\n    \"output\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"format\": {\"enum\": [\"table\", \"json\", \"csv\"]},\n        \"color\": {\"type\": \"boolean\"}\n      }\n    },\n    \"profiles\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"properties\": {\"region\": {\"type\": \"string\", \"minLength\": 1}},\n        \"required\": [\"region\"]\n      }\n    }\n  }\n}\n","json","",[14,127,128,137,154,167,180,193,206,215,223,235,247,255,296,334,340,355,361,369,380,391,398,427,444,450,455,463,474,481,493,525,537,542,548,554],{"__ignoreMap":125},[129,130,133],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"sVt8B","{\n",[129,138,140,144,147,151],{"class":131,"line":139},2,[129,141,143],{"class":142},"sj4cs","  \"$schema\"",[129,145,146],{"class":135},": ",[129,148,150],{"class":149},"sZZnC","\"https:\u002F\u002Fjson-schema.org\u002Fdraft\u002F2020-12\u002Fschema\"",[129,152,153],{"class":135},",\n",[129,155,157,160,162,165],{"class":131,"line":156},3,[129,158,159],{"class":142},"  \"$id\"",[129,161,146],{"class":135},[129,163,164],{"class":149},"\"https:\u002F\u002Fexample.com\u002Fmytool\u002Fconfig.schema.json\"",[129,166,153],{"class":135},[129,168,170,173,175,178],{"class":131,"line":169},4,[129,171,172],{"class":142},"  \"title\"",[129,174,146],{"class":135},[129,176,177],{"class":149},"\"mytool configuration\"",[129,179,153],{"class":135},[129,181,183,186,188,191],{"class":131,"line":182},5,[129,184,185],{"class":142},"  \"type\"",[129,187,146],{"class":135},[129,189,190],{"class":149},"\"object\"",[129,192,153],{"class":135},[129,194,196,199,201,204],{"class":131,"line":195},6,[129,197,198],{"class":142},"  \"additionalProperties\"",[129,200,146],{"class":135},[129,202,203],{"class":142},"false",[129,205,153],{"class":135},[129,207,209,212],{"class":131,"line":208},7,[129,210,211],{"class":142},"  \"properties\"",[129,213,214],{"class":135},": {\n",[129,216,218,221],{"class":131,"line":217},8,[129,219,220],{"class":142},"    \"api\"",[129,222,214],{"class":135},[129,224,226,229,231,233],{"class":131,"line":225},9,[129,227,228],{"class":142},"      \"type\"",[129,230,146],{"class":135},[129,232,190],{"class":149},[129,234,153],{"class":135},[129,236,238,241,243,245],{"class":131,"line":237},10,[129,239,240],{"class":142},"      \"additionalProperties\"",[129,242,146],{"class":135},[129,244,203],{"class":142},[129,246,153],{"class":135},[129,248,250,253],{"class":131,"line":249},11,[129,251,252],{"class":142},"      \"properties\"",[129,254,214],{"class":135},[129,256,258,261,264,267,269,272,275,278,280,283,285,288,290,293],{"class":131,"line":257},12,[129,259,260],{"class":142},"        \"url\"",[129,262,263],{"class":135},": {",[129,265,266],{"class":142},"\"type\"",[129,268,146],{"class":135},[129,270,271],{"class":149},"\"string\"",[129,273,274],{"class":135},", ",[129,276,277],{"class":142},"\"format\"",[129,279,146],{"class":135},[129,281,282],{"class":149},"\"uri\"",[129,284,274],{"class":135},[129,286,287],{"class":142},"\"pattern\"",[129,289,146],{"class":135},[129,291,292],{"class":149},"\"^https?:\u002F\u002F\"",[129,294,295],{"class":135},"},\n",[129,297,299,302,304,306,308,311,313,316,318,321,323,326,328,331],{"class":131,"line":298},13,[129,300,301],{"class":142},"        \"timeout\"",[129,303,263],{"class":135},[129,305,266],{"class":142},[129,307,146],{"class":135},[129,309,310],{"class":149},"\"number\"",[129,312,274],{"class":135},[129,314,315],{"class":142},"\"exclusiveMinimum\"",[129,317,146],{"class":135},[129,319,320],{"class":142},"0",[129,322,274],{"class":135},[129,324,325],{"class":142},"\"maximum\"",[129,327,146],{"class":135},[129,329,330],{"class":142},"600",[129,332,333],{"class":135},"}\n",[129,335,337],{"class":131,"line":336},14,[129,338,339],{"class":135},"      },\n",[129,341,343,346,349,352],{"class":131,"line":342},15,[129,344,345],{"class":142},"      \"required\"",[129,347,348],{"class":135},": [",[129,350,351],{"class":149},"\"url\"",[129,353,354],{"class":135},"]\n",[129,356,358],{"class":131,"line":357},16,[129,359,360],{"class":135},"    },\n",[129,362,364,367],{"class":131,"line":363},17,[129,365,366],{"class":142},"    \"output\"",[129,368,214],{"class":135},[129,370,372,374,376,378],{"class":131,"line":371},18,[129,373,228],{"class":142},[129,375,146],{"class":135},[129,377,190],{"class":149},[129,379,153],{"class":135},[129,381,383,385,387,389],{"class":131,"line":382},19,[129,384,240],{"class":142},[129,386,146],{"class":135},[129,388,203],{"class":142},[129,390,153],{"class":135},[129,392,394,396],{"class":131,"line":393},20,[129,395,252],{"class":142},[129,397,214],{"class":135},[129,399,401,404,406,409,411,414,416,419,421,424],{"class":131,"line":400},21,[129,402,403],{"class":142},"        \"format\"",[129,405,263],{"class":135},[129,407,408],{"class":142},"\"enum\"",[129,410,348],{"class":135},[129,412,413],{"class":149},"\"table\"",[129,415,274],{"class":135},[129,417,418],{"class":149},"\"json\"",[129,420,274],{"class":135},[129,422,423],{"class":149},"\"csv\"",[129,425,426],{"class":135},"]},\n",[129,428,430,433,435,437,439,442],{"class":131,"line":429},22,[129,431,432],{"class":142},"        \"color\"",[129,434,263],{"class":135},[129,436,266],{"class":142},[129,438,146],{"class":135},[129,440,441],{"class":149},"\"boolean\"",[129,443,333],{"class":135},[129,445,447],{"class":131,"line":446},23,[129,448,449],{"class":135},"      }\n",[129,451,453],{"class":131,"line":452},24,[129,454,360],{"class":135},[129,456,458,461],{"class":131,"line":457},25,[129,459,460],{"class":142},"    \"profiles\"",[129,462,214],{"class":135},[129,464,466,468,470,472],{"class":131,"line":465},26,[129,467,228],{"class":142},[129,469,146],{"class":135},[129,471,190],{"class":149},[129,473,153],{"class":135},[129,475,477,479],{"class":131,"line":476},27,[129,478,240],{"class":142},[129,480,214],{"class":135},[129,482,484,487,489,491],{"class":131,"line":483},28,[129,485,486],{"class":142},"        \"type\"",[129,488,146],{"class":135},[129,490,190],{"class":149},[129,492,153],{"class":135},[129,494,496,499,501,504,506,508,510,512,514,517,519,522],{"class":131,"line":495},29,[129,497,498],{"class":142},"        \"properties\"",[129,500,263],{"class":135},[129,502,503],{"class":142},"\"region\"",[129,505,263],{"class":135},[129,507,266],{"class":142},[129,509,146],{"class":135},[129,511,271],{"class":149},[129,513,274],{"class":135},[129,515,516],{"class":142},"\"minLength\"",[129,518,146],{"class":135},[129,520,521],{"class":142},"1",[129,523,524],{"class":135},"}},\n",[129,526,528,531,533,535],{"class":131,"line":527},30,[129,529,530],{"class":142},"        \"required\"",[129,532,348],{"class":135},[129,534,503],{"class":149},[129,536,354],{"class":135},[129,538,540],{"class":131,"line":539},31,[129,541,449],{"class":135},[129,543,545],{"class":131,"line":544},32,[129,546,547],{"class":135},"    }\n",[129,549,551],{"class":131,"line":550},33,[129,552,553],{"class":135},"  }\n",[129,555,557],{"class":131,"line":556},34,[129,558,333],{"class":135},[10,560,561,562,567,568,571,572,274,575,274,578,581,582,587,588,591,592,595,596,599,600,602,603,52],{},"Three choices make it strict in the ways that help users. ",[23,563,564],{},[14,565,566],{},"additionalProperties: false"," on each table turns a misspelt key from a silently ignored setting into an error. ",[23,569,570],{},"Ranges and enums"," (",[14,573,574],{},"exclusiveMinimum",[14,576,577],{},"maximum",[14,579,580],{},"enum",") catch values that parse fine but make no sense. ",[23,583,584],{},[14,585,586],{},"required"," documents which keys have no default. The ",[14,589,590],{},"profiles"," table shows the pattern for user-named sections: ",[14,593,594],{},"additionalProperties"," holds a ",[88,597,598],{},"schema"," rather than ",[14,601,203],{},", so any profile name is allowed but every profile must have a ",[14,604,605],{},"region",[108,607,609],{"id":608},"validating-and-reporting","Validating and reporting",[120,611,615],{"className":612,"code":613,"language":614,"meta":125,"style":125},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fconfig_schema.py\nfrom __future__ import annotations\n\nimport json\nimport tomllib\nfrom dataclasses import dataclass\nfrom functools import cache\nfrom importlib.resources import files\nfrom pathlib import Path\n\nfrom jsonschema import Draft202012Validator\nfrom jsonschema.exceptions import ValidationError\n\n\n@dataclass(frozen=True)\nclass Problem:\n    location: str\n    message: str\n\n    def __str__(self) -> str:\n        return f\"{self.location}: {self.message}\"\n\n\n@cache\ndef validator() -> Draft202012Validator:\n    schema = json.loads(files(\"mytool\").joinpath(\"config.schema.json\").read_text(encoding=\"utf-8\"))\n    Draft202012Validator.check_schema(schema)        # a broken schema is our bug, not the user's\n    return Draft202012Validator(schema, format_checker=Draft202012Validator.FORMAT_CHECKER)\n\n\ndef _location(error: ValidationError) -> str:\n    parts = [str(p) for p in error.absolute_path]\n    if error.validator == \"additionalProperties\":   # point at the unknown key itself\n        extra = sorted(set(error.instance) - set(error.schema.get(\"properties\", {})))\n        parts += extra[:1]\n    return \".\".join(parts) or \"(top level)\"\n\n\ndef _message(error: ValidationError) -> str:\n    match error.validator:\n        case \"additionalProperties\":\n            allowed = \", \".join(sorted(error.schema.get(\"properties\", {})))\n            return f\"unknown key (allowed: {allowed})\"\n        case \"enum\":\n            return \"must be one of \" + \", \".join(map(repr, error.validator_value))\n        case \"required\":\n            return error.message.replace(\"is a required property\", \"is required\")\n        case _:\n            return error.message\n\n\ndef check(data: dict) -> list[Problem]:\n    errors = sorted(validator().iter_errors(data), key=lambda e: list(map(str, e.absolute_path)))\n    return [Problem(_location(e), _message(e)) for e in errors]\n\n\ndef check_file(path: Path) -> list[Problem]:\n    try:\n        data = tomllib.loads(path.read_text(encoding=\"utf-8\"))\n    except tomllib.TOMLDecodeError as exc:\n        return [Problem(str(path), f\"not valid TOML: {exc}\")]\n    return check(data)\n","python",[14,616,617,623,638,644,652,659,671,683,695,707,711,723,735,739,743,765,776,784,791,795,811,843,847,851,856,867,900,908,929,933,937,951,978,998,1031,1047,1064,1069,1074,1088,1097,1107,1130,1152,1162,1188,1198,1216,1224,1232,1237,1242,1259,1295,1313,1318,1323,1334,1342,1361,1376,1407],{"__ignoreMap":125},[129,618,619],{"class":131,"line":132},[129,620,622],{"class":621},"sJ8bj","# src\u002Fmytool\u002Fconfig_schema.py\n",[129,624,625,629,632,635],{"class":131,"line":139},[129,626,628],{"class":627},"szBVR","from",[129,630,631],{"class":142}," __future__",[129,633,634],{"class":627}," import",[129,636,637],{"class":135}," annotations\n",[129,639,640],{"class":131,"line":156},[129,641,643],{"emptyLinePlaceholder":642},true,"\n",[129,645,646,649],{"class":131,"line":169},[129,647,648],{"class":627},"import",[129,650,651],{"class":135}," json\n",[129,653,654,656],{"class":131,"line":182},[129,655,648],{"class":627},[129,657,658],{"class":135}," tomllib\n",[129,660,661,663,666,668],{"class":131,"line":195},[129,662,628],{"class":627},[129,664,665],{"class":135}," dataclasses ",[129,667,648],{"class":627},[129,669,670],{"class":135}," dataclass\n",[129,672,673,675,678,680],{"class":131,"line":208},[129,674,628],{"class":627},[129,676,677],{"class":135}," functools ",[129,679,648],{"class":627},[129,681,682],{"class":135}," cache\n",[129,684,685,687,690,692],{"class":131,"line":217},[129,686,628],{"class":627},[129,688,689],{"class":135}," importlib.resources ",[129,691,648],{"class":627},[129,693,694],{"class":135}," files\n",[129,696,697,699,702,704],{"class":131,"line":225},[129,698,628],{"class":627},[129,700,701],{"class":135}," pathlib ",[129,703,648],{"class":627},[129,705,706],{"class":135}," Path\n",[129,708,709],{"class":131,"line":237},[129,710,643],{"emptyLinePlaceholder":642},[129,712,713,715,718,720],{"class":131,"line":249},[129,714,628],{"class":627},[129,716,717],{"class":135}," jsonschema ",[129,719,648],{"class":627},[129,721,722],{"class":135}," Draft202012Validator\n",[129,724,725,727,730,732],{"class":131,"line":257},[129,726,628],{"class":627},[129,728,729],{"class":135}," jsonschema.exceptions ",[129,731,648],{"class":627},[129,733,734],{"class":135}," ValidationError\n",[129,736,737],{"class":131,"line":298},[129,738,643],{"emptyLinePlaceholder":642},[129,740,741],{"class":131,"line":336},[129,742,643],{"emptyLinePlaceholder":642},[129,744,745,749,752,756,759,762],{"class":131,"line":342},[129,746,748],{"class":747},"sScJk","@dataclass",[129,750,751],{"class":135},"(",[129,753,755],{"class":754},"s4XuR","frozen",[129,757,758],{"class":627},"=",[129,760,761],{"class":142},"True",[129,763,764],{"class":135},")\n",[129,766,767,770,773],{"class":131,"line":357},[129,768,769],{"class":627},"class",[129,771,772],{"class":747}," Problem",[129,774,775],{"class":135},":\n",[129,777,778,781],{"class":131,"line":363},[129,779,780],{"class":135},"    location: ",[129,782,783],{"class":142},"str\n",[129,785,786,789],{"class":131,"line":371},[129,787,788],{"class":135},"    message: ",[129,790,783],{"class":142},[129,792,793],{"class":131,"line":382},[129,794,643],{"emptyLinePlaceholder":642},[129,796,797,800,803,806,809],{"class":131,"line":393},[129,798,799],{"class":627},"    def",[129,801,802],{"class":142}," __str__",[129,804,805],{"class":135},"(self) -> ",[129,807,808],{"class":142},"str",[129,810,775],{"class":135},[129,812,813,816,819,822,825,828,831,833,835,838,840],{"class":131,"line":400},[129,814,815],{"class":627},"        return",[129,817,818],{"class":627}," f",[129,820,821],{"class":149},"\"",[129,823,824],{"class":142},"{self",[129,826,827],{"class":135},".location",[129,829,830],{"class":142},"}",[129,832,146],{"class":149},[129,834,824],{"class":142},[129,836,837],{"class":135},".message",[129,839,830],{"class":142},[129,841,842],{"class":149},"\"\n",[129,844,845],{"class":131,"line":429},[129,846,643],{"emptyLinePlaceholder":642},[129,848,849],{"class":131,"line":446},[129,850,643],{"emptyLinePlaceholder":642},[129,852,853],{"class":131,"line":452},[129,854,855],{"class":747},"@cache\n",[129,857,858,861,864],{"class":131,"line":457},[129,859,860],{"class":627},"def",[129,862,863],{"class":747}," validator",[129,865,866],{"class":135},"() -> Draft202012Validator:\n",[129,868,869,872,874,877,880,883,886,889,892,894,897],{"class":131,"line":465},[129,870,871],{"class":135},"    schema ",[129,873,758],{"class":627},[129,875,876],{"class":135}," json.loads(files(",[129,878,879],{"class":149},"\"mytool\"",[129,881,882],{"class":135},").joinpath(",[129,884,885],{"class":149},"\"config.schema.json\"",[129,887,888],{"class":135},").read_text(",[129,890,891],{"class":754},"encoding",[129,893,758],{"class":627},[129,895,896],{"class":149},"\"utf-8\"",[129,898,899],{"class":135},"))\n",[129,901,902,905],{"class":131,"line":476},[129,903,904],{"class":135},"    Draft202012Validator.check_schema(schema)        ",[129,906,907],{"class":621},"# a broken schema is our bug, not the user's\n",[129,909,910,913,916,919,921,924,927],{"class":131,"line":483},[129,911,912],{"class":627},"    return",[129,914,915],{"class":135}," Draft202012Validator(schema, ",[129,917,918],{"class":754},"format_checker",[129,920,758],{"class":627},[129,922,923],{"class":135},"Draft202012Validator.",[129,925,926],{"class":142},"FORMAT_CHECKER",[129,928,764],{"class":135},[129,930,931],{"class":131,"line":495},[129,932,643],{"emptyLinePlaceholder":642},[129,934,935],{"class":131,"line":527},[129,936,643],{"emptyLinePlaceholder":642},[129,938,939,941,944,947,949],{"class":131,"line":539},[129,940,860],{"class":627},[129,942,943],{"class":747}," _location",[129,945,946],{"class":135},"(error: ValidationError) -> ",[129,948,808],{"class":142},[129,950,775],{"class":135},[129,952,953,956,958,961,963,966,969,972,975],{"class":131,"line":544},[129,954,955],{"class":135},"    parts ",[129,957,758],{"class":627},[129,959,960],{"class":135}," [",[129,962,808],{"class":142},[129,964,965],{"class":135},"(p) ",[129,967,968],{"class":627},"for",[129,970,971],{"class":135}," p ",[129,973,974],{"class":627},"in",[129,976,977],{"class":135}," error.absolute_path]\n",[129,979,980,983,986,989,992,995],{"class":131,"line":550},[129,981,982],{"class":627},"    if",[129,984,985],{"class":135}," error.validator ",[129,987,988],{"class":627},"==",[129,990,991],{"class":149}," \"additionalProperties\"",[129,993,994],{"class":135},":   ",[129,996,997],{"class":621},"# point at the unknown key itself\n",[129,999,1000,1003,1005,1008,1010,1013,1016,1019,1022,1025,1028],{"class":131,"line":556},[129,1001,1002],{"class":135},"        extra ",[129,1004,758],{"class":627},[129,1006,1007],{"class":142}," sorted",[129,1009,751],{"class":135},[129,1011,1012],{"class":142},"set",[129,1014,1015],{"class":135},"(error.instance) ",[129,1017,1018],{"class":627},"-",[129,1020,1021],{"class":142}," set",[129,1023,1024],{"class":135},"(error.schema.get(",[129,1026,1027],{"class":149},"\"properties\"",[129,1029,1030],{"class":135},", {})))\n",[129,1032,1034,1037,1040,1043,1045],{"class":131,"line":1033},35,[129,1035,1036],{"class":135},"        parts ",[129,1038,1039],{"class":627},"+=",[129,1041,1042],{"class":135}," extra[:",[129,1044,521],{"class":142},[129,1046,354],{"class":135},[129,1048,1050,1052,1055,1058,1061],{"class":131,"line":1049},36,[129,1051,912],{"class":627},[129,1053,1054],{"class":149}," \".\"",[129,1056,1057],{"class":135},".join(parts) ",[129,1059,1060],{"class":627},"or",[129,1062,1063],{"class":149}," \"(top level)\"\n",[129,1065,1067],{"class":131,"line":1066},37,[129,1068,643],{"emptyLinePlaceholder":642},[129,1070,1072],{"class":131,"line":1071},38,[129,1073,643],{"emptyLinePlaceholder":642},[129,1075,1077,1079,1082,1084,1086],{"class":131,"line":1076},39,[129,1078,860],{"class":627},[129,1080,1081],{"class":747}," _message",[129,1083,946],{"class":135},[129,1085,808],{"class":142},[129,1087,775],{"class":135},[129,1089,1091,1094],{"class":131,"line":1090},40,[129,1092,1093],{"class":627},"    match",[129,1095,1096],{"class":135}," error.validator:\n",[129,1098,1100,1103,1105],{"class":131,"line":1099},41,[129,1101,1102],{"class":627},"        case",[129,1104,991],{"class":149},[129,1106,775],{"class":135},[129,1108,1110,1113,1115,1118,1121,1124,1126,1128],{"class":131,"line":1109},42,[129,1111,1112],{"class":135},"            allowed ",[129,1114,758],{"class":627},[129,1116,1117],{"class":149}," \", \"",[129,1119,1120],{"class":135},".join(",[129,1122,1123],{"class":142},"sorted",[129,1125,1024],{"class":135},[129,1127,1027],{"class":149},[129,1129,1030],{"class":135},[129,1131,1133,1136,1138,1141,1144,1147,1149],{"class":131,"line":1132},43,[129,1134,1135],{"class":627},"            return",[129,1137,818],{"class":627},[129,1139,1140],{"class":149},"\"unknown key (allowed: ",[129,1142,1143],{"class":142},"{",[129,1145,1146],{"class":135},"allowed",[129,1148,830],{"class":142},[129,1150,1151],{"class":149},")\"\n",[129,1153,1155,1157,1160],{"class":131,"line":1154},44,[129,1156,1102],{"class":627},[129,1158,1159],{"class":149}," \"enum\"",[129,1161,775],{"class":135},[129,1163,1165,1167,1170,1173,1175,1177,1180,1182,1185],{"class":131,"line":1164},45,[129,1166,1135],{"class":627},[129,1168,1169],{"class":149}," \"must be one of \"",[129,1171,1172],{"class":627}," +",[129,1174,1117],{"class":149},[129,1176,1120],{"class":135},[129,1178,1179],{"class":142},"map",[129,1181,751],{"class":135},[129,1183,1184],{"class":142},"repr",[129,1186,1187],{"class":135},", error.validator_value))\n",[129,1189,1191,1193,1196],{"class":131,"line":1190},46,[129,1192,1102],{"class":627},[129,1194,1195],{"class":149}," \"required\"",[129,1197,775],{"class":135},[129,1199,1201,1203,1206,1209,1211,1214],{"class":131,"line":1200},47,[129,1202,1135],{"class":627},[129,1204,1205],{"class":135}," error.message.replace(",[129,1207,1208],{"class":149},"\"is a required property\"",[129,1210,274],{"class":135},[129,1212,1213],{"class":149},"\"is required\"",[129,1215,764],{"class":135},[129,1217,1219,1221],{"class":131,"line":1218},48,[129,1220,1102],{"class":627},[129,1222,1223],{"class":135}," _:\n",[129,1225,1227,1229],{"class":131,"line":1226},49,[129,1228,1135],{"class":627},[129,1230,1231],{"class":135}," error.message\n",[129,1233,1235],{"class":131,"line":1234},50,[129,1236,643],{"emptyLinePlaceholder":642},[129,1238,1240],{"class":131,"line":1239},51,[129,1241,643],{"emptyLinePlaceholder":642},[129,1243,1245,1247,1250,1253,1256],{"class":131,"line":1244},52,[129,1246,860],{"class":627},[129,1248,1249],{"class":747}," check",[129,1251,1252],{"class":135},"(data: ",[129,1254,1255],{"class":142},"dict",[129,1257,1258],{"class":135},") -> list[Problem]:\n",[129,1260,1262,1265,1267,1269,1272,1275,1278,1281,1284,1286,1288,1290,1292],{"class":131,"line":1261},53,[129,1263,1264],{"class":135},"    errors ",[129,1266,758],{"class":627},[129,1268,1007],{"class":142},[129,1270,1271],{"class":135},"(validator().iter_errors(data), ",[129,1273,1274],{"class":754},"key",[129,1276,1277],{"class":627},"=lambda",[129,1279,1280],{"class":135}," e: ",[129,1282,1283],{"class":142},"list",[129,1285,751],{"class":135},[129,1287,1179],{"class":142},[129,1289,751],{"class":135},[129,1291,808],{"class":142},[129,1293,1294],{"class":135},", e.absolute_path)))\n",[129,1296,1298,1300,1303,1305,1308,1310],{"class":131,"line":1297},54,[129,1299,912],{"class":627},[129,1301,1302],{"class":135}," [Problem(_location(e), _message(e)) ",[129,1304,968],{"class":627},[129,1306,1307],{"class":135}," e ",[129,1309,974],{"class":627},[129,1311,1312],{"class":135}," errors]\n",[129,1314,1316],{"class":131,"line":1315},55,[129,1317,643],{"emptyLinePlaceholder":642},[129,1319,1321],{"class":131,"line":1320},56,[129,1322,643],{"emptyLinePlaceholder":642},[129,1324,1326,1328,1331],{"class":131,"line":1325},57,[129,1327,860],{"class":627},[129,1329,1330],{"class":747}," check_file",[129,1332,1333],{"class":135},"(path: Path) -> list[Problem]:\n",[129,1335,1337,1340],{"class":131,"line":1336},58,[129,1338,1339],{"class":627},"    try",[129,1341,775],{"class":135},[129,1343,1345,1348,1350,1353,1355,1357,1359],{"class":131,"line":1344},59,[129,1346,1347],{"class":135},"        data ",[129,1349,758],{"class":627},[129,1351,1352],{"class":135}," tomllib.loads(path.read_text(",[129,1354,891],{"class":754},[129,1356,758],{"class":627},[129,1358,896],{"class":149},[129,1360,899],{"class":135},[129,1362,1364,1367,1370,1373],{"class":131,"line":1363},60,[129,1365,1366],{"class":627},"    except",[129,1368,1369],{"class":135}," tomllib.TOMLDecodeError ",[129,1371,1372],{"class":627},"as",[129,1374,1375],{"class":135}," exc:\n",[129,1377,1379,1381,1384,1386,1389,1392,1395,1397,1400,1402,1404],{"class":131,"line":1378},61,[129,1380,815],{"class":627},[129,1382,1383],{"class":135}," [Problem(",[129,1385,808],{"class":142},[129,1387,1388],{"class":135},"(path), ",[129,1390,1391],{"class":627},"f",[129,1393,1394],{"class":149},"\"not valid TOML: ",[129,1396,1143],{"class":142},[129,1398,1399],{"class":135},"exc",[129,1401,830],{"class":142},[129,1403,821],{"class":149},[129,1405,1406],{"class":135},")]\n",[129,1408,1410,1412],{"class":131,"line":1409},62,[129,1411,912],{"class":627},[129,1413,1414],{"class":135}," check(data)\n",[10,1416,1417,1420,1421,1424,1425,1427,1428,1430,1431,1434],{},[14,1418,1419],{},"iter_errors"," yields every violation rather than stopping at the first, and sorting by path groups problems by table. ",[14,1422,1423],{},"absolute_path"," is the list of keys leading to the failing value, which joins naturally into the dotted notation users see in their file (",[14,1426,29],{},"). The two helpers then translate the library’s messages where they read poorly. An ",[14,1429,594],{}," error normally says “Additional properties are not allowed ('timout' was unexpected)” and points at the ",[88,1432,1433],{},"parent"," table; the helper points at the unknown key itself and lists the keys that are allowed, which is usually enough for the user to spot the typo. Enum errors become a plain list of choices. Everything else uses the library’s message, which is already clear for types and ranges.",[120,1436,1441],{"className":1437,"code":1439,"language":1440,"meta":125},[1438],"language-text","$ mytool config validate ~\u002F.config\u002Fmytool\u002Fconfig.toml\n~\u002F.config\u002Fmytool\u002Fconfig.toml: api.retries: unknown key (allowed: timeout, url)\n~\u002F.config\u002Fmytool\u002Fconfig.toml: api: 'url' is required\n~\u002F.config\u002Fmytool\u002Fconfig.toml: api.timeout: -1 is less than or equal to the minimum of 0\n~\u002F.config\u002Fmytool\u002Fconfig.toml: output.format: must be one of 'table', 'json', 'csv'\n","text",[14,1442,1439],{"__ignoreMap":125},[10,1444,1445,1446,1449,1450,1453],{},"The validator is built once and cached. ",[14,1447,1448],{},"check_schema"," validates the schema against the JSON Schema meta-schema first, so a mistake in ",[88,1451,1452],{},"your"," schema fails loudly in your tests rather than producing confusing results for users.",[108,1455,1457],{"id":1456},"commands-for-users-and-editors","Commands for users and editors",[120,1459,1461],{"className":612,"code":1460,"language":614,"meta":125,"style":125},"# src\u002Fmytool\u002Fvalidate_cmd.py\nimport sys\nfrom importlib.resources import files\nfrom pathlib import Path\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.config_schema import check_file\n\napp = typer.Typer(help=\"Configuration commands.\")\n\n\n@app.command()\ndef validate(path: Annotated[Path, typer.Argument(exists=True, dir_okay=False)]) -> None:\n    \"\"\"Check a configuration file against the schema and report every problem.\"\"\"\n    problems = check_file(path)\n    for problem in problems:\n        typer.echo(f\"{path}: {problem}\", err=True)\n    if problems:\n        raise typer.Exit(1)\n    typer.echo(f\"{path}: ok\", err=True)\n\n\n@app.command()\ndef schema() -> None:\n    \"\"\"Print the JSON Schema for editors and other tools.\"\"\"\n    sys.stdout.write(files(\"mytool\").joinpath(\"config.schema.json\").read_text(encoding=\"utf-8\"))\n",[14,1462,1463,1468,1475,1485,1495,1507,1511,1518,1522,1534,1538,1558,1562,1566,1574,1609,1614,1624,1637,1675,1681,1693,1721,1725,1729,1735,1749,1754],{"__ignoreMap":125},[129,1464,1465],{"class":131,"line":132},[129,1466,1467],{"class":621},"# src\u002Fmytool\u002Fvalidate_cmd.py\n",[129,1469,1470,1472],{"class":131,"line":139},[129,1471,648],{"class":627},[129,1473,1474],{"class":135}," sys\n",[129,1476,1477,1479,1481,1483],{"class":131,"line":156},[129,1478,628],{"class":627},[129,1480,689],{"class":135},[129,1482,648],{"class":627},[129,1484,694],{"class":135},[129,1486,1487,1489,1491,1493],{"class":131,"line":169},[129,1488,628],{"class":627},[129,1490,701],{"class":135},[129,1492,648],{"class":627},[129,1494,706],{"class":135},[129,1496,1497,1499,1502,1504],{"class":131,"line":182},[129,1498,628],{"class":627},[129,1500,1501],{"class":135}," typing ",[129,1503,648],{"class":627},[129,1505,1506],{"class":135}," Annotated\n",[129,1508,1509],{"class":131,"line":195},[129,1510,643],{"emptyLinePlaceholder":642},[129,1512,1513,1515],{"class":131,"line":208},[129,1514,648],{"class":627},[129,1516,1517],{"class":135}," typer\n",[129,1519,1520],{"class":131,"line":217},[129,1521,643],{"emptyLinePlaceholder":642},[129,1523,1524,1526,1529,1531],{"class":131,"line":225},[129,1525,628],{"class":627},[129,1527,1528],{"class":135}," mytool.config_schema ",[129,1530,648],{"class":627},[129,1532,1533],{"class":135}," check_file\n",[129,1535,1536],{"class":131,"line":237},[129,1537,643],{"emptyLinePlaceholder":642},[129,1539,1540,1543,1545,1548,1551,1553,1556],{"class":131,"line":249},[129,1541,1542],{"class":135},"app ",[129,1544,758],{"class":627},[129,1546,1547],{"class":135}," typer.Typer(",[129,1549,1550],{"class":754},"help",[129,1552,758],{"class":627},[129,1554,1555],{"class":149},"\"Configuration commands.\"",[129,1557,764],{"class":135},[129,1559,1560],{"class":131,"line":257},[129,1561,643],{"emptyLinePlaceholder":642},[129,1563,1564],{"class":131,"line":298},[129,1565,643],{"emptyLinePlaceholder":642},[129,1567,1568,1571],{"class":131,"line":336},[129,1569,1570],{"class":747},"@app.command",[129,1572,1573],{"class":135},"()\n",[129,1575,1576,1578,1581,1584,1587,1589,1591,1593,1596,1598,1601,1604,1607],{"class":131,"line":342},[129,1577,860],{"class":627},[129,1579,1580],{"class":747}," validate",[129,1582,1583],{"class":135},"(path: Annotated[Path, typer.Argument(",[129,1585,1586],{"class":754},"exists",[129,1588,758],{"class":627},[129,1590,761],{"class":142},[129,1592,274],{"class":135},[129,1594,1595],{"class":754},"dir_okay",[129,1597,758],{"class":627},[129,1599,1600],{"class":142},"False",[129,1602,1603],{"class":135},")]) -> ",[129,1605,1606],{"class":142},"None",[129,1608,775],{"class":135},[129,1610,1611],{"class":131,"line":357},[129,1612,1613],{"class":149},"    \"\"\"Check a configuration file against the schema and report every problem.\"\"\"\n",[129,1615,1616,1619,1621],{"class":131,"line":363},[129,1617,1618],{"class":135},"    problems ",[129,1620,758],{"class":627},[129,1622,1623],{"class":135}," check_file(path)\n",[129,1625,1626,1629,1632,1634],{"class":131,"line":371},[129,1627,1628],{"class":627},"    for",[129,1630,1631],{"class":135}," problem ",[129,1633,974],{"class":627},[129,1635,1636],{"class":135}," problems:\n",[129,1638,1639,1642,1644,1646,1648,1651,1653,1655,1657,1660,1662,1664,1666,1669,1671,1673],{"class":131,"line":382},[129,1640,1641],{"class":135},"        typer.echo(",[129,1643,1391],{"class":627},[129,1645,821],{"class":149},[129,1647,1143],{"class":142},[129,1649,1650],{"class":135},"path",[129,1652,830],{"class":142},[129,1654,146],{"class":149},[129,1656,1143],{"class":142},[129,1658,1659],{"class":135},"problem",[129,1661,830],{"class":142},[129,1663,821],{"class":149},[129,1665,274],{"class":135},[129,1667,1668],{"class":754},"err",[129,1670,758],{"class":627},[129,1672,761],{"class":142},[129,1674,764],{"class":135},[129,1676,1677,1679],{"class":131,"line":393},[129,1678,982],{"class":627},[129,1680,1636],{"class":135},[129,1682,1683,1686,1689,1691],{"class":131,"line":400},[129,1684,1685],{"class":627},"        raise",[129,1687,1688],{"class":135}," typer.Exit(",[129,1690,521],{"class":142},[129,1692,764],{"class":135},[129,1694,1695,1698,1700,1702,1704,1706,1708,1711,1713,1715,1717,1719],{"class":131,"line":429},[129,1696,1697],{"class":135},"    typer.echo(",[129,1699,1391],{"class":627},[129,1701,821],{"class":149},[129,1703,1143],{"class":142},[129,1705,1650],{"class":135},[129,1707,830],{"class":142},[129,1709,1710],{"class":149},": ok\"",[129,1712,274],{"class":135},[129,1714,1668],{"class":754},[129,1716,758],{"class":627},[129,1718,761],{"class":142},[129,1720,764],{"class":135},[129,1722,1723],{"class":131,"line":446},[129,1724,643],{"emptyLinePlaceholder":642},[129,1726,1727],{"class":131,"line":452},[129,1728,643],{"emptyLinePlaceholder":642},[129,1730,1731,1733],{"class":131,"line":457},[129,1732,1570],{"class":747},[129,1734,1573],{"class":135},[129,1736,1737,1739,1742,1745,1747],{"class":131,"line":465},[129,1738,860],{"class":627},[129,1740,1741],{"class":747}," schema",[129,1743,1744],{"class":135},"() -> ",[129,1746,1606],{"class":142},[129,1748,775],{"class":135},[129,1750,1751],{"class":131,"line":476},[129,1752,1753],{"class":149},"    \"\"\"Print the JSON Schema for editors and other tools.\"\"\"\n",[129,1755,1756,1759,1761,1763,1765,1767,1769,1771,1773],{"class":131,"line":483},[129,1757,1758],{"class":135},"    sys.stdout.write(files(",[129,1760,879],{"class":149},[129,1762,882],{"class":135},[129,1764,885],{"class":149},[129,1766,888],{"class":135},[129,1768,891],{"class":754},[129,1770,758],{"class":627},[129,1772,896],{"class":149},[129,1774,899],{"class":135},[10,1776,1777,1779,1780,1783,1784,1786],{},[14,1778,41],{}," is useful on its own — in CI for teams that keep a shared config in a repository, or after hand-editing — and the runtime loader should call the same ",[14,1781,1782],{},"check_file"," and refuse to run with an invalid file, printing the same messages. ",[14,1785,45],{}," prints the schema on stdout so other tools can consume it.",[81,1788],{"name":1789},"jschema-terminal",[54,1791,1793],{"id":1792},"editor-support-for-free","Editor support for free",[10,1795,1796],{},"The schema becomes much more valuable once users’ editors know about it. For TOML, the Taplo language server (used by the “Even Better TOML” VS Code extension and others) reads a directive on the first line of the file:",[120,1798,1802],{"className":1799,"code":1800,"language":1801,"meta":125,"style":125},"language-toml shiki shiki-themes github-light github-dark","#:schema https:\u002F\u002Fexample.com\u002Fmytool\u002Fconfig.schema.json\n[api]\nurl = \"https:\u002F\u002Fapi.example.com\"\ntimeout = 30\n","toml",[14,1803,1804,1809,1819,1827],{"__ignoreMap":125},[129,1805,1806],{"class":131,"line":132},[129,1807,1808],{"class":621},"#:schema https:\u002F\u002Fexample.com\u002Fmytool\u002Fconfig.schema.json\n",[129,1810,1811,1814,1817],{"class":131,"line":139},[129,1812,1813],{"class":135},"[",[129,1815,1816],{"class":747},"api",[129,1818,354],{"class":135},[129,1820,1821,1824],{"class":131,"line":156},[129,1822,1823],{"class":135},"url = ",[129,1825,1826],{"class":149},"\"https:\u002F\u002Fapi.example.com\"\n",[129,1828,1829,1832],{"class":131,"line":169},[129,1830,1831],{"class":135},"timeout = ",[129,1833,1834],{"class":142},"30\n",[10,1836,1837,1838,1841,1842,1845,1846,1850,1851,1854,1855,1858],{},"With that line, the editor completes key names, shows the ",[14,1839,1840],{},"description"," fields on hover and underlines invalid values as the user types. Put the directive in the template written by ",[14,1843,1844],{},"config init",", as in ",[48,1847,1849],{"href":1848},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command\u002F","writing a config init and edit command",", and publish the schema at a stable URL with each release. For YAML configuration, the equivalent is ",[14,1852,1853],{},"# yaml-language-server: $schema=\u003Curl>","; for a project-level config, registering the schema in the community SchemaStore catalogue makes editors pick it up by filename without any directive at all. Adding ",[14,1856,1857],{},"\"description\""," to every property is the single most useful improvement for that experience.",[54,1860,1862],{"id":1861},"generating-the-schema-from-a-settings-model","Generating the schema from a settings model",[10,1864,1865,1866,1870,1871,1874],{},"If configuration is loaded into a pydantic model, as in ",[48,1867,1869],{"href":1868},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings\u002F","typed settings with pydantic-settings",", the model already contains the rules, and ",[14,1872,1873],{},"Settings.model_json_schema()"," produces a schema from it. Generate the file in a small script, commit it, and add a test that regenerates it and compares — the same “generated file must match” pattern used for documentation. You keep a single source of truth while still shipping a static schema for editors. Pydantic’s own validation errors are good, so many tools use pydantic for runtime checks and the generated schema only for editors.",[81,1876],{"name":1877},"jschema-strictness",[54,1879,1881],{"id":1880},"ux-considerations","UX considerations",[59,1883,1884,1890,1896,1905,1923],{},[62,1885,1886,1889],{},[23,1887,1888],{},"Report everything at once."," Fixing one error per run, five runs in a row, is the experience to avoid.",[62,1891,1892,1895],{},[23,1893,1894],{},"Use the user’s vocabulary."," Dotted TOML paths and the file name; never Python reprs of internal objects or JSON Pointer syntax.",[62,1897,1898,1901,1902,1904],{},[23,1899,1900],{},"Be strict about keys, lenient about extras you expect."," Unknown keys should fail; if you support plugin sections, give them their own ",[14,1903,594],{}," schema rather than turning strictness off.",[62,1906,1907,1910,1911,1914,1915,1918,1919,1922],{},[23,1908,1909],{},"Mind format checks."," ",[14,1912,1913],{},"\"format\": \"uri\""," is only enforced when the optional ",[14,1916,1917],{},"rfc3987"," package is installed (and other formats have their own extras); the ",[14,1920,1921],{},"pattern"," above enforces the important part regardless.",[62,1924,1925,1928,1929,1932,1933,52],{},[23,1926,1927],{},"Keep old keys working."," When renaming a setting, keep the old name in the schema for a release with ",[14,1930,1931],{},"\"deprecated\": true",", and warn — see ",[48,1934,1936],{"href":1935},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F","versioning and deprecating CLI flags",[54,1938,1940],{"id":1939},"testing-the-behaviour","Testing the behaviour",[10,1942,1943],{},"Test the schema itself, one representative problem per rule, and the command-line behaviour:",[120,1945,1947],{"className":612,"code":1946,"language":614,"meta":125,"style":125},"# tests\u002Ftest_config_schema.py\nimport json\nfrom importlib.resources import files\n\nimport pytest\nfrom jsonschema import Draft202012Validator\n\nfrom mytool.config_schema import check, check_file\n\nVALID = {\"api\": {\"url\": \"https:\u002F\u002Fapi.example.com\", \"timeout\": 30},\n         \"output\": {\"format\": \"json\"}, \"profiles\": {\"eu\": {\"region\": \"eu-west-1\"}}}\n\n\ndef test_schema_itself_is_valid():\n    schema = json.loads(files(\"mytool\").joinpath(\"config.schema.json\").read_text())\n    Draft202012Validator.check_schema(schema)\n\n\ndef test_valid_config_has_no_problems():\n    assert check(VALID) == []\n\n\n@pytest.mark.parametrize(\"data,location,fragment\", [\n    ({\"api\": {\"url\": \"https:\u002F\u002Fx\", \"timout\": 5}}, \"api.timout\", \"unknown key\"),\n    ({\"api\": {\"url\": \"https:\u002F\u002Fx\", \"timeout\": 0}}, \"api.timeout\", \"less than or equal to the minimum\"),\n    ({\"api\": {\"url\": \"ftp:\u002F\u002Fx\"}}, \"api.url\", \"does not match\"),\n    ({\"api\": {}}, \"api\", \"'url' is required\"),\n    ({\"output\": {\"format\": \"xml\"}}, \"output.format\", \"must be one of 'table'\"),\n    ({\"profiles\": {\"eu\": {}}}, \"profiles.eu\", \"'region' is required\"),\n])\ndef test_problems_point_at_the_key(data, location, fragment):\n    problems = check(data)\n    assert len(problems) == 1\n    assert problems[0].location == location\n    assert fragment in problems[0].message\n\n\ndef test_all_problems_are_reported_at_once():\n    data = {\"api\": {\"url\": \"https:\u002F\u002Fx\", \"timeout\": -1}, \"output\": {\"format\": \"xml\", \"colour\": True}}\n    assert [p.location for p in check(data)] == [\"api.timeout\", \"output.colour\", \"output.format\"]\n\n\ndef test_broken_toml_is_reported(tmp_path):\n    path = tmp_path \u002F \"config.toml\"\n    path.write_text(\"[api\\n\")\n    [problem] = check_file(path)\n    assert \"not valid TOML\" in problem.message\n",[14,1948,1949,1954,1960,1970,1974,1981,1991,1995,2006,2010,2045,2081,2085,2089,2099,2116,2121,2125,2129,2138,2156,2160,2164,2177,2217,2251,2278,2296,2324,2347,2352,2362,2370,2385,2402,2418,2422,2426,2435,2488,2521,2525,2529,2539,2555,2570,2579],{"__ignoreMap":125},[129,1950,1951],{"class":131,"line":132},[129,1952,1953],{"class":621},"# tests\u002Ftest_config_schema.py\n",[129,1955,1956,1958],{"class":131,"line":139},[129,1957,648],{"class":627},[129,1959,651],{"class":135},[129,1961,1962,1964,1966,1968],{"class":131,"line":156},[129,1963,628],{"class":627},[129,1965,689],{"class":135},[129,1967,648],{"class":627},[129,1969,694],{"class":135},[129,1971,1972],{"class":131,"line":169},[129,1973,643],{"emptyLinePlaceholder":642},[129,1975,1976,1978],{"class":131,"line":182},[129,1977,648],{"class":627},[129,1979,1980],{"class":135}," pytest\n",[129,1982,1983,1985,1987,1989],{"class":131,"line":195},[129,1984,628],{"class":627},[129,1986,717],{"class":135},[129,1988,648],{"class":627},[129,1990,722],{"class":135},[129,1992,1993],{"class":131,"line":208},[129,1994,643],{"emptyLinePlaceholder":642},[129,1996,1997,1999,2001,2003],{"class":131,"line":217},[129,1998,628],{"class":627},[129,2000,1528],{"class":135},[129,2002,648],{"class":627},[129,2004,2005],{"class":135}," check, check_file\n",[129,2007,2008],{"class":131,"line":225},[129,2009,643],{"emptyLinePlaceholder":642},[129,2011,2012,2015,2018,2021,2024,2026,2028,2030,2033,2035,2038,2040,2043],{"class":131,"line":237},[129,2013,2014],{"class":142},"VALID",[129,2016,2017],{"class":627}," =",[129,2019,2020],{"class":135}," {",[129,2022,2023],{"class":149},"\"api\"",[129,2025,263],{"class":135},[129,2027,351],{"class":149},[129,2029,146],{"class":135},[129,2031,2032],{"class":149},"\"https:\u002F\u002Fapi.example.com\"",[129,2034,274],{"class":135},[129,2036,2037],{"class":149},"\"timeout\"",[129,2039,146],{"class":135},[129,2041,2042],{"class":142},"30",[129,2044,295],{"class":135},[129,2046,2047,2050,2052,2054,2056,2058,2061,2064,2066,2069,2071,2073,2075,2078],{"class":131,"line":249},[129,2048,2049],{"class":149},"         \"output\"",[129,2051,263],{"class":135},[129,2053,277],{"class":149},[129,2055,146],{"class":135},[129,2057,418],{"class":149},[129,2059,2060],{"class":135},"}, ",[129,2062,2063],{"class":149},"\"profiles\"",[129,2065,263],{"class":135},[129,2067,2068],{"class":149},"\"eu\"",[129,2070,263],{"class":135},[129,2072,503],{"class":149},[129,2074,146],{"class":135},[129,2076,2077],{"class":149},"\"eu-west-1\"",[129,2079,2080],{"class":135},"}}}\n",[129,2082,2083],{"class":131,"line":257},[129,2084,643],{"emptyLinePlaceholder":642},[129,2086,2087],{"class":131,"line":298},[129,2088,643],{"emptyLinePlaceholder":642},[129,2090,2091,2093,2096],{"class":131,"line":336},[129,2092,860],{"class":627},[129,2094,2095],{"class":747}," test_schema_itself_is_valid",[129,2097,2098],{"class":135},"():\n",[129,2100,2101,2103,2105,2107,2109,2111,2113],{"class":131,"line":342},[129,2102,871],{"class":135},[129,2104,758],{"class":627},[129,2106,876],{"class":135},[129,2108,879],{"class":149},[129,2110,882],{"class":135},[129,2112,885],{"class":149},[129,2114,2115],{"class":135},").read_text())\n",[129,2117,2118],{"class":131,"line":357},[129,2119,2120],{"class":135},"    Draft202012Validator.check_schema(schema)\n",[129,2122,2123],{"class":131,"line":363},[129,2124,643],{"emptyLinePlaceholder":642},[129,2126,2127],{"class":131,"line":371},[129,2128,643],{"emptyLinePlaceholder":642},[129,2130,2131,2133,2136],{"class":131,"line":382},[129,2132,860],{"class":627},[129,2134,2135],{"class":747}," test_valid_config_has_no_problems",[129,2137,2098],{"class":135},[129,2139,2140,2143,2146,2148,2151,2153],{"class":131,"line":393},[129,2141,2142],{"class":627},"    assert",[129,2144,2145],{"class":135}," check(",[129,2147,2014],{"class":142},[129,2149,2150],{"class":135},") ",[129,2152,988],{"class":627},[129,2154,2155],{"class":135}," []\n",[129,2157,2158],{"class":131,"line":400},[129,2159,643],{"emptyLinePlaceholder":642},[129,2161,2162],{"class":131,"line":429},[129,2163,643],{"emptyLinePlaceholder":642},[129,2165,2166,2169,2171,2174],{"class":131,"line":446},[129,2167,2168],{"class":747},"@pytest.mark.parametrize",[129,2170,751],{"class":135},[129,2172,2173],{"class":149},"\"data,location,fragment\"",[129,2175,2176],{"class":135},", [\n",[129,2178,2179,2182,2184,2186,2188,2190,2193,2195,2198,2200,2203,2206,2209,2211,2214],{"class":131,"line":452},[129,2180,2181],{"class":135},"    ({",[129,2183,2023],{"class":149},[129,2185,263],{"class":135},[129,2187,351],{"class":149},[129,2189,146],{"class":135},[129,2191,2192],{"class":149},"\"https:\u002F\u002Fx\"",[129,2194,274],{"class":135},[129,2196,2197],{"class":149},"\"timout\"",[129,2199,146],{"class":135},[129,2201,2202],{"class":142},"5",[129,2204,2205],{"class":135},"}}, ",[129,2207,2208],{"class":149},"\"api.timout\"",[129,2210,274],{"class":135},[129,2212,2213],{"class":149},"\"unknown key\"",[129,2215,2216],{"class":135},"),\n",[129,2218,2219,2221,2223,2225,2227,2229,2231,2233,2235,2237,2239,2241,2244,2246,2249],{"class":131,"line":457},[129,2220,2181],{"class":135},[129,2222,2023],{"class":149},[129,2224,263],{"class":135},[129,2226,351],{"class":149},[129,2228,146],{"class":135},[129,2230,2192],{"class":149},[129,2232,274],{"class":135},[129,2234,2037],{"class":149},[129,2236,146],{"class":135},[129,2238,320],{"class":142},[129,2240,2205],{"class":135},[129,2242,2243],{"class":149},"\"api.timeout\"",[129,2245,274],{"class":135},[129,2247,2248],{"class":149},"\"less than or equal to the minimum\"",[129,2250,2216],{"class":135},[129,2252,2253,2255,2257,2259,2261,2263,2266,2268,2271,2273,2276],{"class":131,"line":465},[129,2254,2181],{"class":135},[129,2256,2023],{"class":149},[129,2258,263],{"class":135},[129,2260,351],{"class":149},[129,2262,146],{"class":135},[129,2264,2265],{"class":149},"\"ftp:\u002F\u002Fx\"",[129,2267,2205],{"class":135},[129,2269,2270],{"class":149},"\"api.url\"",[129,2272,274],{"class":135},[129,2274,2275],{"class":149},"\"does not match\"",[129,2277,2216],{"class":135},[129,2279,2280,2282,2284,2287,2289,2291,2294],{"class":131,"line":476},[129,2281,2181],{"class":135},[129,2283,2023],{"class":149},[129,2285,2286],{"class":135},": {}}, ",[129,2288,2023],{"class":149},[129,2290,274],{"class":135},[129,2292,2293],{"class":149},"\"'url' is required\"",[129,2295,2216],{"class":135},[129,2297,2298,2300,2303,2305,2307,2309,2312,2314,2317,2319,2322],{"class":131,"line":483},[129,2299,2181],{"class":135},[129,2301,2302],{"class":149},"\"output\"",[129,2304,263],{"class":135},[129,2306,277],{"class":149},[129,2308,146],{"class":135},[129,2310,2311],{"class":149},"\"xml\"",[129,2313,2205],{"class":135},[129,2315,2316],{"class":149},"\"output.format\"",[129,2318,274],{"class":135},[129,2320,2321],{"class":149},"\"must be one of 'table'\"",[129,2323,2216],{"class":135},[129,2325,2326,2328,2330,2332,2334,2337,2340,2342,2345],{"class":131,"line":495},[129,2327,2181],{"class":135},[129,2329,2063],{"class":149},[129,2331,263],{"class":135},[129,2333,2068],{"class":149},[129,2335,2336],{"class":135},": {}}}, ",[129,2338,2339],{"class":149},"\"profiles.eu\"",[129,2341,274],{"class":135},[129,2343,2344],{"class":149},"\"'region' is required\"",[129,2346,2216],{"class":135},[129,2348,2349],{"class":131,"line":527},[129,2350,2351],{"class":135},"])\n",[129,2353,2354,2356,2359],{"class":131,"line":539},[129,2355,860],{"class":627},[129,2357,2358],{"class":747}," test_problems_point_at_the_key",[129,2360,2361],{"class":135},"(data, location, fragment):\n",[129,2363,2364,2366,2368],{"class":131,"line":544},[129,2365,1618],{"class":135},[129,2367,758],{"class":627},[129,2369,1414],{"class":135},[129,2371,2372,2374,2377,2380,2382],{"class":131,"line":550},[129,2373,2142],{"class":627},[129,2375,2376],{"class":142}," len",[129,2378,2379],{"class":135},"(problems) ",[129,2381,988],{"class":627},[129,2383,2384],{"class":142}," 1\n",[129,2386,2387,2389,2392,2394,2397,2399],{"class":131,"line":556},[129,2388,2142],{"class":627},[129,2390,2391],{"class":135}," problems[",[129,2393,320],{"class":142},[129,2395,2396],{"class":135},"].location ",[129,2398,988],{"class":627},[129,2400,2401],{"class":135}," location\n",[129,2403,2404,2406,2409,2411,2413,2415],{"class":131,"line":1033},[129,2405,2142],{"class":627},[129,2407,2408],{"class":135}," fragment ",[129,2410,974],{"class":627},[129,2412,2391],{"class":135},[129,2414,320],{"class":142},[129,2416,2417],{"class":135},"].message\n",[129,2419,2420],{"class":131,"line":1049},[129,2421,643],{"emptyLinePlaceholder":642},[129,2423,2424],{"class":131,"line":1066},[129,2425,643],{"emptyLinePlaceholder":642},[129,2427,2428,2430,2433],{"class":131,"line":1071},[129,2429,860],{"class":627},[129,2431,2432],{"class":747}," test_all_problems_are_reported_at_once",[129,2434,2098],{"class":135},[129,2436,2437,2440,2442,2444,2446,2448,2450,2452,2454,2456,2458,2460,2462,2464,2466,2468,2470,2472,2474,2476,2478,2481,2483,2485],{"class":131,"line":1076},[129,2438,2439],{"class":135},"    data ",[129,2441,758],{"class":627},[129,2443,2020],{"class":135},[129,2445,2023],{"class":149},[129,2447,263],{"class":135},[129,2449,351],{"class":149},[129,2451,146],{"class":135},[129,2453,2192],{"class":149},[129,2455,274],{"class":135},[129,2457,2037],{"class":149},[129,2459,146],{"class":135},[129,2461,1018],{"class":627},[129,2463,521],{"class":142},[129,2465,2060],{"class":135},[129,2467,2302],{"class":149},[129,2469,263],{"class":135},[129,2471,277],{"class":149},[129,2473,146],{"class":135},[129,2475,2311],{"class":149},[129,2477,274],{"class":135},[129,2479,2480],{"class":149},"\"colour\"",[129,2482,146],{"class":135},[129,2484,761],{"class":142},[129,2486,2487],{"class":135},"}}\n",[129,2489,2490,2492,2495,2497,2499,2501,2504,2506,2508,2510,2512,2515,2517,2519],{"class":131,"line":1090},[129,2491,2142],{"class":627},[129,2493,2494],{"class":135}," [p.location ",[129,2496,968],{"class":627},[129,2498,971],{"class":135},[129,2500,974],{"class":627},[129,2502,2503],{"class":135}," check(data)] ",[129,2505,988],{"class":627},[129,2507,960],{"class":135},[129,2509,2243],{"class":149},[129,2511,274],{"class":135},[129,2513,2514],{"class":149},"\"output.colour\"",[129,2516,274],{"class":135},[129,2518,2316],{"class":149},[129,2520,354],{"class":135},[129,2522,2523],{"class":131,"line":1099},[129,2524,643],{"emptyLinePlaceholder":642},[129,2526,2527],{"class":131,"line":1109},[129,2528,643],{"emptyLinePlaceholder":642},[129,2530,2531,2533,2536],{"class":131,"line":1132},[129,2532,860],{"class":627},[129,2534,2535],{"class":747}," test_broken_toml_is_reported",[129,2537,2538],{"class":135},"(tmp_path):\n",[129,2540,2541,2544,2546,2549,2552],{"class":131,"line":1154},[129,2542,2543],{"class":135},"    path ",[129,2545,758],{"class":627},[129,2547,2548],{"class":135}," tmp_path ",[129,2550,2551],{"class":627},"\u002F",[129,2553,2554],{"class":149}," \"config.toml\"\n",[129,2556,2557,2560,2563,2566,2568],{"class":131,"line":1164},[129,2558,2559],{"class":135},"    path.write_text(",[129,2561,2562],{"class":149},"\"[api",[129,2564,2565],{"class":142},"\\n",[129,2567,821],{"class":149},[129,2569,764],{"class":135},[129,2571,2572,2575,2577],{"class":131,"line":1190},[129,2573,2574],{"class":135},"    [problem] ",[129,2576,758],{"class":627},[129,2578,1623],{"class":135},[129,2580,2581,2583,2586,2589],{"class":131,"line":1200},[129,2582,2142],{"class":627},[129,2584,2585],{"class":149}," \"not valid TOML\"",[129,2587,2588],{"class":627}," in",[129,2590,2591],{"class":135}," problem.message\n",[120,2593,2595],{"className":612,"code":2594,"language":614,"meta":125,"style":125},"# tests\u002Ftest_validate_cmd.py\nimport json\n\nfrom typer.testing import CliRunner\n\nfrom mytool.validate_cmd import app\n\nrunner = CliRunner()\n\n\ndef test_validate_reports_problems_and_exit_code(tmp_path):\n    path = tmp_path \u002F \"config.toml\"\n    path.write_text('[api]\\nurl = \"https:\u002F\u002Fx\"\\ntimeout = 0\\n[output]\\nformat = \"xml\"\\n')\n    result = runner.invoke(app, [\"validate\", str(path)])\n    assert result.exit_code == 1\n    assert result.stderr.count(str(path)) == 2\n\n\ndef test_validate_ok(tmp_path):\n    path = tmp_path \u002F \"config.toml\"\n    path.write_text('[api]\\nurl = \"https:\u002F\u002Fapi.example.com\"\\n')\n    assert runner.invoke(app, [\"validate\", str(path)]).exit_code == 0\n\n\ndef test_schema_is_printed_as_json():\n    result = runner.invoke(app, [\"schema\"])\n    assert json.loads(result.stdout)[\"title\"] == \"mytool configuration\"\n",[14,2596,2597,2602,2608,2612,2624,2628,2640,2644,2654,2658,2662,2671,2683,2717,2737,2748,2765,2769,2773,2782,2794,2811,2831,2835,2839,2848,2861],{"__ignoreMap":125},[129,2598,2599],{"class":131,"line":132},[129,2600,2601],{"class":621},"# tests\u002Ftest_validate_cmd.py\n",[129,2603,2604,2606],{"class":131,"line":139},[129,2605,648],{"class":627},[129,2607,651],{"class":135},[129,2609,2610],{"class":131,"line":156},[129,2611,643],{"emptyLinePlaceholder":642},[129,2613,2614,2616,2619,2621],{"class":131,"line":169},[129,2615,628],{"class":627},[129,2617,2618],{"class":135}," typer.testing ",[129,2620,648],{"class":627},[129,2622,2623],{"class":135}," CliRunner\n",[129,2625,2626],{"class":131,"line":182},[129,2627,643],{"emptyLinePlaceholder":642},[129,2629,2630,2632,2635,2637],{"class":131,"line":195},[129,2631,628],{"class":627},[129,2633,2634],{"class":135}," mytool.validate_cmd ",[129,2636,648],{"class":627},[129,2638,2639],{"class":135}," app\n",[129,2641,2642],{"class":131,"line":208},[129,2643,643],{"emptyLinePlaceholder":642},[129,2645,2646,2649,2651],{"class":131,"line":217},[129,2647,2648],{"class":135},"runner ",[129,2650,758],{"class":627},[129,2652,2653],{"class":135}," CliRunner()\n",[129,2655,2656],{"class":131,"line":225},[129,2657,643],{"emptyLinePlaceholder":642},[129,2659,2660],{"class":131,"line":237},[129,2661,643],{"emptyLinePlaceholder":642},[129,2663,2664,2666,2669],{"class":131,"line":249},[129,2665,860],{"class":627},[129,2667,2668],{"class":747}," test_validate_reports_problems_and_exit_code",[129,2670,2538],{"class":135},[129,2672,2673,2675,2677,2679,2681],{"class":131,"line":257},[129,2674,2543],{"class":135},[129,2676,758],{"class":627},[129,2678,2548],{"class":135},[129,2680,2551],{"class":627},[129,2682,2554],{"class":149},[129,2684,2685,2687,2690,2692,2695,2697,2700,2702,2705,2707,2710,2712,2715],{"class":131,"line":298},[129,2686,2559],{"class":135},[129,2688,2689],{"class":149},"'[api]",[129,2691,2565],{"class":142},[129,2693,2694],{"class":149},"url = \"https:\u002F\u002Fx\"",[129,2696,2565],{"class":142},[129,2698,2699],{"class":149},"timeout = 0",[129,2701,2565],{"class":142},[129,2703,2704],{"class":149},"[output]",[129,2706,2565],{"class":142},[129,2708,2709],{"class":149},"format = \"xml\"",[129,2711,2565],{"class":142},[129,2713,2714],{"class":149},"'",[129,2716,764],{"class":135},[129,2718,2719,2722,2724,2727,2730,2732,2734],{"class":131,"line":336},[129,2720,2721],{"class":135},"    result ",[129,2723,758],{"class":627},[129,2725,2726],{"class":135}," runner.invoke(app, [",[129,2728,2729],{"class":149},"\"validate\"",[129,2731,274],{"class":135},[129,2733,808],{"class":142},[129,2735,2736],{"class":135},"(path)])\n",[129,2738,2739,2741,2744,2746],{"class":131,"line":342},[129,2740,2142],{"class":627},[129,2742,2743],{"class":135}," result.exit_code ",[129,2745,988],{"class":627},[129,2747,2384],{"class":142},[129,2749,2750,2752,2755,2757,2760,2762],{"class":131,"line":357},[129,2751,2142],{"class":627},[129,2753,2754],{"class":135}," result.stderr.count(",[129,2756,808],{"class":142},[129,2758,2759],{"class":135},"(path)) ",[129,2761,988],{"class":627},[129,2763,2764],{"class":142}," 2\n",[129,2766,2767],{"class":131,"line":363},[129,2768,643],{"emptyLinePlaceholder":642},[129,2770,2771],{"class":131,"line":371},[129,2772,643],{"emptyLinePlaceholder":642},[129,2774,2775,2777,2780],{"class":131,"line":382},[129,2776,860],{"class":627},[129,2778,2779],{"class":747}," test_validate_ok",[129,2781,2538],{"class":135},[129,2783,2784,2786,2788,2790,2792],{"class":131,"line":393},[129,2785,2543],{"class":135},[129,2787,758],{"class":627},[129,2789,2548],{"class":135},[129,2791,2551],{"class":627},[129,2793,2554],{"class":149},[129,2795,2796,2798,2800,2802,2805,2807,2809],{"class":131,"line":400},[129,2797,2559],{"class":135},[129,2799,2689],{"class":149},[129,2801,2565],{"class":142},[129,2803,2804],{"class":149},"url = \"https:\u002F\u002Fapi.example.com\"",[129,2806,2565],{"class":142},[129,2808,2714],{"class":149},[129,2810,764],{"class":135},[129,2812,2813,2815,2817,2819,2821,2823,2826,2828],{"class":131,"line":429},[129,2814,2142],{"class":627},[129,2816,2726],{"class":135},[129,2818,2729],{"class":149},[129,2820,274],{"class":135},[129,2822,808],{"class":142},[129,2824,2825],{"class":135},"(path)]).exit_code ",[129,2827,988],{"class":627},[129,2829,2830],{"class":142}," 0\n",[129,2832,2833],{"class":131,"line":446},[129,2834,643],{"emptyLinePlaceholder":642},[129,2836,2837],{"class":131,"line":452},[129,2838,643],{"emptyLinePlaceholder":642},[129,2840,2841,2843,2846],{"class":131,"line":457},[129,2842,860],{"class":627},[129,2844,2845],{"class":747}," test_schema_is_printed_as_json",[129,2847,2098],{"class":135},[129,2849,2850,2852,2854,2856,2859],{"class":131,"line":465},[129,2851,2721],{"class":135},[129,2853,758],{"class":627},[129,2855,2726],{"class":135},[129,2857,2858],{"class":149},"\"schema\"",[129,2860,2351],{"class":135},[129,2862,2863,2865,2868,2871,2874,2876],{"class":131,"line":476},[129,2864,2142],{"class":627},[129,2866,2867],{"class":135}," json.loads(result.stdout)[",[129,2869,2870],{"class":149},"\"title\"",[129,2872,2873],{"class":135},"] ",[129,2875,988],{"class":627},[129,2877,2878],{"class":149}," \"mytool configuration\"\n",[10,2880,2881],{},"The parametrised test doubles as a specification of the messages users see; when someone loosens a rule by accident, a case fails. Keep a valid example config in the tests (or in your documentation, tested from there) so a schema change that rejects real configurations is caught before release.",[54,2883,2885],{"id":2884},"conclusion","Conclusion",[10,2887,2888,2889,2891,2892,2894,2895,2898,2899,42,2901,2903,2904,2907],{},"Describe the configuration file with a JSON Schema shipped inside the package: ",[14,2890,566],{}," to catch typos, ranges and enums for values, ",[14,2893,586],{}," for keys without defaults. Validate with ",[14,2896,2897],{},"Draft202012Validator.iter_errors"," to collect every problem, convert error paths to dotted keys and rewrite the few unfriendly messages, expose ",[14,2900,41],{},[14,2902,45],{}," commands, put a ",[14,2905,2906],{},"#:schema"," directive in generated files so editors autocomplete and check as users type, and test both the schema and the messages.",[54,2909,2911],{"id":2910},"frequently-asked-questions","Frequently asked questions",[108,2913,2915],{"id":2914},"which-json-schema-draft-should-i-use","Which JSON Schema draft should I use?",[10,2917,2918,2919,2921,2922,2925],{},"Draft 2020-12 is current and supported by the ",[14,2920,37],{}," library and the major editor integrations. Declare it with ",[14,2923,2924],{},"$schema"," and use the matching validator class; mixing drafts is a common source of keywords being silently ignored.",[108,2927,2929],{"id":2928},"is-jsonschema-fast-enough-to-run-on-every-invocation","Is jsonschema fast enough to run on every invocation?",[10,2931,2932,2933,52],{},"For a config file of a few dozen keys, validation takes well under a millisecond once the validator exists. The import costs a few tens of milliseconds, so import it lazily inside the validation function if startup time matters, as discussed in ",[48,2934,2936],{"href":2935},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002F","CLI startup performance and lazy loading",[108,2938,2940],{"id":2939},"can-i-report-line-numbers","Can I report line numbers?",[10,2942,2943,2946],{},[14,2944,2945],{},"tomllib"," does not keep positions. tomlkit preserves them internally but does not expose a stable API for it, so most CLIs report dotted key paths, which users can search for. Editors using the schema show the exact position anyway.",[108,2948,2950],{"id":2949},"should-environment-variables-and-flags-be-validated-with-the-same-schema","Should environment variables and flags be validated with the same schema?",[10,2952,2953,2954,2957,2958,2961],{},"Validate the ",[88,2955,2956],{},"merged"," configuration with the same rules, so a bad ",[14,2959,2960],{},"MYTOOL_API_TIMEOUT=-1"," is caught too — but report the source (“from MYTOOL_API_TIMEOUT”) rather than a file path.",[108,2963,2965],{"id":2964},"what-about-defaults","What about defaults?",[10,2967,2968,2969,2972,2973,2975],{},"JSON Schema’s ",[14,2970,2971],{},"default"," keyword is documentation; validators do not fill values in. Apply defaults in the loader (or the settings model) and keep the ",[14,2974,2971],{}," annotations in the schema so editors can show them.",[54,2977,2979],{"id":2978},"related","Related",[59,2981,2982,2988,2993,2998,3004],{},[62,2983,2984,2985],{},"Up: ",[48,2986,2987],{"href":50},"Configuration files and environment variables",[62,2989,2990],{},[48,2991,2992],{"href":1848},"Writing a config init and edit command",[62,2994,2995],{},[48,2996,2997],{"href":1868},"Typed settings with pydantic-settings",[62,2999,3000],{},[48,3001,3003],{"href":3002},"\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",[62,3005,3006],{},[48,3007,3009],{"href":3008},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","Config precedence: flags, env, files and defaults",[3011,3012,3013],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}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 .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}",{"title":125,"searchDepth":139,"depth":139,"links":3015},[3016,3017,3018,3023,3024,3025,3026,3027,3028,3035],{"id":56,"depth":139,"text":57},{"id":78,"depth":139,"text":79},{"id":105,"depth":139,"text":106,"children":3019},[3020,3021,3022],{"id":110,"depth":156,"text":111},{"id":608,"depth":156,"text":609},{"id":1456,"depth":156,"text":1457},{"id":1792,"depth":139,"text":1793},{"id":1861,"depth":139,"text":1862},{"id":1880,"depth":139,"text":1881},{"id":1939,"depth":139,"text":1940},{"id":2884,"depth":139,"text":2885},{"id":2910,"depth":139,"text":2911,"children":3029},[3030,3031,3032,3033,3034],{"id":2914,"depth":156,"text":2915},{"id":2928,"depth":156,"text":2929},{"id":2939,"depth":156,"text":2940},{"id":2949,"depth":156,"text":2950},{"id":2964,"depth":156,"text":2965},{"id":2978,"depth":139,"text":2979},"2026-10-02","Describe a CLI config file once with JSON Schema: report every problem by key path, flag unknown keys, ship the schema for editor completion, and test it.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema",{"title":5,"description":3037},"advanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema\u002Findex",[3046,3047,3048,1801,3049],"configuration","json-schema","validation","editors","q71tGchVIjfp2M0qdc2iz1_EmVL_IkEXuDY6p-Txq1k",[3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3174,3177,3180,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3313,3316,3319,3322,3325,3328,3331,3334,3337,3340,3343,3346,3349,3352,3355,3358,3361,3364,3367,3370,3373,3376,3379,3382,3385,3388,3391,3394,3397,3400,3403,3406,3409,3412,3415,3418,3421,3424,3427,3430,3433,3436,3439,3442,3445,3448,3451,3454,3457,3460,3463,3466,3468,3471,3474,3477,3480,3483,3486,3489,3492,3495,3498,3501,3504,3507,3510,3513,3516,3519,3522,3525,3528,3531,3534,3537,3540,3543,3546,3549,3552,3555,3558,3561,3564,3567,3570,3573,3576,3579,3582,3585,3588,3591,3594,3597,3600,3603,3606,3609,3612,3615,3618,3621,3624,3627,3630,3633,3636,3639,3642,3645,3648,3651,3654,3657,3660,3663,3666,3669,3672,3675,3678,3681,3684,3687,3690,3693,3696,3699,3702,3705,3708,3711,3714,3717,3720,3723,3726,3729,3732,3735,3738,3741,3744,3747,3750,3753,3756,3759,3762,3765,3768,3771,3774,3777,3780,3783,3786,3789,3792,3795,3798,3801,3804,3807,3810,3813,3816,3819,3822,3825,3828,3831,3834,3837,3840,3843,3846,3849,3852,3855,3858,3861,3864,3867,3870,3873,3876,3879,3882,3885,3888,3891],{"path":3053,"title":3054},"\u002Fabout","About Python CLI Toolcraft",{"path":3056,"title":3057},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":3059,"title":3060},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":3062,"title":3063},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":3065,"title":3066},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":3068,"title":3069},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":3071,"title":3072},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":3074,"title":3075},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":3077,"title":3078},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":3080,"title":3081},"\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":3083,"title":3084},"\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":3086,"title":3087},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":3089,"title":3090},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":3092,"title":3093},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":3095,"title":3096},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":3098,"title":3099},"\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":3101,"title":3102},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":3104,"title":3105},"\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":3107,"title":3108},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":3110,"title":3111},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":3113,"title":3114},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":3116,"title":3117},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":3119,"title":3120},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":3122,"title":3123},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":3125,"title":3126},"\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":3128,"title":3129},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":3131,"title":3132},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":3134,"title":3135},"\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":3137,"title":3138},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":3140,"title":3141},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":3143,"title":3144},"\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":3146,"title":3147},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":3149,"title":3150},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":3152,"title":3153},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":3155,"title":3156},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":3158,"title":3159},"\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":3161,"title":3162},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":3164,"title":3165},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":3167,"title":3168},"\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":3170,"title":3171},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":3173,"title":3003},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps",{"path":3175,"title":3176},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":3178,"title":3179},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":3042,"title":5},{"path":3182,"title":3183},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":3185,"title":3186},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3188,"title":3189},"\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":3191,"title":3192},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":3194,"title":3195},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":3197,"title":3198},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3200,"title":3201},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3203,"title":3204},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":3206,"title":3207},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3209,"title":3210},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":3212,"title":3213},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":3215,"title":3216},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":3218,"title":3219},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":3221,"title":3222},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3224,"title":3225},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":3227,"title":3228},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":3230,"title":3231},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3233,"title":3234},"\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":3236,"title":3237},"\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":3239,"title":3240},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3242,"title":3243},"\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":3245,"title":3246},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3248,"title":3249},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3251,"title":3252},"\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":3254,"title":3255},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3257,"title":3258},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3260,"title":3261},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3263,"title":3264},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":3266,"title":3267},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3269,"title":3270},"\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":3272,"title":3273},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3275,"title":3276},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3278,"title":3279},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3281,"title":3282},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3284,"title":3285},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":3287,"title":3288},"\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":3290,"title":3291},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3293,"title":3294},"\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":3296,"title":3297},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3299,"title":3300},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3302,"title":3303},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":3305,"title":3306},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3308,"title":3309},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3311,"title":3312},"\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":3314,"title":3315},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":3317,"title":3318},"\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":3320,"title":3321},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3323,"title":3324},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3326,"title":3327},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3329,"title":3330},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3332,"title":3333},"\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":3335,"title":3336},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":3338,"title":3339},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":3341,"title":3342},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3344,"title":3345},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3347,"title":3348},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":3350,"title":3351},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3353,"title":3354},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3356,"title":3357},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3359,"title":3360},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":3362,"title":3363},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3365,"title":3366},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3368,"title":3369},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":3371,"title":3372},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3374,"title":3375},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":3377,"title":3378},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":3380,"title":3381},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":3383,"title":3384},"\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":3386,"title":3387},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3389,"title":3390},"\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":3392,"title":3393},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3395,"title":3396},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3398,"title":3399},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":3401,"title":3402},"\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":3404,"title":3405},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":3407,"title":3408},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3410,"title":3411},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3413,"title":3414},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3416,"title":3417},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3419,"title":3420},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":3422,"title":3423},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3425,"title":3426},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3428,"title":3429},"\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":3431,"title":3432},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3434,"title":3435},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3437,"title":3438},"\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":3440,"title":3441},"\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":3443,"title":3444},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3446,"title":3447},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3449,"title":3450},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3452,"title":3453},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":3455,"title":3456},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3458,"title":3459},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":3461,"title":3462},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":3464,"title":3465},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":2551,"title":3467},"Python CLI Toolcraft",{"path":3469,"title":3470},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3472,"title":3473},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3475,"title":3476},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3478,"title":3479},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3481,"title":3482},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":3484,"title":3485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":3487,"title":3488},"\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":3490,"title":3491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3493,"title":3494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3496,"title":3497},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3499,"title":3500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3502,"title":3503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3505,"title":3506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3508,"title":3509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3511,"title":3512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":3514,"title":3515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3517,"title":3518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3520,"title":3521},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3523,"title":3524},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":3526,"title":3527},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3529,"title":3530},"\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":3532,"title":3533},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3535,"title":3536},"\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":3538,"title":3539},"\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":3541,"title":3542},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3544,"title":3545},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3547,"title":3548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3550,"title":3551},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3553,"title":3554},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3556,"title":3557},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3559,"title":3560},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3562,"title":3563},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3565,"title":3566},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3568,"title":3569},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3571,"title":3572},"\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":3574,"title":3575},"\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":3577,"title":3578},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3580,"title":3581},"\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":3583,"title":3584},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3586,"title":3587},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":3589,"title":3590},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3592,"title":3593},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3595,"title":3596},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3598,"title":3599},"\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":3601,"title":3602},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3604,"title":3605},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3607,"title":3608},"\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":3610,"title":3611},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3613,"title":3614},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3616,"title":3617},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3619,"title":3620},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3622,"title":3623},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3625,"title":3626},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3628,"title":3629},"\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":3631,"title":3632},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3634,"title":3635},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":3637,"title":3638},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":3640,"title":3641},"\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":3643,"title":3644},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3646,"title":3647},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":3649,"title":3650},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3652,"title":3653},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3655,"title":3656},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3658,"title":3659},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3661,"title":3662},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3664,"title":3665},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":3667,"title":3668},"\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":3670,"title":3671},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":3673,"title":3674},"\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":3676,"title":3677},"\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":3679,"title":3680},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3682,"title":3683},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3685,"title":3686},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3688,"title":3689},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3691,"title":3692},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3694,"title":3695},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3697,"title":3698},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3700,"title":3701},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3703,"title":3704},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3706,"title":3707},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3709,"title":3710},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3712,"title":3713},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3715,"title":3716},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3718,"title":3719},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3721,"title":3722},"\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":3724,"title":3725},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3727,"title":3728},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3730,"title":3731},"\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":3733,"title":3734},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3736,"title":3737},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3739,"title":3740},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3742,"title":3743},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3745,"title":3746},"\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":3748,"title":3749},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3751,"title":3752},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3754,"title":3755},"\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":3757,"title":3758},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3760,"title":3761},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3763,"title":3764},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3766,"title":3767},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3769,"title":3770},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3772,"title":3773},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3775,"title":3776},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3778,"title":3779},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3781,"title":3782},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3784,"title":3785},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3787,"title":3788},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3790,"title":3791},"\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":3793,"title":3794},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3796,"title":3797},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3799,"title":3800},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3802,"title":3803},"\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":3805,"title":3806},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3808,"title":3809},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3811,"title":3812},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3814,"title":3815},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3817,"title":3818},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3820,"title":3821},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3823,"title":3824},"\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":3826,"title":3827},"\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":3829,"title":3830},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3832,"title":3833},"\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":3835,"title":3836},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3838,"title":3839},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3841,"title":3842},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3844,"title":3845},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3847,"title":3848},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3850,"title":3851},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3853,"title":3854},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3856,"title":3857},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3859,"title":3860},"\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":3862,"title":3863},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3865,"title":3866},"\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":3868,"title":3869},"\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":3871,"title":3872},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3874,"title":3875},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3877,"title":3878},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3880,"title":3881},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3883,"title":3884},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3886,"title":3887},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3889,"title":3890},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3892,"title":3893},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537209]