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