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