[{"data":1,"prerenderedAt":1239},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002F":3,"content-directory":991},{"id":4,"title":5,"body":6,"date":975,"description":976,"difficulty":977,"draft":978,"extension":979,"meta":980,"navigation":157,"path":981,"seo":982,"stem":983,"tags":984,"updated":975,"__hash__":990},"content\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002Findex.md","Handling Broken Pipe and SIGPIPE",{"type":7,"value":8,"toc":953},"minimark",[9,21,26,65,69,81,85,89,96,112,115,119,122,269,281,292,296,299,323,420,430,434,437,468,471,482,485,489,496,499,544,548,557,567,577,581,584,814,821,827,831,834,838,843,856,860,863,867,873,877,880,884,890,894,901,905,910,914,949],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"mytool list | head -n 10"," is an ordinary thing to type, and for many Python CLIs it produces a\n",[13,18,19],{},"BrokenPipeError"," traceback followed by a second warning at shutdown. Nothing went wrong — the\ndownstream program got what it needed and left — but the output says otherwise.",[22,23,25],"h2",{"id":24},"tldr","TL;DR",[27,28,29,36,44,51,58],"ul",{},[30,31,32,33,35],"li",{},"When a downstream reader exits, your next write raises ",[13,34,19],{},".",[30,37,38,39,43],{},"Catch it ",[40,41,42],"strong",{},"at the boundary",", not in each command.",[30,45,46,47,50],{},"Redirect ",[13,48,49],{},"stdout"," to devnull before exiting, or Python prints a second warning during shutdown.",[30,52,53,54,57],{},"Exit ",[40,55,56],{},"0",": the pipeline behaved as designed.",[30,59,60,61,64],{},"Do not restore the default ",[13,62,63],{},"SIGPIPE"," handler unless you understand what it does to your cleanup.",[22,66,68],{"id":67},"prerequisites","Prerequisites",[10,70,71,72,75,76,35],{},"A CLI that produces more than a screen of output, and the single ",[13,73,74],{},"main()"," boundary described in\n",[77,78,80],"a",{"href":79},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","error handling and exit codes",[22,82,84],{"id":83},"what-actually-happens","What actually happens",[86,87],"inline-diagram",{"name":88},"broken-pipe-sequence",[10,90,91,92,95],{},"In a shell pipeline the two processes are joined by a pipe. When ",[13,93,94],{},"head"," has printed its ten lines\nit exits and closes its end. The next time your process writes, the operating system signals that\nnobody is listening.",[10,97,98,99,101,102,105,106,108,109,111],{},"For a C program the default is that ",[13,100,63],{}," terminates the process silently — which is why ",[13,103,104],{},"yes | head"," does not produce an error. Python changes this: it sets ",[13,107,63],{}," to be ignored, so the write\nraises ",[13,110,19],{}," instead, and an unhandled exception prints a traceback.",[10,113,114],{},"That is a reasonable default for a library, because a server should not die because one client\ndisconnected. For a CLI in a pipeline it is exactly wrong, so you handle it.",[22,116,118],{"id":117},"the-fix-in-full","The fix, in full",[86,120],{"name":121},"sigpipe-handling-flow",[123,124,129],"pre",{"className":125,"code":126,"language":127,"meta":128,"style":128},"language-python shiki shiki-themes github-light github-dark","import os\nimport sys\n\ndef main() -> None:\n    try:\n        app()\n    except BrokenPipeError:\n        # The downstream reader exited. Nothing failed; leave quietly.\n        #\n        # Python flushes stdout during interpreter shutdown, which would raise again and print\n        # \"Exception ignored in: \u003C_io.TextIOWrapper name='\u003Cstdout>'>\". Pointing the file\n        # descriptor at devnull first makes that flush a no-op.\n        devnull = os.open(os.devnull, os.O_WRONLY)\n        os.dup2(devnull, sys.stdout.fileno())\n        sys.exit(0)\n","python","",[13,130,131,144,152,159,179,187,193,204,211,217,223,229,235,253,259],{"__ignoreMap":128},[132,133,136,140],"span",{"class":134,"line":135},"line",1,[132,137,139],{"class":138},"szBVR","import",[132,141,143],{"class":142},"sVt8B"," os\n",[132,145,147,149],{"class":134,"line":146},2,[132,148,139],{"class":138},[132,150,151],{"class":142}," sys\n",[132,153,155],{"class":134,"line":154},3,[132,156,158],{"emptyLinePlaceholder":157},true,"\n",[132,160,162,165,169,172,176],{"class":134,"line":161},4,[132,163,164],{"class":138},"def",[132,166,168],{"class":167},"sScJk"," main",[132,170,171],{"class":142},"() -> ",[132,173,175],{"class":174},"sj4cs","None",[132,177,178],{"class":142},":\n",[132,180,182,185],{"class":134,"line":181},5,[132,183,184],{"class":138},"    try",[132,186,178],{"class":142},[132,188,190],{"class":134,"line":189},6,[132,191,192],{"class":142},"        app()\n",[132,194,196,199,202],{"class":134,"line":195},7,[132,197,198],{"class":138},"    except",[132,200,201],{"class":174}," BrokenPipeError",[132,203,178],{"class":142},[132,205,207],{"class":134,"line":206},8,[132,208,210],{"class":209},"sJ8bj","        # The downstream reader exited. Nothing failed; leave quietly.\n",[132,212,214],{"class":134,"line":213},9,[132,215,216],{"class":209},"        #\n",[132,218,220],{"class":134,"line":219},10,[132,221,222],{"class":209},"        # Python flushes stdout during interpreter shutdown, which would raise again and print\n",[132,224,226],{"class":134,"line":225},11,[132,227,228],{"class":209},"        # \"Exception ignored in: \u003C_io.TextIOWrapper name='\u003Cstdout>'>\". Pointing the file\n",[132,230,232],{"class":134,"line":231},12,[132,233,234],{"class":209},"        # descriptor at devnull first makes that flush a no-op.\n",[132,236,238,241,244,247,250],{"class":134,"line":237},13,[132,239,240],{"class":142},"        devnull ",[132,242,243],{"class":138},"=",[132,245,246],{"class":142}," os.open(os.devnull, os.",[132,248,249],{"class":174},"O_WRONLY",[132,251,252],{"class":142},")\n",[132,254,256],{"class":134,"line":255},14,[132,257,258],{"class":142},"        os.dup2(devnull, sys.stdout.fileno())\n",[132,260,262,265,267],{"class":134,"line":261},15,[132,263,264],{"class":142},"        sys.exit(",[132,266,56],{"class":174},[132,268,252],{"class":142},[10,270,271,272,275,276,280],{},"Both halves are needed. Catching the exception stops the traceback; the ",[13,273,274],{},"dup2"," stops the ",[277,278,279],"em",{},"second","\nmessage, which Python prints during shutdown when it tries to flush a stream it cannot write to.\nMany implementations of this fix omit the second half and leave users with a stray warning after an\notherwise clean run.",[10,282,283,284,287,288,291],{},"Exit 0 because nothing failed. A non-zero status makes ",[13,285,286],{},"mytool list | head"," look like an error to\nevery wrapper script that checks it, and ",[13,289,290],{},"set -o pipefail"," will propagate that failure.",[22,293,295],{"id":294},"platform-differences","Platform differences",[86,297],{"name":298},"sigpipe-platform-matrix",[10,300,301,302,304,305,308,309,311,312,314,315,318,319,322],{},"On Linux and macOS the failed write raises ",[13,303,19],{},", which is a subclass of ",[13,306,307],{},"OSError",". On\nWindows there is no ",[13,310,63],{},"; a closed pipe surfaces as an ",[13,313,307],{}," with ",[13,316,317],{},"EINVAL"," or ",[13,320,321],{},"EPIPE","\ndepending on the situation. Catching both covers everything:",[123,324,326],{"className":125,"code":325,"language":127,"meta":128,"style":128},"    except (BrokenPipeError, OSError) as exc:\n        if isinstance(exc, BrokenPipeError) or exc.errno in (errno.EPIPE, errno.EINVAL):\n            devnull = os.open(os.devnull, os.O_WRONLY)\n            os.dup2(devnull, sys.stdout.fileno())\n            sys.exit(0)\n        raise\n",[13,327,328,351,388,401,406,415],{"__ignoreMap":128},[132,329,330,332,335,337,340,342,345,348],{"class":134,"line":135},[132,331,198],{"class":138},[132,333,334],{"class":142}," (",[132,336,19],{"class":174},[132,338,339],{"class":142},", ",[132,341,307],{"class":174},[132,343,344],{"class":142},") ",[132,346,347],{"class":138},"as",[132,349,350],{"class":142}," exc:\n",[132,352,353,356,359,362,364,366,369,372,375,378,380,383,385],{"class":134,"line":146},[132,354,355],{"class":138},"        if",[132,357,358],{"class":174}," isinstance",[132,360,361],{"class":142},"(exc, ",[132,363,19],{"class":174},[132,365,344],{"class":142},[132,367,368],{"class":138},"or",[132,370,371],{"class":142}," exc.errno ",[132,373,374],{"class":138},"in",[132,376,377],{"class":142}," (errno.",[132,379,321],{"class":174},[132,381,382],{"class":142},", errno.",[132,384,317],{"class":174},[132,386,387],{"class":142},"):\n",[132,389,390,393,395,397,399],{"class":134,"line":154},[132,391,392],{"class":142},"            devnull ",[132,394,243],{"class":138},[132,396,246],{"class":142},[132,398,249],{"class":174},[132,400,252],{"class":142},[132,402,403],{"class":134,"line":161},[132,404,405],{"class":142},"            os.dup2(devnull, sys.stdout.fileno())\n",[132,407,408,411,413],{"class":134,"line":181},[132,409,410],{"class":142},"            sys.exit(",[132,412,56],{"class":174},[132,414,252],{"class":142},[132,416,417],{"class":134,"line":189},[132,418,419],{"class":138},"        raise\n",[10,421,422,423,425,426,429],{},"Be careful not to swallow every ",[13,424,307],{}," — a disk full or a permission problem is a genuine\nfailure that should exit non-zero with a message. Checking ",[13,427,428],{},"errno"," keeps the clause narrow.",[22,431,433],{"id":432},"what-about-restoring-the-default-signal-handler","What about restoring the default signal handler?",[10,435,436],{},"You will find advice to do this:",[123,438,440],{"className":125,"code":439,"language":127,"meta":128,"style":128},"import signal\nsignal.signal(signal.SIGPIPE, signal.SIG_DFL)      # think twice\n",[13,441,442,449],{"__ignoreMap":128},[132,443,444,446],{"class":134,"line":135},[132,445,139],{"class":138},[132,447,448],{"class":142}," signal\n",[132,450,451,454,456,459,462,465],{"class":134,"line":146},[132,452,453],{"class":142},"signal.signal(signal.",[132,455,63],{"class":174},[132,457,458],{"class":142},", signal.",[132,460,461],{"class":174},"SIG_DFL",[132,463,464],{"class":142},")      ",[132,466,467],{"class":209},"# think twice\n",[10,469,470],{},"It restores the C behaviour: the process is killed immediately by the signal when a write fails. It\nis short, and it has consequences worth knowing.",[10,472,473,474,477,478,481],{},"Your process dies ",[277,475,476],{},"at the write",", so nothing after it runs — no ",[13,479,480],{},"finally"," blocks, no context\nmanager cleanup, no temporary file removal, no flushing of a partially written output file\nelsewhere. For a tool that only prints, that is acceptable. For a tool that holds a lock, writes a\nstate file, or has a half-finished download in a temporary directory, it means leaving mess behind.",[10,483,484],{},"The exception-based approach lets your cleanup run and costs six lines. Prefer it unless you have\nspecifically decided that immediate termination is correct.",[22,486,488],{"id":487},"where-the-handler-goes","Where the handler goes",[10,490,491,492,495],{},"At the boundary, wrapping the whole application, and nowhere else. A ",[13,493,494],{},"try\u002Fexcept BrokenPipeError","\ninside a command means every command needs one, they drift, and a command added next month has\nnone.",[10,497,498],{},"The one exception is a long generator writing directly to stdout in a loop, where you may want to\nstop iterating rather than unwind through the whole call stack. Even then, re-raise or return\npromptly and let the boundary do the exiting:",[123,500,502],{"className":125,"code":501,"language":127,"meta":128,"style":128},"for row in core.collect(period):\n    try:\n        typer.echo(json.dumps(row.as_dict()))\n    except BrokenPipeError:\n        break            # stop producing; the boundary handles the exit\n",[13,503,504,517,523,528,536],{"__ignoreMap":128},[132,505,506,509,512,514],{"class":134,"line":135},[132,507,508],{"class":138},"for",[132,510,511],{"class":142}," row ",[132,513,374],{"class":138},[132,515,516],{"class":142}," core.collect(period):\n",[132,518,519,521],{"class":134,"line":146},[132,520,184],{"class":138},[132,522,178],{"class":142},[132,524,525],{"class":134,"line":154},[132,526,527],{"class":142},"        typer.echo(json.dumps(row.as_dict()))\n",[132,529,530,532,534],{"class":134,"line":161},[132,531,198],{"class":138},[132,533,201],{"class":174},[132,535,178],{"class":142},[132,537,538,541],{"class":134,"line":181},[132,539,540],{"class":138},"        break",[132,542,543],{"class":209},"            # stop producing; the boundary handles the exit\n",[22,545,547],{"id":546},"ux-considerations","UX considerations",[10,549,550,553,554,556],{},[40,551,552],{},"Say nothing."," A broken pipe is not worth a message, not even on stderr. ",[13,555,94],{}," users are not\nsurprised that the producer stopped, and a warning trains people to ignore your stderr output —\nwhich is where your real errors go.",[10,558,559,562,563,566],{},[40,560,561],{},"Do not treat it as a partial failure."," Some tools exit 141 (128 + SIGPIPE) to mimic the shell.\nUnless your users specifically expect that, exit 0: the alternative surprises ",[13,564,565],{},"set -e"," scripts for\nno benefit.",[10,568,569,572,573,576],{},[40,570,571],{},"Flush deliberately for long output."," When stdout is a pipe, Python buffers in blocks, so a\nconsumer using ",[13,574,575],{},"head -n 1"," may wait for a full buffer before seeing anything. Flushing every few\nhundred lines makes the tool feel responsive without giving up throughput.",[22,578,580],{"id":579},"testing-the-behaviour","Testing the behaviour",[10,582,583],{},"This is one of the few behaviours that genuinely needs a subprocess, because the pipe is the point:",[123,585,587],{"className":125,"code":586,"language":127,"meta":128,"style":128},"import subprocess\nimport sys\n\ndef test_survives_a_closed_downstream():\n    tool = subprocess.Popen(\n        [sys.executable, \"-m\", \"mytool\", \"list\"],\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n    )\n    head = subprocess.Popen([\"head\", \"-n\", \"5\"], stdin=tool.stdout, stdout=subprocess.DEVNULL)\n    tool.stdout.close()          # let the tool see EPIPE when head exits\n    head.wait()\n\n    _, stderr = tool.communicate()\n\n    assert tool.returncode == 0\n    assert b\"BrokenPipeError\" not in stderr\n    assert b\"Exception ignored\" not in stderr        # the shutdown warning, too\n",[13,588,589,596,602,606,616,626,648,665,678,683,728,736,741,745,755,759,774,794],{"__ignoreMap":128},[132,590,591,593],{"class":134,"line":135},[132,592,139],{"class":138},[132,594,595],{"class":142}," subprocess\n",[132,597,598,600],{"class":134,"line":146},[132,599,139],{"class":138},[132,601,151],{"class":142},[132,603,604],{"class":134,"line":154},[132,605,158],{"emptyLinePlaceholder":157},[132,607,608,610,613],{"class":134,"line":161},[132,609,164],{"class":138},[132,611,612],{"class":167}," test_survives_a_closed_downstream",[132,614,615],{"class":142},"():\n",[132,617,618,621,623],{"class":134,"line":181},[132,619,620],{"class":142},"    tool ",[132,622,243],{"class":138},[132,624,625],{"class":142}," subprocess.Popen(\n",[132,627,628,631,635,637,640,642,645],{"class":134,"line":189},[132,629,630],{"class":142},"        [sys.executable, ",[132,632,634],{"class":633},"sZZnC","\"-m\"",[132,636,339],{"class":142},[132,638,639],{"class":633},"\"mytool\"",[132,641,339],{"class":142},[132,643,644],{"class":633},"\"list\"",[132,646,647],{"class":142},"],\n",[132,649,650,654,656,659,662],{"class":134,"line":195},[132,651,653],{"class":652},"s4XuR","        stdout",[132,655,243],{"class":138},[132,657,658],{"class":142},"subprocess.",[132,660,661],{"class":174},"PIPE",[132,663,664],{"class":142},",\n",[132,666,667,670,672,674,676],{"class":134,"line":206},[132,668,669],{"class":652},"        stderr",[132,671,243],{"class":138},[132,673,658],{"class":142},[132,675,661],{"class":174},[132,677,664],{"class":142},[132,679,680],{"class":134,"line":213},[132,681,682],{"class":142},"    )\n",[132,684,685,688,690,693,696,698,701,703,706,709,712,714,717,719,721,723,726],{"class":134,"line":219},[132,686,687],{"class":142},"    head ",[132,689,243],{"class":138},[132,691,692],{"class":142}," subprocess.Popen([",[132,694,695],{"class":633},"\"head\"",[132,697,339],{"class":142},[132,699,700],{"class":633},"\"-n\"",[132,702,339],{"class":142},[132,704,705],{"class":633},"\"5\"",[132,707,708],{"class":142},"], ",[132,710,711],{"class":652},"stdin",[132,713,243],{"class":138},[132,715,716],{"class":142},"tool.stdout, ",[132,718,49],{"class":652},[132,720,243],{"class":138},[132,722,658],{"class":142},[132,724,725],{"class":174},"DEVNULL",[132,727,252],{"class":142},[132,729,730,733],{"class":134,"line":225},[132,731,732],{"class":142},"    tool.stdout.close()          ",[132,734,735],{"class":209},"# let the tool see EPIPE when head exits\n",[132,737,738],{"class":134,"line":231},[132,739,740],{"class":142},"    head.wait()\n",[132,742,743],{"class":134,"line":237},[132,744,158],{"emptyLinePlaceholder":157},[132,746,747,750,752],{"class":134,"line":255},[132,748,749],{"class":142},"    _, stderr ",[132,751,243],{"class":138},[132,753,754],{"class":142}," tool.communicate()\n",[132,756,757],{"class":134,"line":261},[132,758,158],{"emptyLinePlaceholder":157},[132,760,762,765,768,771],{"class":134,"line":761},16,[132,763,764],{"class":138},"    assert",[132,766,767],{"class":142}," tool.returncode ",[132,769,770],{"class":138},"==",[132,772,773],{"class":174}," 0\n",[132,775,777,779,782,785,788,791],{"class":134,"line":776},17,[132,778,764],{"class":138},[132,780,781],{"class":138}," b",[132,783,784],{"class":633},"\"BrokenPipeError\"",[132,786,787],{"class":138}," not",[132,789,790],{"class":138}," in",[132,792,793],{"class":142}," stderr\n",[132,795,797,799,801,804,806,808,811],{"class":134,"line":796},18,[132,798,764],{"class":138},[132,800,781],{"class":138},[132,802,803],{"class":633},"\"Exception ignored\"",[132,805,787],{"class":138},[132,807,790],{"class":138},[132,809,810],{"class":142}," stderr        ",[132,812,813],{"class":209},"# the shutdown warning, too\n",[10,815,816,817,820],{},"The last assertion is the one that catches an incomplete fix. Plenty of tools catch the exception\nand still print ",[13,818,819],{},"Exception ignored in: \u003C_io.TextIOWrapper …>"," afterwards, which looks just as broken\nto a user.",[10,822,823,824,826],{},"Mark the test as Unix-only if your CI matrix includes Windows, where ",[13,825,94],{}," may not exist; the\nbehaviour there is worth its own test using a Python reader.",[22,828,830],{"id":829},"conclusion","Conclusion",[10,832,833],{},"Six lines at the boundary turn a traceback into silence: catch the error, redirect stdout so the\nshutdown flush cannot fail, exit 0. The alternative — restoring the default signal handler — is\nshorter and skips your cleanup, which is a trade worth making deliberately rather than by copying a\nsnippet.",[22,835,837],{"id":836},"frequently-asked-questions","Frequently asked questions",[839,840,842],"h3",{"id":841},"why-do-i-get-two-messages-not-one","Why do I get two messages, not one?",[10,844,845,846,848,849,852,853,855],{},"The first is the unhandled ",[13,847,19],{}," from your write. The second comes from the interpreter\nflushing ",[13,850,851],{},"sys.stdout"," during shutdown, which fails the same way and prints \"Exception ignored\". The\n",[13,854,274],{}," to devnull is what prevents the second.",[839,857,859],{"id":858},"does-this-affect-stderr-as-well","Does this affect stderr as well?",[10,861,862],{},"It can, if stderr is also piped into something that exits. It is much rarer, and the same handler\ncovers it — but do not redirect stderr to devnull in the fix, or you will suppress genuine error\nmessages on the way out.",[839,864,866],{"id":865},"should-i-use-exit-code-141","Should I use exit code 141?",[10,868,869,870,872],{},"Only if your users expect shell-like semantics. 141 is what a shell reports when a child is killed\nby ",[13,871,63],{},", and reproducing it from Python surprises scripts that check for zero. Exit 0 is the\nfriendlier default for a tool.",[839,874,876],{"id":875},"does-click-or-typer-handle-this-for-me","Does Click or Typer handle this for me?",[10,878,879],{},"Not by default. Click has historically included some handling around its own echo path, but the\nexception can still escape from your own writes, and neither framework silences the shutdown\nwarning. The boundary handler is yours to add.",[839,881,883],{"id":882},"can-i-avoid-the-problem-by-not-writing-so-much","Can I avoid the problem by not writing so much?",[10,885,886,887,889],{},"No — the failure is about the reader leaving, not about volume. Even a short command can meet it\nif the consumer is ",[13,888,575],{},". Handle it once at the boundary and stop thinking about it.",[839,891,893],{"id":892},"is-this-worth-handling-in-a-tool-that-prints-one-line","Is this worth handling in a tool that prints one line?",[10,895,896,897,900],{},"Yes, because the cost is six lines at a boundary you already have. A single-line command still\nmeets ",[13,898,899],{},"| head -n 0",", and more importantly the handler is written once for the whole application\nrather than per command — so a future command that streams thousands of records is covered before\nit exists.",[839,902,904],{"id":903},"does-the-same-problem-apply-to-writing-to-a-file","Does the same problem apply to writing to a file?",[10,906,907,908,35],{},"No. A closed pipe is specific to a reader that went away; writing to a file fails for different\nreasons — no space, no permission — which are genuine errors deserving a message and a non-zero\nexit. That is exactly why the handler checks the error type rather than swallowing every ",[13,909,307],{},[22,911,913],{"id":912},"related","Related",[27,915,916,923,930,936,943],{},[30,917,918,919],{},"Up: ",[77,920,922],{"href":921},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","Working with stdin, stdout and pipes",[30,924,925,926],{},"Sideways: ",[77,927,929],{"href":928},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","Emitting JSON output for scripting",[30,931,925,932],{},[77,933,935],{"href":934},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","Detecting a TTY and adapting output",[30,937,938,939],{},"Related: ",[77,940,942],{"href":941},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","Handling keyboard interrupt cleanly",[30,944,938,945],{},[77,946,948],{"href":947},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[950,951,952],"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .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}",{"title":128,"searchDepth":146,"depth":146,"links":954},[955,956,957,958,959,960,961,962,963,964,965,974],{"id":24,"depth":146,"text":25},{"id":67,"depth":146,"text":68},{"id":83,"depth":146,"text":84},{"id":117,"depth":146,"text":118},{"id":294,"depth":146,"text":295},{"id":432,"depth":146,"text":433},{"id":487,"depth":146,"text":488},{"id":546,"depth":146,"text":547},{"id":579,"depth":146,"text":580},{"id":829,"depth":146,"text":830},{"id":836,"depth":146,"text":837,"children":966},[967,968,969,970,971,972,973],{"id":841,"depth":154,"text":842},{"id":858,"depth":154,"text":859},{"id":865,"depth":154,"text":866},{"id":875,"depth":154,"text":876},{"id":882,"depth":154,"text":883},{"id":892,"depth":154,"text":893},{"id":903,"depth":154,"text":904},{"id":912,"depth":146,"text":913},"2026-08-01","Stop a Python CLI printing a traceback under head - catch BrokenPipeError at the boundary, silence the shutdown warning, and exit cleanly across platforms.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe",{"title":5,"description":976},"advanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002Findex",[985,986,987,988,989],"pipes","sigpipe","errors","unix","exit-codes","MCSnu6fsa-P9R_iuZdXCGZt7lUShgb4XCSA8A4Qb-ss",[992,995,998,1001,1004,1007,1010,1013,1016,1019,1022,1025,1028,1031,1034,1037,1040,1043,1046,1049,1052,1055,1058,1061,1064,1067,1070,1073,1076,1077,1080,1083,1086,1089,1092,1095,1098,1101,1104,1107,1110,1113,1116,1119,1122,1125,1128,1131,1134,1137,1140,1143,1146,1149,1152,1155,1158,1161,1164,1167,1170,1173,1176,1179,1182,1185,1188,1191,1194,1197,1200,1203,1206,1209,1212,1215,1218,1221,1224,1227,1230,1233,1236],{"path":993,"title":994},"\u002Fabout","About Python CLI Toolcraft",{"path":996,"title":997},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":999,"title":1000},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1002,"title":1003},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1005,"title":1006},"\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":1008,"title":1009},"\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":1011,"title":1012},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1014,"title":1015},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1017,"title":1018},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1020,"title":1021},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1023,"title":1024},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1026,"title":1027},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1029,"title":1030},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1032,"title":1033},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1035,"title":1036},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1038,"title":1039},"\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":1041,"title":1042},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1044,"title":1045},"\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":1047,"title":1048},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1050,"title":1051},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1053,"title":1054},"\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":1056,"title":1057},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1059,"title":1060},"\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":1062,"title":1063},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1065,"title":1066},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1068,"title":1069},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1071,"title":1072},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1074,"title":1075},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":981,"title":5},{"path":1078,"title":1079},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1081,"title":1082},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1084,"title":1085},"\u002F","Python CLI Toolcraft",{"path":1087,"title":1088},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1090,"title":1091},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1093,"title":1094},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1096,"title":1097},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1099,"title":1100},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1102,"title":1103},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1105,"title":1106},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1108,"title":1109},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1111,"title":1112},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1114,"title":1115},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1117,"title":1118},"\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":1120,"title":1121},"\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":1123,"title":1124},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1126,"title":1127},"\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":1129,"title":1130},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1132,"title":1133},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1135,"title":1136},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1138,"title":1139},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1141,"title":1142},"\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":1144,"title":1145},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1147,"title":1148},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1150,"title":1151},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1153,"title":1154},"\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":1156,"title":1157},"\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":1159,"title":1160},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1162,"title":1163},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1165,"title":1166},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1168,"title":1169},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1171,"title":1172},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1174,"title":1175},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1177,"title":1178},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1180,"title":1181},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1183,"title":1184},"\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":1186,"title":1187},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1189,"title":1190},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1192,"title":1193},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1195,"title":1196},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1198,"title":1199},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1201,"title":1202},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1204,"title":1205},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1207,"title":1208},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1210,"title":1211},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1213,"title":1214},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1216,"title":1217},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1219,"title":1220},"\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":1222,"title":1223},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1225,"title":1226},"\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":1228,"title":1229},"\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":1231,"title":1232},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1234,"title":1235},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1237,"title":1238},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690030]