[{"data":1,"prerenderedAt":1981},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F":3,"content-directory":1829},{"id":4,"title":5,"body":6,"date":1813,"description":1814,"difficulty":1815,"draft":1816,"extension":1817,"meta":1818,"navigation":369,"path":1819,"seo":1820,"stem":1821,"tags":1822,"updated":1813,"__hash__":1828},"content\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002Findex.md","Friendly Error Messages and Tracebacks",{"type":7,"value":8,"toc":1802},"minimark",[9,22,27,79,83,86,258,265,337,343,347,350,818,825,1001,1025,1029,1039,1054,1121,1154,1169,1249,1253,1256,1429,1445,1449,1460,1528,1554,1560,1564,1575,1631,1634,1670,1686,1690,1768,1772,1798],[10,11,12,13,17,18,21],"p",{},"When your CLI fails, the user should see one clear sentence telling them what went wrong and how to fix it — not fifteen lines of Python internals ending in ",[14,15,16],"code",{},"FileNotFoundError",". A raw traceback is a debugging tool leaking into a user interface. This guide shows how to catch the errors you expect, write messages people can act on, and keep the full traceback one ",[14,19,20],{},"--debug"," flag away for when you actually need it.",[23,24,26],"h2",{"id":25},"tldr","TL;DR",[28,29,30,34,41,57,64,71],"ul",{},[31,32,33],"li",{},"A raw traceback is bad UX: it exposes internals, buries the real cause, and reads as \"this tool crashed\" rather than \"you gave me a path that doesn't exist.\"",[31,35,36,37,40],{},"Install one top-level ",[14,38,39],{},"try\u002Fexcept"," boundary that maps expected exceptions to a message plus an exit code, and let only genuine bugs fall through.",[31,42,43,44,48,49,52,53,56],{},"Write every error to ",[45,46,47],"strong",{},"stderr",", not stdout, with ",[14,50,51],{},"click.echo(msg, err=True)"," or ",[14,54,55],{},"print(msg, file=sys.stderr)",".",[31,58,59,60,63],{},"Add a ",[14,61,62],{},"--debug\u002F--verbose"," flag that re-raises the full traceback so developers can still see it on demand.",[31,65,66,67,70],{},"Use ",[14,68,69],{},"rich.traceback.install(show_locals=True)"," for gorgeous, readable tracebacks while developing — behind the same debug flag.",[31,72,73,74,78],{},"Style messages as ",[75,76,77],"em",{},"what happened + how to fix it",", so the next action is obvious.",[23,80,82],{"id":81},"why-raw-tracebacks-are-bad-ux","Why raw tracebacks are bad UX",[10,84,85],{},"Here is what an uncaught exception looks like to someone who just wanted to run your tool:",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-bash shiki shiki-themes github-light github-dark","$ mytool build --config app.toml\nTraceback (most recent call last):\n  File \"\u002Fusr\u002Flib\u002Fpython3.11\u002F...\u002Fcli.py\", line 88, in build\n    data = load_config(path)\n  File \"\u002Fusr\u002Flib\u002Fpython3.11\u002F...\u002Fconfig.py\", line 22, in load_config\n    with open(path) as fh:\n         ^^^^^^^^^^\nFileNotFoundError: [Errno 2] No such file or directory: 'app.toml'\n","bash","",[14,94,95,118,140,161,182,200,222,228],{"__ignoreMap":92},[96,97,100,104,108,111,115],"span",{"class":98,"line":99},"line",1,[96,101,103],{"class":102},"sScJk","$",[96,105,107],{"class":106},"sZZnC"," mytool",[96,109,110],{"class":106}," build",[96,112,114],{"class":113},"sj4cs"," --config",[96,116,117],{"class":106}," app.toml\n",[96,119,121,124,128,131,134,137],{"class":98,"line":120},2,[96,122,123],{"class":102},"Traceback",[96,125,127],{"class":126},"sVt8B"," (most ",[96,129,130],{"class":106},"recent",[96,132,133],{"class":106}," call",[96,135,136],{"class":106}," last",[96,138,139],{"class":126},"):\n",[96,141,143,146,149,152,155,158],{"class":98,"line":142},3,[96,144,145],{"class":102},"  File",[96,147,148],{"class":106}," \"\u002Fusr\u002Flib\u002Fpython3.11\u002F...\u002Fcli.py\",",[96,150,151],{"class":106}," line",[96,153,154],{"class":106}," 88,",[96,156,157],{"class":106}," in",[96,159,160],{"class":106}," build\n",[96,162,164,167,170,173,176,179],{"class":98,"line":163},4,[96,165,166],{"class":102},"    data",[96,168,169],{"class":106}," =",[96,171,172],{"class":106}," load_config",[96,174,175],{"class":126},"(",[96,177,178],{"class":102},"path",[96,180,181],{"class":126},")\n",[96,183,185,187,190,192,195,197],{"class":98,"line":184},5,[96,186,145],{"class":102},[96,188,189],{"class":106}," \"\u002Fusr\u002Flib\u002Fpython3.11\u002F...\u002Fconfig.py\",",[96,191,151],{"class":106},[96,193,194],{"class":106}," 22,",[96,196,157],{"class":106},[96,198,199],{"class":106}," load_config\n",[96,201,203,206,209,211,213,216,219],{"class":98,"line":202},6,[96,204,205],{"class":102},"    with",[96,207,208],{"class":106}," open",[96,210,175],{"class":126},[96,212,178],{"class":102},[96,214,215],{"class":126},") ",[96,217,218],{"class":106},"as",[96,220,221],{"class":106}," fh:\n",[96,223,225],{"class":98,"line":224},7,[96,226,227],{"class":102},"         ^^^^^^^^^^\n",[96,229,231,234,237,240,243,246,249,252,255],{"class":98,"line":230},8,[96,232,233],{"class":102},"FileNotFoundError:",[96,235,236],{"class":126}," [Errno ",[96,238,239],{"class":106},"2]",[96,241,242],{"class":106}," No",[96,244,245],{"class":106}," such",[96,247,248],{"class":106}," file",[96,250,251],{"class":106}," or",[96,253,254],{"class":106}," directory:",[96,256,257],{"class":106}," 'app.toml'\n",[10,259,260,261,264],{},"Three problems. First, the ",[75,262,263],{},"actual"," issue — the file isn't there — is on the last line, after a stack the user can do nothing with. Second, it exposes your internal file layout and function names, which is noise at best and an information leak at worst. Third, it reads as a crash: the user assumes your tool is broken, not that they typed the wrong path. Compare:",[87,266,268],{"className":89,"code":267,"language":91,"meta":92,"style":92},"$ mytool build --config app.toml\nerror: config file not found: app.toml\n  hint: create it with 'mytool init' or pass --config \u003Cpath>\n",[14,269,270,282,300],{"__ignoreMap":92},[96,271,272,274,276,278,280],{"class":98,"line":99},[96,273,103],{"class":102},[96,275,107],{"class":106},[96,277,110],{"class":106},[96,279,114],{"class":113},[96,281,117],{"class":106},[96,283,284,287,290,292,295,298],{"class":98,"line":120},[96,285,286],{"class":102},"error:",[96,288,289],{"class":106}," config",[96,291,248],{"class":106},[96,293,294],{"class":106}," not",[96,296,297],{"class":106}," found:",[96,299,117],{"class":106},[96,301,302,305,308,311,314,317,319,322,324,328,331,334],{"class":98,"line":142},[96,303,304],{"class":102},"  hint:",[96,306,307],{"class":106}," create",[96,309,310],{"class":106}," it",[96,312,313],{"class":106}," with",[96,315,316],{"class":106}," 'mytool init'",[96,318,251],{"class":106},[96,320,321],{"class":106}," pass",[96,323,114],{"class":113},[96,325,327],{"class":326},"szBVR"," \u003C",[96,329,330],{"class":106},"pat",[96,332,333],{"class":126},"h",[96,335,336],{"class":326},">\n",[10,338,339,340,342],{},"Same failure, but now the user knows exactly what to do. The traceback isn't gone — it's just moved behind ",[14,341,20],{},", where the person who can use it will look.",[23,344,346],{"id":345},"a-top-level-error-boundary","A top-level error boundary",[10,348,349],{},"The cleanest way to guarantee no command leaks a traceback is to catch everything in one place: a boundary wrapping your entry point. Individual commands raise; the boundary decides how failure is presented.",[87,351,355],{"className":352,"code":353,"language":354,"meta":92,"style":92},"language-python shiki shiki-themes github-light github-dark","import sys\n\nclass CLIError(Exception):\n    \"\"\"An anticipated failure. `hint` is optional next-step guidance.\"\"\"\n\n    def __init__(self, message: str, *, code: int = 1, hint: str | None = None):\n        super().__init__(message)\n        self.code = code\n        self.hint = hint\n\n\ndef main(argv: list[str] | None = None) -> int:\n    args = parse_args(argv)\n    try:\n        run(args)\n        return 0\n    except CLIError as exc:\n        print(f\"error: {exc}\", file=sys.stderr)\n        if exc.hint:\n            print(f\"  hint: {exc.hint}\", file=sys.stderr)\n        return exc.code\n    except KeyboardInterrupt:\n        print(\"aborted\", file=sys.stderr)\n        return 130\n    except Exception as exc:                 # an unanticipated bug\n        if args.debug:\n            raise                            # full traceback for developers\n        print(f\"internal error: {exc}\", file=sys.stderr)\n        print(\"  run again with --debug for a full traceback\", file=sys.stderr)\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","python",[14,356,357,365,371,386,391,395,443,457,471,484,489,494,528,539,547,553,562,576,613,622,652,660,670,688,696,714,722,731,759,777,785,790,795,812],{"__ignoreMap":92},[96,358,359,362],{"class":98,"line":99},[96,360,361],{"class":326},"import",[96,363,364],{"class":126}," sys\n",[96,366,367],{"class":98,"line":120},[96,368,370],{"emptyLinePlaceholder":369},true,"\n",[96,372,373,376,379,381,384],{"class":98,"line":142},[96,374,375],{"class":326},"class",[96,377,378],{"class":102}," CLIError",[96,380,175],{"class":126},[96,382,383],{"class":113},"Exception",[96,385,139],{"class":126},[96,387,388],{"class":98,"line":163},[96,389,390],{"class":106},"    \"\"\"An anticipated failure. `hint` is optional next-step guidance.\"\"\"\n",[96,392,393],{"class":98,"line":184},[96,394,370],{"emptyLinePlaceholder":369},[96,396,397,400,403,406,409,412,415,418,421,423,426,429,431,434,437,439,441],{"class":98,"line":202},[96,398,399],{"class":326},"    def",[96,401,402],{"class":113}," __init__",[96,404,405],{"class":126},"(self, message: ",[96,407,408],{"class":113},"str",[96,410,411],{"class":126},", ",[96,413,414],{"class":326},"*",[96,416,417],{"class":126},", code: ",[96,419,420],{"class":113},"int",[96,422,169],{"class":326},[96,424,425],{"class":113}," 1",[96,427,428],{"class":126},", hint: ",[96,430,408],{"class":113},[96,432,433],{"class":326}," |",[96,435,436],{"class":113}," None",[96,438,169],{"class":326},[96,440,436],{"class":113},[96,442,139],{"class":126},[96,444,445,448,451,454],{"class":98,"line":224},[96,446,447],{"class":113},"        super",[96,449,450],{"class":126},"().",[96,452,453],{"class":113},"__init__",[96,455,456],{"class":126},"(message)\n",[96,458,459,462,465,468],{"class":98,"line":230},[96,460,461],{"class":113},"        self",[96,463,464],{"class":126},".code ",[96,466,467],{"class":326},"=",[96,469,470],{"class":126}," code\n",[96,472,474,476,479,481],{"class":98,"line":473},9,[96,475,461],{"class":113},[96,477,478],{"class":126},".hint ",[96,480,467],{"class":326},[96,482,483],{"class":126}," hint\n",[96,485,487],{"class":98,"line":486},10,[96,488,370],{"emptyLinePlaceholder":369},[96,490,492],{"class":98,"line":491},11,[96,493,370],{"emptyLinePlaceholder":369},[96,495,497,500,503,506,508,511,514,516,518,520,523,525],{"class":98,"line":496},12,[96,498,499],{"class":326},"def",[96,501,502],{"class":102}," main",[96,504,505],{"class":126},"(argv: list[",[96,507,408],{"class":113},[96,509,510],{"class":126},"] ",[96,512,513],{"class":326},"|",[96,515,436],{"class":113},[96,517,169],{"class":326},[96,519,436],{"class":113},[96,521,522],{"class":126},") -> ",[96,524,420],{"class":113},[96,526,527],{"class":126},":\n",[96,529,531,534,536],{"class":98,"line":530},13,[96,532,533],{"class":126},"    args ",[96,535,467],{"class":326},[96,537,538],{"class":126}," parse_args(argv)\n",[96,540,542,545],{"class":98,"line":541},14,[96,543,544],{"class":326},"    try",[96,546,527],{"class":126},[96,548,550],{"class":98,"line":549},15,[96,551,552],{"class":126},"        run(args)\n",[96,554,556,559],{"class":98,"line":555},16,[96,557,558],{"class":326},"        return",[96,560,561],{"class":113}," 0\n",[96,563,565,568,571,573],{"class":98,"line":564},17,[96,566,567],{"class":326},"    except",[96,569,570],{"class":126}," CLIError ",[96,572,218],{"class":326},[96,574,575],{"class":126}," exc:\n",[96,577,579,582,584,587,590,593,596,599,602,604,608,610],{"class":98,"line":578},18,[96,580,581],{"class":113},"        print",[96,583,175],{"class":126},[96,585,586],{"class":326},"f",[96,588,589],{"class":106},"\"error: ",[96,591,592],{"class":113},"{",[96,594,595],{"class":126},"exc",[96,597,598],{"class":113},"}",[96,600,601],{"class":106},"\"",[96,603,411],{"class":126},[96,605,607],{"class":606},"s4XuR","file",[96,609,467],{"class":326},[96,611,612],{"class":126},"sys.stderr)\n",[96,614,616,619],{"class":98,"line":615},19,[96,617,618],{"class":326},"        if",[96,620,621],{"class":126}," exc.hint:\n",[96,623,625,628,630,632,635,637,640,642,644,646,648,650],{"class":98,"line":624},20,[96,626,627],{"class":113},"            print",[96,629,175],{"class":126},[96,631,586],{"class":326},[96,633,634],{"class":106},"\"  hint: ",[96,636,592],{"class":113},[96,638,639],{"class":126},"exc.hint",[96,641,598],{"class":113},[96,643,601],{"class":106},[96,645,411],{"class":126},[96,647,607],{"class":606},[96,649,467],{"class":326},[96,651,612],{"class":126},[96,653,655,657],{"class":98,"line":654},21,[96,656,558],{"class":326},[96,658,659],{"class":126}," exc.code\n",[96,661,663,665,668],{"class":98,"line":662},22,[96,664,567],{"class":326},[96,666,667],{"class":113}," KeyboardInterrupt",[96,669,527],{"class":126},[96,671,673,675,677,680,682,684,686],{"class":98,"line":672},23,[96,674,581],{"class":113},[96,676,175],{"class":126},[96,678,679],{"class":106},"\"aborted\"",[96,681,411],{"class":126},[96,683,607],{"class":606},[96,685,467],{"class":326},[96,687,612],{"class":126},[96,689,691,693],{"class":98,"line":690},24,[96,692,558],{"class":326},[96,694,695],{"class":113}," 130\n",[96,697,699,701,704,707,710],{"class":98,"line":698},25,[96,700,567],{"class":326},[96,702,703],{"class":113}," Exception",[96,705,706],{"class":326}," as",[96,708,709],{"class":126}," exc:                 ",[96,711,713],{"class":712},"sJ8bj","# an unanticipated bug\n",[96,715,717,719],{"class":98,"line":716},26,[96,718,618],{"class":326},[96,720,721],{"class":126}," args.debug:\n",[96,723,725,728],{"class":98,"line":724},27,[96,726,727],{"class":326},"            raise",[96,729,730],{"class":712},"                            # full traceback for developers\n",[96,732,734,736,738,740,743,745,747,749,751,753,755,757],{"class":98,"line":733},28,[96,735,581],{"class":113},[96,737,175],{"class":126},[96,739,586],{"class":326},[96,741,742],{"class":106},"\"internal error: ",[96,744,592],{"class":113},[96,746,595],{"class":126},[96,748,598],{"class":113},[96,750,601],{"class":106},[96,752,411],{"class":126},[96,754,607],{"class":606},[96,756,467],{"class":326},[96,758,612],{"class":126},[96,760,762,764,766,769,771,773,775],{"class":98,"line":761},29,[96,763,581],{"class":113},[96,765,175],{"class":126},[96,767,768],{"class":106},"\"  run again with --debug for a full traceback\"",[96,770,411],{"class":126},[96,772,607],{"class":606},[96,774,467],{"class":326},[96,776,612],{"class":126},[96,778,780,782],{"class":98,"line":779},30,[96,781,558],{"class":326},[96,783,784],{"class":113}," 1\n",[96,786,788],{"class":98,"line":787},31,[96,789,370],{"emptyLinePlaceholder":369},[96,791,793],{"class":98,"line":792},32,[96,794,370],{"emptyLinePlaceholder":369},[96,796,798,801,804,807,810],{"class":98,"line":797},33,[96,799,800],{"class":326},"if",[96,802,803],{"class":113}," __name__",[96,805,806],{"class":326}," ==",[96,808,809],{"class":106}," \"__main__\"",[96,811,527],{"class":126},[96,813,815],{"class":98,"line":814},34,[96,816,817],{"class":126},"    sys.exit(main())\n",[10,819,820,821,824],{},"The pattern that makes this scale: deep in your code, translate low-level exceptions into ",[14,822,823],{},"CLIError"," with a good message at the point where you have the most context.",[87,826,828],{"className":352,"code":827,"language":354,"meta":92,"style":92},"def load_config(path: str) -> dict:\n    try:\n        with open(path, \"rb\") as fh:\n            return tomllib.load(fh)\n    except FileNotFoundError:\n        raise CLIError(\n            f\"config file not found: {path}\",\n            code=66,                          # EX_NOINPUT\n            hint=\"create it with 'mytool init' or pass --config \u003Cpath>\",\n        )\n    except tomllib.TOMLDecodeError as exc:\n        raise CLIError(f\"config file {path} is not valid TOML: {exc}\", code=65)\n",[14,829,830,848,854,873,881,890,898,917,933,945,950,961],{"__ignoreMap":92},[96,831,832,834,836,839,841,843,846],{"class":98,"line":99},[96,833,499],{"class":326},[96,835,172],{"class":102},[96,837,838],{"class":126},"(path: ",[96,840,408],{"class":113},[96,842,522],{"class":126},[96,844,845],{"class":113},"dict",[96,847,527],{"class":126},[96,849,850,852],{"class":98,"line":120},[96,851,544],{"class":326},[96,853,527],{"class":126},[96,855,856,859,861,864,867,869,871],{"class":98,"line":142},[96,857,858],{"class":326},"        with",[96,860,208],{"class":113},[96,862,863],{"class":126},"(path, ",[96,865,866],{"class":106},"\"rb\"",[96,868,215],{"class":126},[96,870,218],{"class":326},[96,872,221],{"class":126},[96,874,875,878],{"class":98,"line":163},[96,876,877],{"class":326},"            return",[96,879,880],{"class":126}," tomllib.load(fh)\n",[96,882,883,885,888],{"class":98,"line":184},[96,884,567],{"class":326},[96,886,887],{"class":113}," FileNotFoundError",[96,889,527],{"class":126},[96,891,892,895],{"class":98,"line":202},[96,893,894],{"class":326},"        raise",[96,896,897],{"class":126}," CLIError(\n",[96,899,900,903,906,908,910,912,914],{"class":98,"line":224},[96,901,902],{"class":326},"            f",[96,904,905],{"class":106},"\"config file not found: ",[96,907,592],{"class":113},[96,909,178],{"class":126},[96,911,598],{"class":113},[96,913,601],{"class":106},[96,915,916],{"class":126},",\n",[96,918,919,922,924,927,930],{"class":98,"line":230},[96,920,921],{"class":606},"            code",[96,923,467],{"class":326},[96,925,926],{"class":113},"66",[96,928,929],{"class":126},",                          ",[96,931,932],{"class":712},"# EX_NOINPUT\n",[96,934,935,938,940,943],{"class":98,"line":473},[96,936,937],{"class":606},"            hint",[96,939,467],{"class":326},[96,941,942],{"class":106},"\"create it with 'mytool init' or pass --config \u003Cpath>\"",[96,944,916],{"class":126},[96,946,947],{"class":98,"line":486},[96,948,949],{"class":126},"        )\n",[96,951,952,954,957,959],{"class":98,"line":491},[96,953,567],{"class":326},[96,955,956],{"class":126}," tomllib.TOMLDecodeError ",[96,958,218],{"class":326},[96,960,575],{"class":126},[96,962,963,965,968,970,973,975,977,979,982,984,986,988,990,992,994,996,999],{"class":98,"line":496},[96,964,894],{"class":326},[96,966,967],{"class":126}," CLIError(",[96,969,586],{"class":326},[96,971,972],{"class":106},"\"config file ",[96,974,592],{"class":113},[96,976,178],{"class":126},[96,978,598],{"class":113},[96,980,981],{"class":106}," is not valid TOML: ",[96,983,592],{"class":113},[96,985,595],{"class":126},[96,987,598],{"class":113},[96,989,601],{"class":106},[96,991,411],{"class":126},[96,993,14],{"class":606},[96,995,467],{"class":326},[96,997,998],{"class":113},"65",[96,1000,181],{"class":126},[10,1002,1003,1004,1007,1008,1010,1011,1013,1014,411,1016,1018,1019,1024],{},"Everything you ",[75,1005,1006],{},"expected"," becomes a tidy ",[14,1009,823],{},"; everything you didn't stays a real exception and gets the traceback (under ",[14,1012,20],{},") it deserves. The exit codes here (",[14,1015,926],{},[14,1017,998],{},") come from the ",[1020,1021,1023],"a",{"href":1022},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools"," conventions — pair the two guides so your messages and your codes tell a consistent story.",[23,1026,1028],{"id":1027},"always-write-errors-to-stderr","Always write errors to stderr",[10,1030,1031,1032,1034,1035,1038],{},"Errors go to ",[45,1033,47],{},", never stdout. Stdout is reserved for the tool's real output — the data a downstream program consumes. If an error lands on stdout, ",[14,1036,1037],{},"mytool export | jq ."," breaks because your error sentence is now sitting inside the JSON stream.",[10,1040,1041,1042,1045,1046,1049,1050,1053],{},"With plain Python, that's the ",[14,1043,1044],{},"file=sys.stderr"," argument you saw above. In ",[45,1047,1048],{},"Click",", use ",[14,1051,1052],{},"err=True",":",[87,1055,1057],{"className":352,"code":1056,"language":354,"meta":92,"style":92},"import click\n\nclick.echo(\"error: registry unreachable\", err=True)\nclick.secho(\"error: registry unreachable\", err=True, fg=\"red\")   # colored\n",[14,1058,1059,1066,1070,1090],{"__ignoreMap":92},[96,1060,1061,1063],{"class":98,"line":99},[96,1062,361],{"class":326},[96,1064,1065],{"class":126}," click\n",[96,1067,1068],{"class":98,"line":120},[96,1069,370],{"emptyLinePlaceholder":369},[96,1071,1072,1075,1078,1080,1083,1085,1088],{"class":98,"line":142},[96,1073,1074],{"class":126},"click.echo(",[96,1076,1077],{"class":106},"\"error: registry unreachable\"",[96,1079,411],{"class":126},[96,1081,1082],{"class":606},"err",[96,1084,467],{"class":326},[96,1086,1087],{"class":113},"True",[96,1089,181],{"class":126},[96,1091,1092,1095,1097,1099,1101,1103,1105,1107,1110,1112,1115,1118],{"class":98,"line":163},[96,1093,1094],{"class":126},"click.secho(",[96,1096,1077],{"class":106},[96,1098,411],{"class":126},[96,1100,1082],{"class":606},[96,1102,467],{"class":326},[96,1104,1087],{"class":113},[96,1106,411],{"class":126},[96,1108,1109],{"class":606},"fg",[96,1111,467],{"class":326},[96,1113,1114],{"class":106},"\"red\"",[96,1116,1117],{"class":126},")   ",[96,1119,1120],{"class":712},"# colored\n",[10,1122,1123,1126,1127,1130,1131,1134,1135,52,1138,1141,1142,52,1145,1148,1149,1153],{},[14,1124,1125],{},"click.secho"," adds color, and Click strips the color codes automatically when stderr is not a TTY (a pipe or a log file), so you never get ",[14,1128,1129],{},"\\x1b[31m"," garbage in your CI logs. In ",[45,1132,1133],{},"Typer",", the same call works via ",[14,1136,1137],{},"typer.echo(msg, err=True)",[14,1139,1140],{},"typer.secho(...)",", and raising ",[14,1143,1144],{},"typer.BadParameter",[14,1146,1147],{},"typer.Exit(code=...)"," routes through Typer's own stderr handling. If you're deciding between the two frameworks, ",[1020,1150,1152],{"href":1151},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click"," compares their error ergonomics directly.",[10,1155,1156,1157,1160,1161,1164,1165,1168],{},"Click also gives you ",[14,1158,1159],{},"ClickException"," as a built-in version of the boundary above: raise it anywhere and Click prints ",[14,1162,1163],{},"Error: \u003Cmessage>"," to stderr and exits ",[14,1166,1167],{},"1"," for you.",[87,1170,1172],{"className":352,"code":1171,"language":354,"meta":92,"style":92},"@click.command()\n@click.option(\"--config\", \"config_path\")\ndef build(config_path: str) -> None:\n    if not os.path.exists(config_path):\n        raise click.ClickException(f\"config file not found: {config_path}\")\n",[14,1173,1174,1182,1199,1217,1227],{"__ignoreMap":92},[96,1175,1176,1179],{"class":98,"line":99},[96,1177,1178],{"class":102},"@click.command",[96,1180,1181],{"class":126},"()\n",[96,1183,1184,1187,1189,1192,1194,1197],{"class":98,"line":120},[96,1185,1186],{"class":102},"@click.option",[96,1188,175],{"class":126},[96,1190,1191],{"class":106},"\"--config\"",[96,1193,411],{"class":126},[96,1195,1196],{"class":106},"\"config_path\"",[96,1198,181],{"class":126},[96,1200,1201,1203,1205,1208,1210,1212,1215],{"class":98,"line":142},[96,1202,499],{"class":326},[96,1204,110],{"class":102},[96,1206,1207],{"class":126},"(config_path: ",[96,1209,408],{"class":113},[96,1211,522],{"class":126},[96,1213,1214],{"class":113},"None",[96,1216,527],{"class":126},[96,1218,1219,1222,1224],{"class":98,"line":163},[96,1220,1221],{"class":326},"    if",[96,1223,294],{"class":326},[96,1225,1226],{"class":126}," os.path.exists(config_path):\n",[96,1228,1229,1231,1234,1236,1238,1240,1243,1245,1247],{"class":98,"line":184},[96,1230,894],{"class":326},[96,1232,1233],{"class":126}," click.ClickException(",[96,1235,586],{"class":326},[96,1237,905],{"class":106},[96,1239,592],{"class":113},[96,1241,1242],{"class":126},"config_path",[96,1244,598],{"class":113},[96,1246,601],{"class":106},[96,1248,181],{"class":126},[23,1250,1252],{"id":1251},"a-debug-flag-that-re-raises-the-traceback","A --debug flag that re-raises the traceback",[10,1254,1255],{},"Hiding the traceback for users must not mean losing it for maintainers. A single global flag toggles between the two audiences. With Click, make it an eager, expose-nothing option stored on the context:",[87,1257,1259],{"className":352,"code":1258,"language":354,"meta":92,"style":92},"import click\n\n@click.group()\n@click.option(\"--debug\", is_flag=True, help=\"Show full tracebacks on error.\")\n@click.pass_context\ndef cli(ctx: click.Context, debug: bool) -> None:\n    ctx.obj = {\"debug\": debug}\n\n\n@cli.command()\n@click.pass_context\ndef build(ctx: click.Context) -> None:\n    try:\n        run_build()\n    except Exception:\n        if ctx.obj[\"debug\"]:\n            raise                         # Click prints the full traceback\n        raise click.ClickException(\"build failed; re-run with --debug for details\")\n",[14,1260,1261,1267,1271,1278,1308,1313,1332,1348,1352,1356,1363,1367,1380,1386,1391,1399,1411,1418],{"__ignoreMap":92},[96,1262,1263,1265],{"class":98,"line":99},[96,1264,361],{"class":326},[96,1266,1065],{"class":126},[96,1268,1269],{"class":98,"line":120},[96,1270,370],{"emptyLinePlaceholder":369},[96,1272,1273,1276],{"class":98,"line":142},[96,1274,1275],{"class":102},"@click.group",[96,1277,1181],{"class":126},[96,1279,1280,1282,1284,1287,1289,1292,1294,1296,1298,1301,1303,1306],{"class":98,"line":163},[96,1281,1186],{"class":102},[96,1283,175],{"class":126},[96,1285,1286],{"class":106},"\"--debug\"",[96,1288,411],{"class":126},[96,1290,1291],{"class":606},"is_flag",[96,1293,467],{"class":326},[96,1295,1087],{"class":113},[96,1297,411],{"class":126},[96,1299,1300],{"class":606},"help",[96,1302,467],{"class":326},[96,1304,1305],{"class":106},"\"Show full tracebacks on error.\"",[96,1307,181],{"class":126},[96,1309,1310],{"class":98,"line":184},[96,1311,1312],{"class":102},"@click.pass_context\n",[96,1314,1315,1317,1320,1323,1326,1328,1330],{"class":98,"line":202},[96,1316,499],{"class":326},[96,1318,1319],{"class":102}," cli",[96,1321,1322],{"class":126},"(ctx: click.Context, debug: ",[96,1324,1325],{"class":113},"bool",[96,1327,522],{"class":126},[96,1329,1214],{"class":113},[96,1331,527],{"class":126},[96,1333,1334,1337,1339,1342,1345],{"class":98,"line":224},[96,1335,1336],{"class":126},"    ctx.obj ",[96,1338,467],{"class":326},[96,1340,1341],{"class":126}," {",[96,1343,1344],{"class":106},"\"debug\"",[96,1346,1347],{"class":126},": debug}\n",[96,1349,1350],{"class":98,"line":230},[96,1351,370],{"emptyLinePlaceholder":369},[96,1353,1354],{"class":98,"line":473},[96,1355,370],{"emptyLinePlaceholder":369},[96,1357,1358,1361],{"class":98,"line":486},[96,1359,1360],{"class":102},"@cli.command",[96,1362,1181],{"class":126},[96,1364,1365],{"class":98,"line":491},[96,1366,1312],{"class":102},[96,1368,1369,1371,1373,1376,1378],{"class":98,"line":496},[96,1370,499],{"class":326},[96,1372,110],{"class":102},[96,1374,1375],{"class":126},"(ctx: click.Context) -> ",[96,1377,1214],{"class":113},[96,1379,527],{"class":126},[96,1381,1382,1384],{"class":98,"line":530},[96,1383,544],{"class":326},[96,1385,527],{"class":126},[96,1387,1388],{"class":98,"line":541},[96,1389,1390],{"class":126},"        run_build()\n",[96,1392,1393,1395,1397],{"class":98,"line":549},[96,1394,567],{"class":326},[96,1396,703],{"class":113},[96,1398,527],{"class":126},[96,1400,1401,1403,1406,1408],{"class":98,"line":555},[96,1402,618],{"class":326},[96,1404,1405],{"class":126}," ctx.obj[",[96,1407,1344],{"class":106},[96,1409,1410],{"class":126},"]:\n",[96,1412,1413,1415],{"class":98,"line":564},[96,1414,727],{"class":326},[96,1416,1417],{"class":712},"                         # Click prints the full traceback\n",[96,1419,1420,1422,1424,1427],{"class":98,"line":578},[96,1421,894],{"class":326},[96,1423,1233],{"class":126},[96,1425,1426],{"class":106},"\"build failed; re-run with --debug for details\"",[96,1428,181],{"class":126},[10,1430,1431,1432,1435,1436,1440,1441,1444],{},"Sharing that ",[14,1433,1434],{},"debug"," flag across every subcommand via the context object is exactly the pattern in ",[1020,1437,1439],{"href":1438},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects\u002F","sharing state with Click context objects",". An environment variable — ",[14,1442,1443],{},"MYTOOL_DEBUG=1"," — makes a good second toggle for CI, where adding a flag to every invocation is awkward.",[23,1446,1448],{"id":1447},"rich-tracebacks-for-development","Rich tracebacks for development",[10,1450,1451,1452,1455,1456,1459],{},"When you ",[75,1453,1454],{},"do"," want the traceback, a plain one is still hard to read. ",[14,1457,1458],{},"rich.traceback.install()"," replaces Python's default handler with a syntax-highlighted, framed version that shows local variables at each frame:",[87,1461,1463],{"className":352,"code":1462,"language":354,"meta":92,"style":92},"from rich.traceback import install\n\ndef enable_debug() -> None:\n    install(show_locals=True, width=120, suppress=[click])\n",[14,1464,1465,1478,1482,1496],{"__ignoreMap":92},[96,1466,1467,1470,1473,1475],{"class":98,"line":99},[96,1468,1469],{"class":326},"from",[96,1471,1472],{"class":126}," rich.traceback ",[96,1474,361],{"class":326},[96,1476,1477],{"class":126}," install\n",[96,1479,1480],{"class":98,"line":120},[96,1481,370],{"emptyLinePlaceholder":369},[96,1483,1484,1486,1489,1492,1494],{"class":98,"line":142},[96,1485,499],{"class":326},[96,1487,1488],{"class":102}," enable_debug",[96,1490,1491],{"class":126},"() -> ",[96,1493,1214],{"class":113},[96,1495,527],{"class":126},[96,1497,1498,1501,1504,1506,1508,1510,1513,1515,1518,1520,1523,1525],{"class":98,"line":163},[96,1499,1500],{"class":126},"    install(",[96,1502,1503],{"class":606},"show_locals",[96,1505,467],{"class":326},[96,1507,1087],{"class":113},[96,1509,411],{"class":126},[96,1511,1512],{"class":606},"width",[96,1514,467],{"class":326},[96,1516,1517],{"class":113},"120",[96,1519,411],{"class":126},[96,1521,1522],{"class":606},"suppress",[96,1524,467],{"class":326},[96,1526,1527],{"class":126},"[click])\n",[10,1529,1530,1531,1534,1535,1537,1538,1541,1542,1544,1545,1548,1549,1553],{},"Call ",[14,1532,1533],{},"enable_debug()"," only when ",[14,1536,20],{}," is set, so users never see it. ",[14,1539,1540],{},"show_locals=True"," prints the value of every local at each frame — invaluable for \"how did ",[14,1543,178],{}," end up empty?\" — and ",[14,1546,1547],{},"suppress=[click]"," hides frames from framework internals so the trace stays focused on your code. Rich is also what powers the progress bars and tables in ",[1020,1550,1552],{"href":1551},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI with Rich",", so if you already depend on it, this is free.",[10,1555,1556,1557,1559],{},"One caution: ",[14,1558,1540],{}," can dump secrets (tokens, passwords) sitting in local variables. Keep it behind the debug flag and never enable it in a mode that ships logs off the machine.",[23,1561,1563],{"id":1562},"writing-actionable-messages","Writing actionable messages",[10,1565,1566,1567,1570,1571,1574],{},"A good error message answers two questions: ",[75,1568,1569],{},"what happened"," and ",[75,1572,1573],{},"what do I do now",". Most tracebacks answer neither; most bad hand-written messages answer only the first.",[1576,1577,1578,1591],"table",{},[1579,1580,1581],"thead",{},[1582,1583,1584,1588],"tr",{},[1585,1586,1587],"th",{},"Weak",[1585,1589,1590],{},"Actionable",[1592,1593,1594,1607,1619],"tbody",{},[1582,1595,1596,1602],{},[1597,1598,1599],"td",{},[14,1600,1601],{},"Error: invalid input",[1597,1603,1604],{},[14,1605,1606],{},"error: --replicas must be a positive integer, got '-3'",[1582,1608,1609,1614],{},[1597,1610,1611],{},[14,1612,1613],{},"Error: 404",[1597,1615,1616],{},[14,1617,1618],{},"error: image 'app:v9' not found in registry; check the tag with 'mytool images'",[1582,1620,1621,1626],{},[1597,1622,1623],{},[14,1624,1625],{},"Permission denied",[1597,1627,1628],{},[14,1629,1630],{},"error: cannot write \u002Fetc\u002Fmytool.conf: permission denied; try sudo or --config ~\u002F.config\u002Fmytool.conf",[10,1632,1633],{},"Three rules that make messages land:",[1635,1636,1637,1647,1657],"ol",{},[31,1638,1639,1642,1643,1646],{},[45,1640,1641],{},"Name the thing."," Include the value, the path, the flag — the specific noun that failed. \"config not found\" is weaker than \"config not found: ",[14,1644,1645],{},"app.toml","\".",[31,1648,1649,1652,1653,1656],{},[45,1650,1651],{},"Suggest the fix."," A ",[14,1654,1655],{},"hint:"," line with the next command turns a dead end into a step. Users copy-paste hints.",[31,1658,1659,1662,1663,1666,1667,1669],{},[45,1660,1661],{},"Match the user's vocabulary."," Say ",[14,1664,1665],{},"--config",", the flag they typed — not ",[14,1668,1242],{},", your internal variable name. The message lives in their world, not your code's.",[10,1671,1672,1673,1677,1678,1681,1682,1685],{},"The same principle drives validation errors at the argument boundary; ",[1020,1674,1676],{"href":1675},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002F","advanced argument validation strategies"," shows how to turn a Pydantic ",[14,1679,1680],{},"ValidationError"," into a field-addressed, exit-",[14,1683,1684],{},"2"," message in the same house style.",[23,1687,1689],{"id":1688},"production-notes","Production notes",[28,1691,1692,1714,1727,1740,1757],{},[31,1693,1694,1700,1701,1570,1704,1707,1708,1710,1711,1713],{},[45,1695,1696,1697,56],{},"Never bare ",[14,1698,1699],{},"except:"," It swallows ",[14,1702,1703],{},"SystemExit",[14,1705,1706],{},"KeyboardInterrupt",", so your exit codes and Ctrl-C stop working. Catch ",[14,1709,383],{}," at the boundary, and let ",[14,1712,1706],{}," have its own arm.",[31,1715,1716,1719,1720,1723,1724,1726],{},[45,1717,1718],{},"Preserve the cause when translating."," ",[14,1721,1722],{},"raise CLIError(...) from exc"," keeps the chain so ",[14,1725,20],{}," still shows the original exception underneath yours.",[31,1728,1729,1732,1733,1736,1737,1739],{},[45,1730,1731],{},"Chained exceptions in tracebacks."," With Rich installed, a ",[14,1734,1735],{},"raise ... from ..."," renders both frames clearly; without a ",[14,1738,1469],{},", Python appends \"During handling of the above exception,\" which confuses users — so translate deliberately.",[31,1741,1742,1719,1745,1748,1749,1752,1753,1756],{},[45,1743,1744],{},"Broken pipes.",[14,1746,1747],{},"mytool | head"," closes the pipe early and can raise ",[14,1750,1751],{},"BrokenPipeError"," deep in a ",[14,1754,1755],{},"print",". Catch it near the boundary and exit quietly instead of printing a traceback about it.",[31,1758,1759,1762,1763,1767],{},[45,1760,1761],{},"Structured diagnostics."," Human-readable stderr messages and machine-readable logs are different channels. If you emit both, keep the friendly message for the user and route structured detail through logging — see ",[1020,1764,1766],{"href":1765},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging for CLI apps",", which keeps logs on stderr and off your data stream.",[23,1769,1771],{"id":1770},"related","Related",[28,1773,1774,1781,1787,1792],{},[31,1775,1776,1777],{},"Up: ",[1020,1778,1780],{"href":1779},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","Error handling and exit codes for CLIs",[31,1782,1783,1784],{},"Sideways: ",[1020,1785,1786],{"href":1022},"Choosing exit codes for CLI tools",[31,1788,1783,1789],{},[1020,1790,1791],{"href":1765},"Structured logging for CLI apps",[31,1793,1794,1795],{},"Related: ",[1020,1796,1797],{"href":1675},"Advanced argument validation strategies",[1799,1800,1801],"style",{},"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 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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":92,"searchDepth":120,"depth":120,"links":1803},[1804,1805,1806,1807,1808,1809,1810,1811,1812],{"id":25,"depth":120,"text":26},{"id":81,"depth":120,"text":82},{"id":345,"depth":120,"text":346},{"id":1027,"depth":120,"text":1028},{"id":1251,"depth":120,"text":1252},{"id":1447,"depth":120,"text":1448},{"id":1562,"depth":120,"text":1563},{"id":1688,"depth":120,"text":1689},{"id":1770,"depth":120,"text":1771},"2026-07-05","Replace Python tracebacks with clear CLI errors: catch expected exceptions, write actionable messages to stderr, and keep tracebacks behind --debug.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks",{"title":5,"description":1814},"advanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002Findex",[1823,1824,1825,1826,1827],"errors","exit-codes","cli","click","typer","gqVM3ZYfc8cXPZ3hEZ0Mq1jiAjO_Siuv-0jJWKVuCHg",[1830,1833,1836,1839,1842,1843,1846,1849,1852,1855,1858,1861,1864,1867,1870,1873,1876,1879,1882,1885,1888,1891,1894,1897,1900,1903,1906,1909,1912,1915,1918,1921,1924,1927,1930,1933,1936,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978],{"path":1831,"title":1832},"\u002Fabout","About Python CLI Toolcraft",{"path":1834,"title":1835},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1837,"title":1838},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1840,"title":1841},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1819,"title":5},{"path":1844,"title":1845},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1847,"title":1848},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1850,"title":1851},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1853,"title":1854},"\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":1856,"title":1857},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1859,"title":1860},"\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":1862,"title":1863},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1865,"title":1866},"\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":1868,"title":1869},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1871,"title":1872},"\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":1874,"title":1875},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1877,"title":1878},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1880,"title":1881},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1883,"title":1884},"\u002F","Python CLI Toolcraft",{"path":1886,"title":1887},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1889,"title":1890},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1892,"title":1893},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1895,"title":1896},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1898,"title":1899},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1901,"title":1902},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1904,"title":1905},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1907,"title":1908},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1910,"title":1911},"\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":1913,"title":1914},"\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":1916,"title":1917},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1919,"title":1920},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1922,"title":1923},"\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":1925,"title":1926},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1928,"title":1929},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1931,"title":1932},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1934,"title":1935},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1937,"title":1938},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1940,"title":1941},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1943,"title":1944},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1946,"title":1947},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1949,"title":1950},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1952,"title":1953},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1955,"title":1956},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1958,"title":1959},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1961,"title":1962},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1964,"title":1965},"\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":1967,"title":1968},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1970,"title":1971},"\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":1973,"title":1974},"\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":1976,"title":1977},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1979,"title":1980},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867195]