[{"data":1,"prerenderedAt":1528},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F":3,"content-directory":1281},{"id":4,"title":5,"body":6,"date":1265,"description":1266,"difficulty":1267,"draft":1268,"extension":1269,"meta":1270,"navigation":154,"path":1271,"seo":1272,"stem":1273,"tags":1274,"updated":1265,"__hash__":1280},"content\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002Findex.md","Handling Keyboard Interrupt Cleanly",{"type":7,"value":8,"toc":1243},"minimark",[9,18,23,66,70,83,87,91,105,115,119,250,253,264,267,270,274,277,283,289,379,385,465,483,489,560,567,571,580,590,600,604,614,620,630,634,637,643,649,711,721,724,728,731,951,954,1106,1109,1113,1116,1120,1125,1131,1135,1145,1153,1165,1172,1195,1199,1202,1206,1239],[10,11,12,13,17],"p",{},"Ctrl-C is the most common way a long-running command ends. The default Python behaviour — a\n",[14,15,16],"code",{},"KeyboardInterrupt"," traceback pointing at whatever line happened to be executing — makes a\ndeliberate cancellation look like a crash, and an exit code of 0 or 1 tells the calling script the\nwrong story.",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,42,45,56,63],"ul",{},[27,28,29,30,32,33,37,38,41],"li",{},"Catch ",[14,31,16],{}," at the ",[34,35,36],"strong",{},"boundary"," and exit ",[34,39,40],{},"130"," (128 + SIGINT).",[27,43,44],{},"Print nothing more than a short \"cancelled\" on stderr — no traceback.",[27,46,47,48,55],{},"Put cleanup in ",[34,49,50,51,54],{},"context managers and ",[14,52,53],{},"finally"," blocks",", not in a signal handler.",[27,57,58,59,62],{},"Write output ",[34,60,61],{},"atomically"," so an interrupted run leaves no half-written file.",[27,64,65],{},"Terminate child processes you started, or they outlive the parent.",[19,67,69],{"id":68},"prerequisites","Prerequisites",[10,71,72,73,76,77,82],{},"The single ",[14,74,75],{},"main()"," boundary from\n",[78,79,81],"a",{"href":80},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","error handling and exit codes",",\nand a command that runs long enough for someone to interrupt it.",[19,84,86],{"id":85},"what-ctrl-c-actually-does","What Ctrl-C actually does",[88,89],"inline-diagram",{"name":90},"interrupt-sequence",[10,92,93,94,97,98,100,101,104],{},"The terminal sends ",[14,95,96],{},"SIGINT"," to the foreground process group. Python's default handler raises\n",[14,99,16],{}," at the next opportunity — which is wherever execution happens to be, including\ninside a loop, a ",[14,102,103],{},"with"," block, or a library call.",[10,106,107,108,111,112,114],{},"That has two consequences worth internalising. Because it is an ",[34,109,110],{},"exception",", your ",[14,113,53],{},"\nblocks and context managers run, which is why cleanup belongs there. And because it can be raised\nalmost anywhere, no single line of your code can assume it will complete.",[19,116,118],{"id":117},"the-boundary-handler","The boundary handler",[120,121,126],"pre",{"className":122,"code":123,"language":124,"meta":125,"style":125},"language-python shiki shiki-themes github-light github-dark","import sys\nimport typer\n\ndef main() -> None:\n    try:\n        app()\n    except KeyboardInterrupt:\n        typer.secho(\"cancelled\", fg=typer.colors.YELLOW, err=True)\n        sys.exit(130)\n","python","",[14,127,128,141,149,156,176,184,190,201,240],{"__ignoreMap":125},[129,130,133,137],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"szBVR","import",[129,138,140],{"class":139},"sVt8B"," sys\n",[129,142,144,146],{"class":131,"line":143},2,[129,145,136],{"class":135},[129,147,148],{"class":139}," typer\n",[129,150,152],{"class":131,"line":151},3,[129,153,155],{"emptyLinePlaceholder":154},true,"\n",[129,157,159,162,166,169,173],{"class":131,"line":158},4,[129,160,161],{"class":135},"def",[129,163,165],{"class":164},"sScJk"," main",[129,167,168],{"class":139},"() -> ",[129,170,172],{"class":171},"sj4cs","None",[129,174,175],{"class":139},":\n",[129,177,179,182],{"class":131,"line":178},5,[129,180,181],{"class":135},"    try",[129,183,175],{"class":139},[129,185,187],{"class":131,"line":186},6,[129,188,189],{"class":139},"        app()\n",[129,191,193,196,199],{"class":131,"line":192},7,[129,194,195],{"class":135},"    except",[129,197,198],{"class":171}," KeyboardInterrupt",[129,200,175],{"class":139},[129,202,204,207,211,214,218,221,224,227,229,232,234,237],{"class":131,"line":203},8,[129,205,206],{"class":139},"        typer.secho(",[129,208,210],{"class":209},"sZZnC","\"cancelled\"",[129,212,213],{"class":139},", ",[129,215,217],{"class":216},"s4XuR","fg",[129,219,220],{"class":135},"=",[129,222,223],{"class":139},"typer.colors.",[129,225,226],{"class":171},"YELLOW",[129,228,213],{"class":139},[129,230,231],{"class":216},"err",[129,233,220],{"class":135},[129,235,236],{"class":171},"True",[129,238,239],{"class":139},")\n",[129,241,243,246,248],{"class":131,"line":242},9,[129,244,245],{"class":139},"        sys.exit(",[129,247,40],{"class":171},[129,249,239],{"class":139},[88,251],{"name":252},"exit-130-decision",[10,254,255,256,259,260,263],{},"130 is ",[14,257,258],{},"128 + SIGINT",", the value a shell reports when a child is killed by that signal. It is what\n",[14,261,262],{},"set -e"," scripts and CI systems expect, and it is distinguishable from both success and a genuine\nfailure.",[10,265,266],{},"Exiting 0 is the worst option: a wrapper script sees success and proceeds to the next step, having\nskipped the work. Exiting 1 is merely unhelpful — it hides a cancellation among real errors.",[10,268,269],{},"Keep the message to one word on stderr. The user knows what they pressed; a paragraph explaining\nthat the operation was cancelled is noise, and a traceback reads as a defect in your tool.",[19,271,273],{"id":272},"cleaning-up-properly","Cleaning up properly",[88,275],{"name":276},"interrupt-cleanup-stack",[10,278,279,280,282],{},"Four things routinely need tidying, and all four are ",[14,281,53],{}," blocks rather than signal handlers.",[10,284,285,288],{},[34,286,287],{},"Partial output."," Write to a temporary file beside the destination and rename at the end. The\nrename is atomic on every platform that matters, so an interrupted run leaves the old file intact\nrather than a truncated new one:",[120,290,292],{"className":122,"code":291,"language":124,"meta":125,"style":125},"def write_atomic(path: Path, data: str) -> None:\n    tmp = path.with_suffix(path.suffix + \".tmp\")\n    try:\n        tmp.write_text(data, encoding=\"utf-8\")\n        tmp.replace(path)\n    finally:\n        tmp.unlink(missing_ok=True)\n",[14,293,294,314,332,338,353,358,365],{"__ignoreMap":125},[129,295,296,298,301,304,307,310,312],{"class":131,"line":132},[129,297,161],{"class":135},[129,299,300],{"class":164}," write_atomic",[129,302,303],{"class":139},"(path: Path, data: ",[129,305,306],{"class":171},"str",[129,308,309],{"class":139},") -> ",[129,311,172],{"class":171},[129,313,175],{"class":139},[129,315,316,319,321,324,327,330],{"class":131,"line":143},[129,317,318],{"class":139},"    tmp ",[129,320,220],{"class":135},[129,322,323],{"class":139}," path.with_suffix(path.suffix ",[129,325,326],{"class":135},"+",[129,328,329],{"class":209}," \".tmp\"",[129,331,239],{"class":139},[129,333,334,336],{"class":131,"line":151},[129,335,181],{"class":135},[129,337,175],{"class":139},[129,339,340,343,346,348,351],{"class":131,"line":158},[129,341,342],{"class":139},"        tmp.write_text(data, ",[129,344,345],{"class":216},"encoding",[129,347,220],{"class":135},[129,349,350],{"class":209},"\"utf-8\"",[129,352,239],{"class":139},[129,354,355],{"class":131,"line":178},[129,356,357],{"class":139},"        tmp.replace(path)\n",[129,359,360,363],{"class":131,"line":186},[129,361,362],{"class":135},"    finally",[129,364,175],{"class":139},[129,366,367,370,373,375,377],{"class":131,"line":192},[129,368,369],{"class":139},"        tmp.unlink(",[129,371,372],{"class":216},"missing_ok",[129,374,220],{"class":135},[129,376,236],{"class":171},[129,378,239],{"class":139},[10,380,381,384],{},[34,382,383],{},"Locks."," A lock file left behind blocks the next run and needs manual removal, which is a support\nticket waiting to happen:",[120,386,388],{"className":122,"code":387,"language":124,"meta":125,"style":125},"@contextmanager\ndef exclusive_lock(path: Path):\n    lock = path.with_suffix(\".lock\")\n    lock.touch(exist_ok=False)\n    try:\n        yield\n    finally:\n        lock.unlink(missing_ok=True)\n",[14,389,390,395,405,420,435,441,446,452],{"__ignoreMap":125},[129,391,392],{"class":131,"line":132},[129,393,394],{"class":164},"@contextmanager\n",[129,396,397,399,402],{"class":131,"line":143},[129,398,161],{"class":135},[129,400,401],{"class":164}," exclusive_lock",[129,403,404],{"class":139},"(path: Path):\n",[129,406,407,410,412,415,418],{"class":131,"line":151},[129,408,409],{"class":139},"    lock ",[129,411,220],{"class":135},[129,413,414],{"class":139}," path.with_suffix(",[129,416,417],{"class":209},"\".lock\"",[129,419,239],{"class":139},[129,421,422,425,428,430,433],{"class":131,"line":158},[129,423,424],{"class":139},"    lock.touch(",[129,426,427],{"class":216},"exist_ok",[129,429,220],{"class":135},[129,431,432],{"class":171},"False",[129,434,239],{"class":139},[129,436,437,439],{"class":131,"line":178},[129,438,181],{"class":135},[129,440,175],{"class":139},[129,442,443],{"class":131,"line":186},[129,444,445],{"class":135},"        yield\n",[129,447,448,450],{"class":131,"line":192},[129,449,362],{"class":135},[129,451,175],{"class":139},[129,453,454,457,459,461,463],{"class":131,"line":203},[129,455,456],{"class":139},"        lock.unlink(",[129,458,372],{"class":216},[129,460,220],{"class":135},[129,462,236],{"class":171},[129,464,239],{"class":139},[10,466,467,470,471,474,475,478,479,482],{},[34,468,469],{},"Terminal state."," If you hid the cursor or entered an alternate screen, restore it — otherwise the\nuser's shell is left in a strange state after cancelling. Rich's ",[14,472,473],{},"Progress"," and ",[14,476,477],{},"Live"," handle this\nin their own ",[14,480,481],{},"__exit__",", which is a good reason to use them as context managers rather than starting\nand stopping them by hand.",[10,484,485,488],{},[34,486,487],{},"Child processes."," A subprocess you started does not necessarily die with you:",[120,490,492],{"className":122,"code":491,"language":124,"meta":125,"style":125},"proc = subprocess.Popen([...])\ntry:\n    proc.wait()\nexcept KeyboardInterrupt:\n    proc.terminate()\n    proc.wait(timeout=5)\n    raise                     # let the boundary decide the exit code\n",[14,493,494,510,517,522,531,536,551],{"__ignoreMap":125},[129,495,496,499,501,504,507],{"class":131,"line":132},[129,497,498],{"class":139},"proc ",[129,500,220],{"class":135},[129,502,503],{"class":139}," subprocess.Popen([",[129,505,506],{"class":171},"...",[129,508,509],{"class":139},"])\n",[129,511,512,515],{"class":131,"line":143},[129,513,514],{"class":135},"try",[129,516,175],{"class":139},[129,518,519],{"class":131,"line":151},[129,520,521],{"class":139},"    proc.wait()\n",[129,523,524,527,529],{"class":131,"line":158},[129,525,526],{"class":135},"except",[129,528,198],{"class":171},[129,530,175],{"class":139},[129,532,533],{"class":131,"line":178},[129,534,535],{"class":139},"    proc.terminate()\n",[129,537,538,541,544,546,549],{"class":131,"line":186},[129,539,540],{"class":139},"    proc.wait(",[129,542,543],{"class":216},"timeout",[129,545,220],{"class":135},[129,547,548],{"class":171},"5",[129,550,239],{"class":139},[129,552,553,556],{"class":131,"line":192},[129,554,555],{"class":135},"    raise",[129,557,559],{"class":558},"sJ8bj","                     # let the boundary decide the exit code\n",[10,561,562,563,566],{},"Note the ",[14,564,565],{},"raise",". The command handles its own resource and then lets the interrupt continue\npropagating, so the exit code decision stays in one place.",[19,568,570],{"id":569},"what-not-to-do","What not to do",[10,572,573,579],{},[34,574,575,576,578],{},"Do not install a ",[14,577,96],{}," handler"," that sets a flag and returns. It sounds tidy and it means\nCtrl-C no longer interrupts a blocking call — a user pressing it during a network read waits for the\ntimeout, presses it again, and gets a much less graceful exit. The exception-based default is better\nthan almost anything you would write.",[10,581,582,585,586,589],{},[34,583,584],{},"Do not swallow it."," ",[14,587,588],{},"except KeyboardInterrupt: pass"," inside a loop makes a command impossible to\nstop, which is genuinely infuriating. If you catch it locally to clean up, re-raise.",[10,591,592,595,596,599],{},[34,593,594],{},"Do not treat a second Ctrl-C as a failure of the first."," Users press it twice when the first\nappeared to do nothing. If your cleanup is slow, say so — ",[14,597,598],{},"cancelling…"," on stderr — so the second\npress is informed rather than reflexive.",[19,601,603],{"id":602},"ux-considerations","UX considerations",[10,605,606,609,610,613],{},[34,607,608],{},"Say what was not done."," For a command with partial results, one line is worth printing:\n",[14,611,612],{},"cancelled after 412 of 1,000 files; nothing was uploaded",". It tells the user whether they need to\nundo anything.",[10,615,616,619],{},[34,617,618],{},"Make cancellation safe by design."," A command that can be interrupted at any point without leaving\ninconsistent state is better than one with elaborate cleanup. Atomic writes and idempotent\noperations get you most of the way there.",[10,621,622,625,626,629],{},[34,623,624],{},"Consider a resume hint."," If the operation supports resuming, the cancellation message is the\nright place to mention it: ",[14,627,628],{},"re-run with --resume to continue",".",[19,631,633],{"id":632},"interrupting-work-that-is-already-partly-done","Interrupting work that is already partly done",[10,635,636],{},"For a command that has completed part of its work, the useful question is what the user should be\nable to do next. Three arrangements, in increasing order of effort.",[10,638,639,642],{},[34,640,641],{},"Idempotent by design."," If re-running the command simply skips what is already done, cancellation\nneeds no special handling at all — the user runs it again. A sync that compares checksums is\nnaturally in this category, and it is worth aiming for.",[10,644,645,648],{},[34,646,647],{},"Report the boundary."," When the work is not idempotent, say exactly where it stopped, so a human\ncan decide:",[120,650,652],{"className":122,"code":651,"language":124,"meta":125,"style":125},"except KeyboardInterrupt:\n    err.print(f\"[yellow]cancelled after {done} of {total} files[\u002F]\")\n    err.print(\"[dim]uploaded files are complete; re-run to continue[\u002F]\")\n    raise\n",[14,653,654,662,697,706],{"__ignoreMap":125},[129,655,656,658,660],{"class":131,"line":132},[129,657,526],{"class":135},[129,659,198],{"class":171},[129,661,175],{"class":139},[129,663,664,667,670,673,676,679,682,685,687,690,692,695],{"class":131,"line":143},[129,665,666],{"class":139},"    err.print(",[129,668,669],{"class":135},"f",[129,671,672],{"class":209},"\"[yellow]cancelled after ",[129,674,675],{"class":171},"{",[129,677,678],{"class":139},"done",[129,680,681],{"class":171},"}",[129,683,684],{"class":209}," of ",[129,686,675],{"class":171},[129,688,689],{"class":139},"total",[129,691,681],{"class":171},[129,693,694],{"class":209}," files[\u002F]\"",[129,696,239],{"class":139},[129,698,699,701,704],{"class":131,"line":151},[129,700,666],{"class":139},[129,702,703],{"class":209},"\"[dim]uploaded files are complete; re-run to continue[\u002F]\"",[129,705,239],{"class":139},[129,707,708],{"class":131,"line":158},[129,709,710],{"class":135},"    raise\n",[10,712,713,716,717,720],{},[34,714,715],{},"Record progress."," For genuinely long operations, write a small state file as you go and support\n",[14,718,719],{},"--resume",". It is real work — the state file needs the same atomic-write treatment as your output —\nso reserve it for commands where restarting is expensive rather than merely annoying.",[10,722,723],{},"What ties all three together is that the decision belongs to the user. A tool that silently rolls\nback an hour of completed work because someone pressed Ctrl-C is as unhelpful as one that leaves no\nindication of where it stopped.",[19,725,727],{"id":726},"testing-the-behaviour","Testing the behaviour",[10,729,730],{},"The in-process cases are easy — raise the exception where the work happens:",[120,732,734],{"className":122,"code":733,"language":124,"meta":125,"style":125},"def test_interrupt_exits_130(cli, monkeypatch):\n    def boom(*args, **kwargs):\n        raise KeyboardInterrupt\n\n    monkeypatch.setattr(\"mytool.commands.sync.core.sync_directory\", boom)\n\n    result = cli.invoke(app, [\"sync\", \"data\"], catch_exceptions=True)\n\n    assert result.exit_code == 130\n    assert \"cancelled\" in result.stderr\n    assert \"Traceback\" not in result.stderr\n\ndef test_lock_is_released_on_interrupt(tmp_path):\n    with pytest.raises(KeyboardInterrupt):\n        with exclusive_lock(tmp_path \u002F \"state.json\"):\n            raise KeyboardInterrupt\n\n    assert not (tmp_path \u002F \"state.lock\").exists()\n",[14,735,736,746,769,777,781,792,796,826,830,844,858,873,878,889,903,920,928,933],{"__ignoreMap":125},[129,737,738,740,743],{"class":131,"line":132},[129,739,161],{"class":135},[129,741,742],{"class":164}," test_interrupt_exits_130",[129,744,745],{"class":139},"(cli, monkeypatch):\n",[129,747,748,751,754,757,760,763,766],{"class":131,"line":143},[129,749,750],{"class":135},"    def",[129,752,753],{"class":164}," boom",[129,755,756],{"class":139},"(",[129,758,759],{"class":135},"*",[129,761,762],{"class":139},"args, ",[129,764,765],{"class":135},"**",[129,767,768],{"class":139},"kwargs):\n",[129,770,771,774],{"class":131,"line":151},[129,772,773],{"class":135},"        raise",[129,775,776],{"class":171}," KeyboardInterrupt\n",[129,778,779],{"class":131,"line":158},[129,780,155],{"emptyLinePlaceholder":154},[129,782,783,786,789],{"class":131,"line":178},[129,784,785],{"class":139},"    monkeypatch.setattr(",[129,787,788],{"class":209},"\"mytool.commands.sync.core.sync_directory\"",[129,790,791],{"class":139},", boom)\n",[129,793,794],{"class":131,"line":186},[129,795,155],{"emptyLinePlaceholder":154},[129,797,798,801,803,806,809,811,814,817,820,822,824],{"class":131,"line":192},[129,799,800],{"class":139},"    result ",[129,802,220],{"class":135},[129,804,805],{"class":139}," cli.invoke(app, [",[129,807,808],{"class":209},"\"sync\"",[129,810,213],{"class":139},[129,812,813],{"class":209},"\"data\"",[129,815,816],{"class":139},"], ",[129,818,819],{"class":216},"catch_exceptions",[129,821,220],{"class":135},[129,823,236],{"class":171},[129,825,239],{"class":139},[129,827,828],{"class":131,"line":203},[129,829,155],{"emptyLinePlaceholder":154},[129,831,832,835,838,841],{"class":131,"line":242},[129,833,834],{"class":135},"    assert",[129,836,837],{"class":139}," result.exit_code ",[129,839,840],{"class":135},"==",[129,842,843],{"class":171}," 130\n",[129,845,847,849,852,855],{"class":131,"line":846},10,[129,848,834],{"class":135},[129,850,851],{"class":209}," \"cancelled\"",[129,853,854],{"class":135}," in",[129,856,857],{"class":139}," result.stderr\n",[129,859,861,863,866,869,871],{"class":131,"line":860},11,[129,862,834],{"class":135},[129,864,865],{"class":209}," \"Traceback\"",[129,867,868],{"class":135}," not",[129,870,854],{"class":135},[129,872,857],{"class":139},[129,874,876],{"class":131,"line":875},12,[129,877,155],{"emptyLinePlaceholder":154},[129,879,881,883,886],{"class":131,"line":880},13,[129,882,161],{"class":135},[129,884,885],{"class":164}," test_lock_is_released_on_interrupt",[129,887,888],{"class":139},"(tmp_path):\n",[129,890,892,895,898,900],{"class":131,"line":891},14,[129,893,894],{"class":135},"    with",[129,896,897],{"class":139}," pytest.raises(",[129,899,16],{"class":171},[129,901,902],{"class":139},"):\n",[129,904,906,909,912,915,918],{"class":131,"line":905},15,[129,907,908],{"class":135},"        with",[129,910,911],{"class":139}," exclusive_lock(tmp_path ",[129,913,914],{"class":135},"\u002F",[129,916,917],{"class":209}," \"state.json\"",[129,919,902],{"class":139},[129,921,923,926],{"class":131,"line":922},16,[129,924,925],{"class":135},"            raise",[129,927,776],{"class":171},[129,929,931],{"class":131,"line":930},17,[129,932,155],{"emptyLinePlaceholder":154},[129,934,936,938,940,943,945,948],{"class":131,"line":935},18,[129,937,834],{"class":135},[129,939,868],{"class":135},[129,941,942],{"class":139}," (tmp_path ",[129,944,914],{"class":135},[129,946,947],{"class":209}," \"state.lock\"",[129,949,950],{"class":139},").exists()\n",[10,952,953],{},"For the real signal path, one subprocess test proves the whole chain:",[120,955,957],{"className":122,"code":956,"language":124,"meta":125,"style":125},"import os, signal, subprocess, sys, time\n\ndef test_real_sigint_is_handled():\n    proc = subprocess.Popen(\n        [sys.executable, \"-m\", \"mytool\", \"sync\", \".\u002Fbig\"],\n        stderr=subprocess.PIPE,\n    )\n    time.sleep(0.5)\n    proc.send_signal(signal.SIGINT)\n    _, stderr = proc.communicate(timeout=10)\n\n    assert proc.returncode == 130\n    assert b\"Traceback\" not in stderr\n",[14,958,959,966,970,980,990,1015,1031,1036,1046,1055,1074,1078,1089],{"__ignoreMap":125},[129,960,961,963],{"class":131,"line":132},[129,962,136],{"class":135},[129,964,965],{"class":139}," os, signal, subprocess, sys, time\n",[129,967,968],{"class":131,"line":143},[129,969,155],{"emptyLinePlaceholder":154},[129,971,972,974,977],{"class":131,"line":151},[129,973,161],{"class":135},[129,975,976],{"class":164}," test_real_sigint_is_handled",[129,978,979],{"class":139},"():\n",[129,981,982,985,987],{"class":131,"line":158},[129,983,984],{"class":139},"    proc ",[129,986,220],{"class":135},[129,988,989],{"class":139}," subprocess.Popen(\n",[129,991,992,995,998,1000,1003,1005,1007,1009,1012],{"class":131,"line":178},[129,993,994],{"class":139},"        [sys.executable, ",[129,996,997],{"class":209},"\"-m\"",[129,999,213],{"class":139},[129,1001,1002],{"class":209},"\"mytool\"",[129,1004,213],{"class":139},[129,1006,808],{"class":209},[129,1008,213],{"class":139},[129,1010,1011],{"class":209},"\".\u002Fbig\"",[129,1013,1014],{"class":139},"],\n",[129,1016,1017,1020,1022,1025,1028],{"class":131,"line":186},[129,1018,1019],{"class":216},"        stderr",[129,1021,220],{"class":135},[129,1023,1024],{"class":139},"subprocess.",[129,1026,1027],{"class":171},"PIPE",[129,1029,1030],{"class":139},",\n",[129,1032,1033],{"class":131,"line":192},[129,1034,1035],{"class":139},"    )\n",[129,1037,1038,1041,1044],{"class":131,"line":203},[129,1039,1040],{"class":139},"    time.sleep(",[129,1042,1043],{"class":171},"0.5",[129,1045,239],{"class":139},[129,1047,1048,1051,1053],{"class":131,"line":242},[129,1049,1050],{"class":139},"    proc.send_signal(signal.",[129,1052,96],{"class":171},[129,1054,239],{"class":139},[129,1056,1057,1060,1062,1065,1067,1069,1072],{"class":131,"line":846},[129,1058,1059],{"class":139},"    _, stderr ",[129,1061,220],{"class":135},[129,1063,1064],{"class":139}," proc.communicate(",[129,1066,543],{"class":216},[129,1068,220],{"class":135},[129,1070,1071],{"class":171},"10",[129,1073,239],{"class":139},[129,1075,1076],{"class":131,"line":860},[129,1077,155],{"emptyLinePlaceholder":154},[129,1079,1080,1082,1085,1087],{"class":131,"line":875},[129,1081,834],{"class":135},[129,1083,1084],{"class":139}," proc.returncode ",[129,1086,840],{"class":135},[129,1088,843],{"class":171},[129,1090,1091,1093,1096,1099,1101,1103],{"class":131,"line":880},[129,1092,834],{"class":135},[129,1094,1095],{"class":135}," b",[129,1097,1098],{"class":209},"\"Traceback\"",[129,1100,868],{"class":135},[129,1102,854],{"class":135},[129,1104,1105],{"class":139}," stderr\n",[10,1107,1108],{},"That test is worth the subprocess: it is the only way to verify that the signal, the exception, the\ncleanup and the exit code all line up in the real process.",[19,1110,1112],{"id":1111},"conclusion","Conclusion",[10,1114,1115],{},"Catch the interrupt once at the boundary, exit 130, print one word, and let context managers do the\ncleanup. Avoid signal handlers, avoid swallowing it, and write output atomically so an interrupted\nrun is simply a run that did not finish — rather than one that left a mess.",[19,1117,1119],{"id":1118},"frequently-asked-questions","Frequently asked questions",[1121,1122,1124],"h3",{"id":1123},"why-130-rather-than-1-or-0","Why 130 rather than 1 or 0?",[10,1126,1127,1128,1130],{},"Because it is what the shell reports for a process killed by ",[14,1129,96],{},", so wrapper scripts and CI\nsystems already understand it. Zero is actively harmful — a cancelled run reported as success means\nthe next step in a pipeline proceeds as though the work happened.",[1121,1132,1134],{"id":1133},"does-click-or-typer-handle-this-already","Does Click or Typer handle this already?",[10,1136,1137,1138,1140,1141,1144],{},"Partially. Click converts ",[14,1139,16],{}," into an ",[14,1142,1143],{},"Abort"," and exits with code 1, which is better\nthan a traceback and still not the conventional value. Adding the boundary handler gives you 130 and\ncontrol of the message.",[1121,1146,1148,1149,1152],{"id":1147},"what-about-sigterm","What about ",[14,1150,1151],{},"SIGTERM","?",[10,1154,1155,1156,1158,1159,1161,1162,629],{},"Worth handling for anything that might run under a supervisor or in a container, where ",[14,1157,1151],{}," is\nhow shutdown is requested. Install a handler that raises ",[14,1160,16],{}," (or a custom exception)\nso the same cleanup path runs, and exit 143 — ",[14,1163,1164],{},"128 + SIGTERM",[1121,1166,1168,1169,1171],{"id":1167},"will-my-finally-blocks-always-run","Will my ",[14,1170,53],{}," blocks always run?",[10,1173,1174,1175,1177,1178,1182,1183,1186,1187,1190,1191,629],{},"For ",[14,1176,16],{},", yes, because it is an ordinary exception. They do ",[1179,1180,1181],"em",{},"not"," run if the process\nis killed by ",[14,1184,1185],{},"SIGKILL"," or by restoring the default ",[14,1188,1189],{},"SIGPIPE"," handler, which is one of the arguments\nagainst that approach in\n",[78,1192,1194],{"href":1193},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002F","handling broken pipe",[1121,1196,1198],{"id":1197},"how-do-i-stop-a-long-loop-promptly","How do I stop a long loop promptly?",[10,1200,1201],{},"You usually do not need to — the exception is raised at the next bytecode boundary, which is\nimmediate in practice. Where it feels slow is a blocking call with a long timeout, and the fix is a\nshorter timeout with retries rather than a signal handler.",[19,1203,1205],{"id":1204},"related","Related",[24,1207,1208,1214,1221,1227,1233],{},[27,1209,1210,1211],{},"Up: ",[78,1212,1213],{"href":80},"Error handling and exit codes for CLIs",[27,1215,1216,1217],{},"Sideways: ",[78,1218,1220],{"href":1219},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[27,1222,1216,1223],{},[78,1224,1226],{"href":1225},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","Friendly error messages and tracebacks",[27,1228,1229,1230],{},"Related: ",[78,1231,1232],{"href":1193},"Handling broken pipe and SIGPIPE",[27,1234,1229,1235],{},[78,1236,1238],{"href":1237},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis\u002F","Adding progress bars and spinners to Python CLIs",[1240,1241,1242],"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":125,"searchDepth":143,"depth":143,"links":1244},[1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1264],{"id":21,"depth":143,"text":22},{"id":68,"depth":143,"text":69},{"id":85,"depth":143,"text":86},{"id":117,"depth":143,"text":118},{"id":272,"depth":143,"text":273},{"id":569,"depth":143,"text":570},{"id":602,"depth":143,"text":603},{"id":632,"depth":143,"text":633},{"id":726,"depth":143,"text":727},{"id":1111,"depth":143,"text":1112},{"id":1118,"depth":143,"text":1119,"children":1256},[1257,1258,1259,1261,1263],{"id":1123,"depth":151,"text":1124},{"id":1133,"depth":151,"text":1134},{"id":1147,"depth":151,"text":1260},"What about SIGTERM?",{"id":1167,"depth":151,"text":1262},"Will my finally blocks always run?",{"id":1197,"depth":151,"text":1198},{"id":1204,"depth":143,"text":1205},"2026-08-01","Make Ctrl-C behave in a Python CLI - exit 130 without a traceback, run cleanup through context managers, and never report a cancelled run as success.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly",{"title":5,"description":1266},"advanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002Findex",[1275,1276,1277,1278,1279],"signals","keyboard-interrupt","exit-codes","cleanup","errors","p5zVHkUHFft1SdnhKabnsepwksbskifvA-a5TDcAsvo",[1282,1285,1288,1291,1294,1297,1300,1303,1306,1309,1312,1315,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1370,1373,1375,1378,1381,1384,1387,1390,1393,1396,1399,1402,1405,1408,1411,1414,1417,1420,1423,1426,1429,1432,1435,1438,1441,1444,1447,1450,1453,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525],{"path":1283,"title":1284},"\u002Fabout","About Python CLI Toolcraft",{"path":1286,"title":1287},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1289,"title":1290},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1292,"title":1293},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1295,"title":1296},"\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":1298,"title":1299},"\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":1301,"title":1302},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1304,"title":1305},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1307,"title":1308},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1310,"title":1311},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1313,"title":1314},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1271,"title":5},{"path":1317,"title":1318},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1320,"title":1321},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1323,"title":1324},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1326,"title":1327},"\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":1329,"title":1330},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1332,"title":1333},"\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":1335,"title":1336},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1338,"title":1339},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1341,"title":1342},"\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":1344,"title":1345},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1347,"title":1348},"\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":1350,"title":1351},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1353,"title":1354},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1356,"title":1357},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1359,"title":1360},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1362,"title":1363},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1365,"title":1366},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1368,"title":1369},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1371,"title":1372},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":914,"title":1374},"Python CLI Toolcraft",{"path":1376,"title":1377},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1379,"title":1380},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1382,"title":1383},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1385,"title":1386},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1388,"title":1389},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1391,"title":1392},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1394,"title":1395},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1397,"title":1398},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1400,"title":1401},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1403,"title":1404},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1406,"title":1407},"\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":1409,"title":1410},"\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":1412,"title":1413},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1415,"title":1416},"\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":1418,"title":1419},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1421,"title":1422},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1424,"title":1425},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1427,"title":1428},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1430,"title":1431},"\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":1433,"title":1434},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1436,"title":1437},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1439,"title":1440},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1442,"title":1443},"\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":1445,"title":1446},"\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":1448,"title":1449},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1451,"title":1452},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1454,"title":1455},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1457,"title":1458},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1460,"title":1461},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1463,"title":1464},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1466,"title":1467},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1469,"title":1470},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1472,"title":1473},"\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":1475,"title":1476},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1478,"title":1479},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1481,"title":1482},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1484,"title":1485},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1487,"title":1488},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1490,"title":1491},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1493,"title":1494},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1496,"title":1497},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1499,"title":1500},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1502,"title":1503},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1505,"title":1506},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1508,"title":1509},"\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":1511,"title":1512},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1514,"title":1515},"\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":1517,"title":1518},"\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":1520,"title":1521},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1523,"title":1524},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1526,"title":1527},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690028]