[{"data":1,"prerenderedAt":1427},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F":3,"content-directory":1276},{"id":4,"title":5,"body":6,"date":1260,"description":1261,"difficulty":1262,"draft":1263,"extension":1264,"meta":1265,"navigation":219,"path":1266,"seo":1267,"stem":1268,"tags":1269,"updated":1260,"__hash__":1275},"content\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Findex.md","Error Handling and Exit Codes for CLIs",{"type":7,"value":8,"toc":1250},"minimark",[9,13,18,106,110,114,129,178,195,198,326,348,352,355,380,389,402,566,578,582,590,638,654,658,661,689,710,807,825,843,847,850,1101,1121,1125,1211,1215,1246],[10,11,12],"p",{},"A command-line tool is judged as much by how it fails as by how it succeeds. When something goes wrong, a script piping into your CLI, a CI job gating a deploy, or a human at 2 a.m. all need the same three things: a truthful exit code, a message that says what to do next, and no wall of Python internals. This overview shows how to design failure on purpose — classifying errors, keeping stdout and stderr honest, and installing one error boundary so every command exits cleanly.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,61,76,99],"ul",{},[22,23,24,25,29],"li",{},"Exit code ",[26,27,28],"code",{},"0"," means success; anything non-zero means failure. That number is the only part of your output a shell reads, so treat it as your CLI's real API.",[22,31,32,33,37,38,41,42,45,46,49,50,53,54,56,57,60],{},"Sort every failure into three buckets: ",[34,35,36],"strong",{},"usage errors"," (bad invocation, exit ",[26,39,40],{},"2","), ",[34,43,44],{},"expected runtime errors"," (file missing, network down — exit ",[26,47,48],{},"1"," or a specific code), and ",[34,51,52],{},"unexpected bugs"," (exit ",[26,55,48],{},", print a traceback only under ",[26,58,59],{},"--debug",").",[22,62,63,64,67,68,71,72,75],{},"Send results to ",[34,65,66],{},"stdout",", send diagnostics and errors to ",[34,69,70],{},"stderr",", so ",[26,73,74],{},"mytool | jq"," never chokes on a warning.",[22,77,78,79,82,83,86,87,90,91,94,95,98],{},"Signal failure with the right tool: ",[26,80,81],{},"raise SystemExit(code)",", ",[26,84,85],{},"raise typer.Exit(code=...)",", or ",[26,88,89],{},"raise click.ClickException(msg)"," — not ",[26,92,93],{},"print()"," plus ",[26,96,97],{},"sys.exit",".",[22,100,101,102,105],{},"Wrap ",[26,103,104],{},"main()"," in one top-level error boundary that maps exceptions to messages and codes, so no command leaks a raw traceback.",[107,108],"inline-diagram",{"name":109},"exit-code-flow",[14,111,113],{"id":112},"exit-codes-are-the-clis-api-for-scripts","Exit codes are the CLI's API for scripts",[10,115,116,117,120,121,124,125,128],{},"Humans read your error text. Machines read your exit code, and nothing else. The moment your tool is used in a pipeline — ",[26,118,119],{},"mytool build && mytool deploy",", a ",[26,122,123],{},"Makefile"," target, a GitHub Actions step — the surrounding shell decides what happens next purely from ",[26,126,127],{},"$?",", the status of the last command.",[130,131,136],"pre",{"className":132,"code":133,"language":134,"meta":135,"style":135},"language-bash shiki shiki-themes github-light github-dark","$ mytool deploy --env prod\n$ echo $?\n0\n","bash","",[26,137,138,161,172],{"__ignoreMap":135},[139,140,143,147,151,154,158],"span",{"class":141,"line":142},"line",1,[139,144,146],{"class":145},"sScJk","$",[139,148,150],{"class":149},"sZZnC"," mytool",[139,152,153],{"class":149}," deploy",[139,155,157],{"class":156},"sj4cs"," --env",[139,159,160],{"class":149}," prod\n",[139,162,164,166,169],{"class":141,"line":163},2,[139,165,146],{"class":145},[139,167,168],{"class":149}," echo",[139,170,171],{"class":156}," $?\n",[139,173,175],{"class":141,"line":174},3,[139,176,177],{"class":145},"0\n",[10,179,180,181,184,185,187,188,191,192,194],{},"That ",[26,182,183],{},"&&"," chain only advances when the left side exits ",[26,186,28],{},". If your tool prints ",[26,189,190],{},"Error: could not connect"," to the screen but still exits ",[26,193,28],{},", the deploy proceeds on a lie. Getting the number right is not a nicety; it is the contract every automation depends on.",[10,196,197],{},"A minimal, honest program looks like this:",[130,199,203],{"className":200,"code":201,"language":202,"meta":135,"style":135},"language-python shiki shiki-themes github-light github-dark","import sys\n\ndef main() -> int:\n    if not config_exists():\n        print(\"error: no config found; run 'mytool init' first\", file=sys.stderr)\n        return 1\n    run()\n    return 0\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n","python",[26,204,205,215,221,238,250,274,283,289,298,303,320],{"__ignoreMap":135},[139,206,207,211],{"class":141,"line":142},[139,208,210],{"class":209},"szBVR","import",[139,212,214],{"class":213},"sVt8B"," sys\n",[139,216,217],{"class":141,"line":163},[139,218,220],{"emptyLinePlaceholder":219},true,"\n",[139,222,223,226,229,232,235],{"class":141,"line":174},[139,224,225],{"class":209},"def",[139,227,228],{"class":145}," main",[139,230,231],{"class":213},"() -> ",[139,233,234],{"class":156},"int",[139,236,237],{"class":213},":\n",[139,239,241,244,247],{"class":141,"line":240},4,[139,242,243],{"class":209},"    if",[139,245,246],{"class":209}," not",[139,248,249],{"class":213}," config_exists():\n",[139,251,253,256,259,262,264,268,271],{"class":141,"line":252},5,[139,254,255],{"class":156},"        print",[139,257,258],{"class":213},"(",[139,260,261],{"class":149},"\"error: no config found; run 'mytool init' first\"",[139,263,82],{"class":213},[139,265,267],{"class":266},"s4XuR","file",[139,269,270],{"class":209},"=",[139,272,273],{"class":213},"sys.stderr)\n",[139,275,277,280],{"class":141,"line":276},6,[139,278,279],{"class":209},"        return",[139,281,282],{"class":156}," 1\n",[139,284,286],{"class":141,"line":285},7,[139,287,288],{"class":213},"    run()\n",[139,290,292,295],{"class":141,"line":291},8,[139,293,294],{"class":209},"    return",[139,296,297],{"class":156}," 0\n",[139,299,301],{"class":141,"line":300},9,[139,302,220],{"emptyLinePlaceholder":219},[139,304,306,309,312,315,318],{"class":141,"line":305},10,[139,307,308],{"class":209},"if",[139,310,311],{"class":156}," __name__",[139,313,314],{"class":209}," ==",[139,316,317],{"class":149}," \"__main__\"",[139,319,237],{"class":213},[139,321,323],{"class":141,"line":322},11,[139,324,325],{"class":213},"    sys.exit(main())\n",[10,327,328,329,331,332,334,335,338,339,342,343,98],{},"Returning an ",[26,330,234],{}," from ",[26,333,104],{}," and handing it to ",[26,336,337],{},"sys.exit()"," keeps the exit logic in one place and makes the function trivially testable. Which specific numbers to use — and when the richer ",[26,340,341],{},"sysexits.h"," codes earn their keep — is its own topic; see ",[344,345,347],"a",{"href":346},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",[14,349,351],{"id":350},"a-failure-taxonomy-usage-expected-unexpected","A failure taxonomy: usage, expected, unexpected",[10,353,354],{},"Every failure your CLI can hit falls into one of three categories, and each wants different handling.",[10,356,357,360,361,364,365,367,368,372,373,376,377,379],{},[34,358,359],{},"Usage errors"," are the caller's fault at the invocation level: an unknown flag, a missing required argument, a value that fails validation. The user needs to fix the command line and retry. Both ",[26,362,363],{},"argparse"," and Click already exit ",[26,366,40],{}," for these and print a short usage hint, so match that convention. Argument-level validation belongs here — see ",[344,369,371],{"href":370},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002F","advanced argument validation strategies"," for turning a ",[26,374,375],{},"ValidationError"," into a clean exit-",[26,378,40],{}," message.",[10,381,382,385,386,388],{},[34,383,384],{},"Expected runtime errors"," are conditions your code anticipates but cannot prevent: a file that isn't there, a network timeout, a permission denied, an API returning 409. These are not bugs — they are the world being the world. Catch them, print a one-line explanation, and exit non-zero (commonly ",[26,387,48],{},", or a specific code so scripts can branch).",[10,390,391,394,395,120,398,401],{},[34,392,393],{},"Unexpected bugs"," are the case you did not foresee: an ",[26,396,397],{},"AttributeError",[26,399,400],{},"KeyError"," deep in your own logic. Here a traceback is genuinely useful — but only to whoever can fix the code. For everyone else it is noise that hides the real message. The answer is to keep the traceback available behind a flag and show a calm one-liner by default.",[130,403,405],{"className":200,"code":404,"language":202,"meta":135,"style":135},"class ExpectedError(Exception):\n    \"\"\"A failure we anticipated; message is safe to show the user.\"\"\"\n\ndef load_project(path: str) -> dict:\n    try:\n        with open(path, encoding=\"utf-8\") as fh:\n            return parse(fh.read())\n    except FileNotFoundError:\n        raise ExpectedError(f\"project file not found: {path}\")\n    except PermissionError:\n        raise ExpectedError(f\"cannot read {path}: permission denied\")\n",[26,406,407,423,428,432,453,460,488,496,506,535,544],{"__ignoreMap":135},[139,408,409,412,415,417,420],{"class":141,"line":142},[139,410,411],{"class":209},"class",[139,413,414],{"class":145}," ExpectedError",[139,416,258],{"class":213},[139,418,419],{"class":156},"Exception",[139,421,422],{"class":213},"):\n",[139,424,425],{"class":141,"line":163},[139,426,427],{"class":149},"    \"\"\"A failure we anticipated; message is safe to show the user.\"\"\"\n",[139,429,430],{"class":141,"line":174},[139,431,220],{"emptyLinePlaceholder":219},[139,433,434,436,439,442,445,448,451],{"class":141,"line":240},[139,435,225],{"class":209},[139,437,438],{"class":145}," load_project",[139,440,441],{"class":213},"(path: ",[139,443,444],{"class":156},"str",[139,446,447],{"class":213},") -> ",[139,449,450],{"class":156},"dict",[139,452,237],{"class":213},[139,454,455,458],{"class":141,"line":252},[139,456,457],{"class":209},"    try",[139,459,237],{"class":213},[139,461,462,465,468,471,474,476,479,482,485],{"class":141,"line":276},[139,463,464],{"class":209},"        with",[139,466,467],{"class":156}," open",[139,469,470],{"class":213},"(path, ",[139,472,473],{"class":266},"encoding",[139,475,270],{"class":209},[139,477,478],{"class":149},"\"utf-8\"",[139,480,481],{"class":213},") ",[139,483,484],{"class":209},"as",[139,486,487],{"class":213}," fh:\n",[139,489,490,493],{"class":141,"line":285},[139,491,492],{"class":209},"            return",[139,494,495],{"class":213}," parse(fh.read())\n",[139,497,498,501,504],{"class":141,"line":291},[139,499,500],{"class":209},"    except",[139,502,503],{"class":156}," FileNotFoundError",[139,505,237],{"class":213},[139,507,508,511,514,517,520,523,526,529,532],{"class":141,"line":300},[139,509,510],{"class":209},"        raise",[139,512,513],{"class":213}," ExpectedError(",[139,515,516],{"class":209},"f",[139,518,519],{"class":149},"\"project file not found: ",[139,521,522],{"class":156},"{",[139,524,525],{"class":213},"path",[139,527,528],{"class":156},"}",[139,530,531],{"class":149},"\"",[139,533,534],{"class":213},")\n",[139,536,537,539,542],{"class":141,"line":305},[139,538,500],{"class":209},[139,540,541],{"class":156}," PermissionError",[139,543,237],{"class":213},[139,545,546,548,550,552,555,557,559,561,564],{"class":141,"line":322},[139,547,510],{"class":209},[139,549,513],{"class":213},[139,551,516],{"class":209},[139,553,554],{"class":149},"\"cannot read ",[139,556,522],{"class":156},[139,558,525],{"class":213},[139,560,528],{"class":156},[139,562,563],{"class":149},": permission denied\"",[139,565,534],{"class":213},[10,567,568,569,572,573,577],{},"The discipline is to raise your own ",[26,570,571],{},"ExpectedError"," for the anticipated cases and let everything else bubble up as a genuine bug. The ",[344,574,576],{"href":575},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages and tracebacks"," guide builds this into a full boundary.",[14,579,581],{"id":580},"keep-stdout-and-stderr-honest","Keep stdout and stderr honest",[10,583,584,585,589],{},"The single most common CLI hygiene bug is writing errors to stdout. Stdout is for your tool's ",[586,587,588],"em",{},"output"," — the JSON, the table, the value another program will consume. Stderr is for everything else: errors, warnings, progress, prompts.",[130,591,593],{"className":200,"code":592,"language":202,"meta":135,"style":135},"import sys\n\nprint(json.dumps(result))                      # data → stdout\nprint(\"warning: cache stale, refetching\", file=sys.stderr)  # noise → stderr\n",[26,594,595,601,605,617],{"__ignoreMap":135},[139,596,597,599],{"class":141,"line":142},[139,598,210],{"class":209},[139,600,214],{"class":213},[139,602,603],{"class":141,"line":163},[139,604,220],{"emptyLinePlaceholder":219},[139,606,607,610,613],{"class":141,"line":174},[139,608,609],{"class":156},"print",[139,611,612],{"class":213},"(json.dumps(result))                      ",[139,614,616],{"class":615},"sJ8bj","# data → stdout\n",[139,618,619,621,623,626,628,630,632,635],{"class":141,"line":240},[139,620,609],{"class":156},[139,622,258],{"class":213},[139,624,625],{"class":149},"\"warning: cache stale, refetching\"",[139,627,82],{"class":213},[139,629,267],{"class":266},[139,631,270],{"class":209},[139,633,634],{"class":213},"sys.stderr)  ",[139,636,637],{"class":615},"# noise → stderr\n",[10,639,640,641,644,645,648,649,653],{},"Why it matters: users pipe your data into other tools. If a warning lands on stdout, ",[26,642,643],{},"mytool export | jq ."," fails to parse because a human sentence is now sitting in the middle of the JSON stream. Keeping diagnostics on stderr means the pipe stays clean while the human still sees the message on their terminal. This separation also lets someone run ",[26,646,647],{},"mytool export > out.json"," and still watch progress and errors scroll past live. Structured diagnostics deserve the same care; the ",[344,650,652],{"href":651},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging for CLI apps"," section covers routing logs to stderr so they never pollute your data channel.",[14,655,657],{"id":656},"signalling-failure-systemexit-exit-and-clickexception","Signalling failure: SystemExit, Exit, and ClickException",[10,659,660],{},"Python gives you several ways to end a program, and the difference matters.",[10,662,663,666,667,670,671,674,675,677,678,681,682,684,685,688],{},[26,664,665],{},"sys.exit(code)"," raises ",[26,668,669],{},"SystemExit",", which unwinds the stack (running ",[26,672,673],{},"finally"," blocks and context managers) before the interpreter exits with ",[26,676,26],{},". Because it is an exception, a stray ",[26,679,680],{},"except Exception:"," can swallow it — so catch ",[26,683,419],{},", never bare ",[26,686,687],{},"except:",", if you want exits to work.",[10,690,691,692,695,696,699,700,703,704,706,707,709],{},"In ",[34,693,694],{},"Click",", prefer ",[26,697,698],{},"raise click.ClickException(message)",". Click catches it, prints ",[26,701,702],{},"Error: message"," to stderr, and exits ",[26,705,48],{}," automatically — no manual ",[26,708,97],{}," needed:",[130,711,713],{"className":200,"code":712,"language":202,"meta":135,"style":135},"import click\n\n@click.command()\n@click.argument(\"name\")\ndef greet(name: str) -> None:\n    if not name.isascii():\n        raise click.ClickException(\"name must be ASCII\")\n    click.echo(f\"hello {name}\")\n",[26,714,715,722,726,734,746,765,774,786],{"__ignoreMap":135},[139,716,717,719],{"class":141,"line":142},[139,718,210],{"class":209},[139,720,721],{"class":213}," click\n",[139,723,724],{"class":141,"line":163},[139,725,220],{"emptyLinePlaceholder":219},[139,727,728,731],{"class":141,"line":174},[139,729,730],{"class":145},"@click.command",[139,732,733],{"class":213},"()\n",[139,735,736,739,741,744],{"class":141,"line":240},[139,737,738],{"class":145},"@click.argument",[139,740,258],{"class":213},[139,742,743],{"class":149},"\"name\"",[139,745,534],{"class":213},[139,747,748,750,753,756,758,760,763],{"class":141,"line":252},[139,749,225],{"class":209},[139,751,752],{"class":145}," greet",[139,754,755],{"class":213},"(name: ",[139,757,444],{"class":156},[139,759,447],{"class":213},[139,761,762],{"class":156},"None",[139,764,237],{"class":213},[139,766,767,769,771],{"class":141,"line":276},[139,768,243],{"class":209},[139,770,246],{"class":209},[139,772,773],{"class":213}," name.isascii():\n",[139,775,776,778,781,784],{"class":141,"line":285},[139,777,510],{"class":209},[139,779,780],{"class":213}," click.ClickException(",[139,782,783],{"class":149},"\"name must be ASCII\"",[139,785,534],{"class":213},[139,787,788,791,793,796,798,801,803,805],{"class":141,"line":291},[139,789,790],{"class":213},"    click.echo(",[139,792,516],{"class":209},[139,794,795],{"class":149},"\"hello ",[139,797,522],{"class":156},[139,799,800],{"class":213},"name",[139,802,528],{"class":156},[139,804,531],{"class":149},[139,806,534],{"class":213},[10,808,809,810,813,814,817,818,821,822,824],{},"Subclass ",[26,811,812],{},"ClickException"," and override ",[26,815,816],{},"exit_code"," to change the number, or raise ",[26,819,820],{},"click.UsageError"," to get exit ",[26,823,40],{}," with a usage hint.",[10,826,691,827,82,830,833,834,837,838,842],{},[34,828,829],{},"Typer",[26,831,832],{},"raise typer.Exit(code=1)"," ends the command with that code, and ",[26,835,836],{},"typer.BadParameter"," gives you the usage-error path. Typer and Click share the same underlying machinery, so the mental model carries across both. If you are choosing between the frameworks, ",[344,839,841],{"href":840},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click"," compares them head to head.",[14,844,846],{"id":845},"one-top-level-error-boundary","One top-level error boundary",[10,848,849],{},"Tie it together with a single boundary around your entry point. Every command flows through it, so no individual command has to remember to handle failure:",[130,851,853],{"className":200,"code":852,"language":202,"meta":135,"style":135},"import sys\n\ndef main(argv: list[str] | None = None) -> int:\n    args = parse_args(argv)\n    try:\n        run(args)\n        return 0\n    except ExpectedError as exc:\n        print(f\"error: {exc}\", file=sys.stderr)\n        return 1\n    except KeyboardInterrupt:\n        print(\"aborted\", file=sys.stderr)\n        return 130          # 128 + SIGINT(2)\n    except Exception as exc:         # a real bug\n        if args.debug:\n            raise                    # full traceback for developers\n        print(f\"internal error: {exc} (run with --debug for details)\", file=sys.stderr)\n        return 1\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",[26,854,855,861,865,896,906,912,917,923,935,963,969,978,996,1007,1024,1033,1042,1071,1078,1083,1096],{"__ignoreMap":135},[139,856,857,859],{"class":141,"line":142},[139,858,210],{"class":209},[139,860,214],{"class":213},[139,862,863],{"class":141,"line":163},[139,864,220],{"emptyLinePlaceholder":219},[139,866,867,869,871,874,876,879,882,885,888,890,892,894],{"class":141,"line":174},[139,868,225],{"class":209},[139,870,228],{"class":145},[139,872,873],{"class":213},"(argv: list[",[139,875,444],{"class":156},[139,877,878],{"class":213},"] ",[139,880,881],{"class":209},"|",[139,883,884],{"class":156}," None",[139,886,887],{"class":209}," =",[139,889,884],{"class":156},[139,891,447],{"class":213},[139,893,234],{"class":156},[139,895,237],{"class":213},[139,897,898,901,903],{"class":141,"line":240},[139,899,900],{"class":213},"    args ",[139,902,270],{"class":209},[139,904,905],{"class":213}," parse_args(argv)\n",[139,907,908,910],{"class":141,"line":252},[139,909,457],{"class":209},[139,911,237],{"class":213},[139,913,914],{"class":141,"line":276},[139,915,916],{"class":213},"        run(args)\n",[139,918,919,921],{"class":141,"line":285},[139,920,279],{"class":209},[139,922,297],{"class":156},[139,924,925,927,930,932],{"class":141,"line":291},[139,926,500],{"class":209},[139,928,929],{"class":213}," ExpectedError ",[139,931,484],{"class":209},[139,933,934],{"class":213}," exc:\n",[139,936,937,939,941,943,946,948,951,953,955,957,959,961],{"class":141,"line":300},[139,938,255],{"class":156},[139,940,258],{"class":213},[139,942,516],{"class":209},[139,944,945],{"class":149},"\"error: ",[139,947,522],{"class":156},[139,949,950],{"class":213},"exc",[139,952,528],{"class":156},[139,954,531],{"class":149},[139,956,82],{"class":213},[139,958,267],{"class":266},[139,960,270],{"class":209},[139,962,273],{"class":213},[139,964,965,967],{"class":141,"line":305},[139,966,279],{"class":209},[139,968,282],{"class":156},[139,970,971,973,976],{"class":141,"line":322},[139,972,500],{"class":209},[139,974,975],{"class":156}," KeyboardInterrupt",[139,977,237],{"class":213},[139,979,981,983,985,988,990,992,994],{"class":141,"line":980},12,[139,982,255],{"class":156},[139,984,258],{"class":213},[139,986,987],{"class":149},"\"aborted\"",[139,989,82],{"class":213},[139,991,267],{"class":266},[139,993,270],{"class":209},[139,995,273],{"class":213},[139,997,999,1001,1004],{"class":141,"line":998},13,[139,1000,279],{"class":209},[139,1002,1003],{"class":156}," 130",[139,1005,1006],{"class":615},"          # 128 + SIGINT(2)\n",[139,1008,1010,1012,1015,1018,1021],{"class":141,"line":1009},14,[139,1011,500],{"class":209},[139,1013,1014],{"class":156}," Exception",[139,1016,1017],{"class":209}," as",[139,1019,1020],{"class":213}," exc:         ",[139,1022,1023],{"class":615},"# a real bug\n",[139,1025,1027,1030],{"class":141,"line":1026},15,[139,1028,1029],{"class":209},"        if",[139,1031,1032],{"class":213}," args.debug:\n",[139,1034,1036,1039],{"class":141,"line":1035},16,[139,1037,1038],{"class":209},"            raise",[139,1040,1041],{"class":615},"                    # full traceback for developers\n",[139,1043,1045,1047,1049,1051,1054,1056,1058,1060,1063,1065,1067,1069],{"class":141,"line":1044},17,[139,1046,255],{"class":156},[139,1048,258],{"class":213},[139,1050,516],{"class":209},[139,1052,1053],{"class":149},"\"internal error: ",[139,1055,522],{"class":156},[139,1057,950],{"class":213},[139,1059,528],{"class":156},[139,1061,1062],{"class":149}," (run with --debug for details)\"",[139,1064,82],{"class":213},[139,1066,267],{"class":266},[139,1068,270],{"class":209},[139,1070,273],{"class":213},[139,1072,1074,1076],{"class":141,"line":1073},18,[139,1075,279],{"class":209},[139,1077,282],{"class":156},[139,1079,1081],{"class":141,"line":1080},19,[139,1082,220],{"emptyLinePlaceholder":219},[139,1084,1086,1088,1090,1092,1094],{"class":141,"line":1085},20,[139,1087,308],{"class":209},[139,1089,311],{"class":156},[139,1091,314],{"class":209},[139,1093,317],{"class":149},[139,1095,237],{"class":213},[139,1097,1099],{"class":141,"line":1098},21,[139,1100,325],{"class":213},[10,1102,1103,1104,1106,1107,1110,1111,1114,1115,1117,1118,1120],{},"Three things earn their place here: ",[26,1105,571],{}," becomes a tidy one-liner, ",[26,1108,1109],{},"KeyboardInterrupt"," exits ",[26,1112,1113],{},"130"," (the shell convention for a Ctrl-C'd process) instead of dumping a traceback, and genuine bugs stay quiet unless ",[26,1116,59],{}," is set. Click and Typer give you most of this for free — but a hand-rolled ",[26,1119,363],{}," CLI needs the boundary written out, and even framework apps benefit from wrapping unexpected exceptions.",[14,1122,1124],{"id":1123},"production-notes","Production notes",[19,1126,1127,1148,1168,1182,1198],{},[22,1128,1129,1132,1133,1136,1137,1140,1141,1144,1145,1147],{},[34,1130,1131],{},"Test the number, not just the text."," Assert exit codes in CI. Click's ",[26,1134,1135],{},"CliRunner"," exposes ",[26,1138,1139],{},"result.exit_code","; for a subprocess, check ",[26,1142,1143],{},"completed.returncode",". A tool that prints the right error but exits ",[26,1146,28],{}," will silently break pipelines.",[22,1149,1150,1157,1158,1160,1161,1163,1164,1167],{},[34,1151,1152,1154,1155,98],{},[26,1153,673],{}," still runs on ",[26,1156,669],{}," Temp-file cleanup and lock release in a ",[26,1159,673],{}," block or context manager execute during a normal ",[26,1162,97],{},", but are skipped on ",[26,1165,1166],{},"os._exit()"," — never use the latter to bail out.",[22,1169,1170,1173,1174,1177,1178,1181],{},[34,1171,1172],{},"Broken pipes."," When a downstream consumer closes early (",[26,1175,1176],{},"mytool | head","), Python may raise ",[26,1179,1180],{},"BrokenPipeError",". Catch it near your boundary and exit quietly rather than printing a traceback.",[22,1183,1184,1187,1188,1190,1191,1193,1194,1197],{},[34,1185,1186],{},"Windows."," Exit codes are 32-bit there and signal-based codes like ",[26,1189,1113],{}," are a Unix convention; keep your meaningful codes in the ",[26,1192,28],{},"–",[26,1195,1196],{},"125"," range for portability.",[22,1199,1200,1203,1204,1207,1208,1210],{},[34,1201,1202],{},"Never exceed 255."," Exit codes wrap modulo 256, so ",[26,1205,1206],{},"sys.exit(256)"," becomes ",[26,1209,28],{}," — a silent success. Keep custom codes small and reserved values clear.",[14,1212,1214],{"id":1213},"related","Related",[19,1216,1217,1224,1230,1235,1241],{},[22,1218,1219,1220],{},"Up: ",[344,1221,1223],{"href":1222},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing for Python CLIs",[22,1225,1226,1227],{},"Down: ",[344,1228,1229],{"href":346},"Choosing exit codes for CLI tools",[22,1231,1226,1232],{},[344,1233,1234],{"href":575},"Friendly error messages and tracebacks",[22,1236,1237,1238],{},"Sideways: ",[344,1239,1240],{"href":370},"Advanced argument validation strategies",[22,1242,1237,1243],{},[344,1244,1245],{"href":651},"Structured logging for CLI apps",[1247,1248,1249],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":135,"searchDepth":163,"depth":163,"links":1251},[1252,1253,1254,1255,1256,1257,1258,1259],{"id":16,"depth":163,"text":17},{"id":112,"depth":163,"text":113},{"id":350,"depth":163,"text":351},{"id":580,"depth":163,"text":581},{"id":656,"depth":163,"text":657},{"id":845,"depth":163,"text":846},{"id":1123,"depth":163,"text":1124},{"id":1213,"depth":163,"text":1214},"2026-07-05","Design predictable Python CLI failure: meaningful exit codes, clean error messages instead of tracebacks, and errors that scripts and CI can detect.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes",{"title":5,"description":1261},"advanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Findex",[1270,1271,1272,1273,1274],"errors","exit-codes","cli","click","typer","0VnggvEDcFjBvrXW8FGZz3m7ny9z7YbVeNoG6QwIzNo",[1277,1280,1283,1286,1289,1292,1293,1296,1299,1302,1304,1307,1310,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1370,1373,1376,1379,1382,1385,1388,1391,1394,1397,1400,1403,1406,1409,1412,1415,1418,1421,1424],{"path":1278,"title":1279},"\u002Fabout","About Python CLI Toolcraft",{"path":1281,"title":1282},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1284,"title":1285},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1287,"title":1288},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1290,"title":1291},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1266,"title":5},{"path":1294,"title":1295},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1297,"title":1298},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1300,"title":1301},"\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":1303,"title":1223},"\u002Fadvanced-input-parsing-user-experience",{"path":1305,"title":1306},"\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":1308,"title":1309},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1311,"title":1312},"\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":1314,"title":1315},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1317,"title":1318},"\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":1320,"title":1321},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1323,"title":1324},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1326,"title":1327},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1329,"title":1330},"\u002F","Python CLI Toolcraft",{"path":1332,"title":1333},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1335,"title":1336},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1338,"title":1339},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1341,"title":1342},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1344,"title":1345},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1347,"title":1348},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1350,"title":1351},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1353,"title":1354},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1356,"title":1357},"\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":1359,"title":1360},"\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":1362,"title":1363},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1365,"title":1366},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1368,"title":1369},"\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":1371,"title":1372},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1374,"title":1375},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1377,"title":1378},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1380,"title":1381},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1383,"title":1384},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1386,"title":1387},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1389,"title":1390},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1392,"title":1393},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1395,"title":1396},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1398,"title":1399},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1401,"title":1402},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1404,"title":1405},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1407,"title":1408},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1410,"title":1411},"\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":1413,"title":1414},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1416,"title":1417},"\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":1419,"title":1420},"\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":1422,"title":1423},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1425,"title":1426},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867195]