[{"data":1,"prerenderedAt":2849},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers\u002F":3,"content-directory":2004},{"id":4,"title":5,"body":6,"date":1988,"description":1989,"difficulty":1990,"draft":1991,"extension":1992,"meta":1993,"navigation":191,"path":1994,"seo":1995,"stem":1996,"tags":1997,"updated":1988,"__hash__":2003},"content\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers\u002Findex.md","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"type":7,"value":8,"toc":1969},"minimark",[9,33,38,56,60,64,67,100,106,110,143,147,150,560,563,892,908,1233,1236,1243,1247,1291,1294,1298,1301,1852,1859,1863,1876,1880,1888,1891,1895,1898,1902,1905,1909,1912,1916,1919,1923,1930,1934,1965],[10,11,12,13,17,18,21,22,26,27,32],"p",{},"Modern CLI output leans on the terminal: spinners that redraw ten times a second, progress bars that rewrite one line, tables drawn with box characters, colour as the only difference between \"ok\" and \"failed\". On a capable terminal with a sighted user that looks great. In other places it ranges from noisy to unusable. Emacs shell buffers and some IDE consoles set ",[14,15,16],"code",{},"TERM=dumb"," and cannot move the cursor, so every redraw becomes a new line. A screen reader announces each spinner frame, or reads ",[14,19,20],{},"┏━━━━┳━━━━┓"," as a string of symbol names. Braille displays and logs captured from a pseudo-terminal show the same clutter. Supporting these environments does not mean giving up rich output — it means detecting when to switch to ",[23,24,25],"strong",{},"linear, plain output"," and making that mode a first-class, tested part of the CLI. This guide builds that switch. It belongs to the ",[28,29,31],"a",{"href":30},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002F","cross-platform terminal topic",".",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,49],"ul",{},[42,43,44,45,32],"li",{},"A CLI that uses Rich for progress, tables or colour, as in ",[28,46,48],{"href":47},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI with Rich",[42,50,51,52,32],{},"The colour conventions from ",[28,53,55],{"href":54},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color\u002F","respecting NO_COLOR and FORCE_COLOR",[34,57,59],{"id":58},"three-separate-concerns","Three separate concerns",[61,62],"inline-diagram",{"name":63},"plain-concerns",[10,65,66],{},"\"Plain output\" bundles three decisions that are worth keeping separate, because different environments need different combinations:",[68,69,70,80,86],"ol",{},[42,71,72,75,76,79],{},[23,73,74],{},"Colour"," — off for ",[14,77,78],{},"NO_COLOR",", pipes and dumb terminals.",[42,81,82,85],{},[23,83,84],{},"Animation and cursor movement"," — spinners, live-updating progress, redrawn tables. Off for dumb terminals, non-TTY output and screen-reader users.",[42,87,88,91,92,95,96,99],{},[23,89,90],{},"Decoration"," — box-drawing tables, Unicode symbols like ",[14,93,94],{},"✔"," and ",[14,97,98],{},"⠋",", emoji. Off for screen-reader users, and on terminals that cannot encode them.",[10,101,102,103,105],{},"A sighted user with ",[14,104,78],{}," still wants animation; a screen-reader user may be fine with colour (it is simply not announced) but needs no animation and no decoration.",[34,107,109],{"id":108},"what-rich-already-does","What Rich already does",[10,111,112,113,115,116,119,120,123,124,127,128,130,131,134,135,139,140,142],{},"Rich detects some of this itself. With ",[14,114,16],{},", a Rich ",[14,117,118],{},"Console"," reports ",[14,121,122],{},"is_dumb_terminal",", disables colour, and a ",[14,125,126],{},"Progress"," bar does not animate: in a test, a three-step progress run under ",[14,129,16],{}," produced two plain lines of output with no escape sequences and no carriage returns, where the same run on ",[14,132,133],{},"xterm-256color"," produced continuous redraws. When stdout is not a terminal, Rich likewise drops colour and animation. What Rich cannot detect is a ",[136,137,138],"em",{},"person"," who needs linear output on a perfectly capable terminal — screen-reader users rarely set ",[14,141,16],{},", because other programs need full terminal features. That case needs an explicit switch.",[34,144,146],{"id":145},"the-recipe","The recipe",[10,148,149],{},"Centralise the decision in one policy object, computed once at startup:",[151,152,157],"pre",{"className":153,"code":154,"language":155,"meta":156,"style":156},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Foutputmode.py\nfrom __future__ import annotations\n\nimport os\nimport sys\nfrom dataclasses import dataclass\n\n\n@dataclass(frozen=True)\nclass OutputMode:\n    color: bool\n    animate: bool\n    decorate: bool\n\n\ndef detect(*, plain_flag: bool = False, stream=None) -> OutputMode:\n    stream = stream or sys.stdout\n    env = os.environ\n    tty = stream.isatty()\n    dumb = env.get(\"TERM\", \"\") == \"dumb\"\n    plain = plain_flag or env.get(\"MYTOOL_PLAIN\", \"\") not in (\"\", \"0\")\n    no_color = \"NO_COLOR\" in env and env[\"NO_COLOR\"] != \"\"\n    return OutputMode(\n        color=tty and not dumb and not no_color,\n        animate=tty and not dumb and not plain,\n        decorate=not plain,\n    )\n","python","",[14,158,159,168,186,193,202,210,223,228,233,256,268,277,285,293,298,303,340,357,368,379,409,451,485,494,520,543,554],{"__ignoreMap":156},[160,161,164],"span",{"class":162,"line":163},"line",1,[160,165,167],{"class":166},"sJ8bj","# src\u002Fmytool\u002Foutputmode.py\n",[160,169,171,175,179,182],{"class":162,"line":170},2,[160,172,174],{"class":173},"szBVR","from",[160,176,178],{"class":177},"sj4cs"," __future__",[160,180,181],{"class":173}," import",[160,183,185],{"class":184},"sVt8B"," annotations\n",[160,187,189],{"class":162,"line":188},3,[160,190,192],{"emptyLinePlaceholder":191},true,"\n",[160,194,196,199],{"class":162,"line":195},4,[160,197,198],{"class":173},"import",[160,200,201],{"class":184}," os\n",[160,203,205,207],{"class":162,"line":204},5,[160,206,198],{"class":173},[160,208,209],{"class":184}," sys\n",[160,211,213,215,218,220],{"class":162,"line":212},6,[160,214,174],{"class":173},[160,216,217],{"class":184}," dataclasses ",[160,219,198],{"class":173},[160,221,222],{"class":184}," dataclass\n",[160,224,226],{"class":162,"line":225},7,[160,227,192],{"emptyLinePlaceholder":191},[160,229,231],{"class":162,"line":230},8,[160,232,192],{"emptyLinePlaceholder":191},[160,234,236,240,243,247,250,253],{"class":162,"line":235},9,[160,237,239],{"class":238},"sScJk","@dataclass",[160,241,242],{"class":184},"(",[160,244,246],{"class":245},"s4XuR","frozen",[160,248,249],{"class":173},"=",[160,251,252],{"class":177},"True",[160,254,255],{"class":184},")\n",[160,257,259,262,265],{"class":162,"line":258},10,[160,260,261],{"class":173},"class",[160,263,264],{"class":238}," OutputMode",[160,266,267],{"class":184},":\n",[160,269,271,274],{"class":162,"line":270},11,[160,272,273],{"class":184},"    color: ",[160,275,276],{"class":177},"bool\n",[160,278,280,283],{"class":162,"line":279},12,[160,281,282],{"class":184},"    animate: ",[160,284,276],{"class":177},[160,286,288,291],{"class":162,"line":287},13,[160,289,290],{"class":184},"    decorate: ",[160,292,276],{"class":177},[160,294,296],{"class":162,"line":295},14,[160,297,192],{"emptyLinePlaceholder":191},[160,299,301],{"class":162,"line":300},15,[160,302,192],{"emptyLinePlaceholder":191},[160,304,306,309,312,314,317,320,323,326,329,332,334,337],{"class":162,"line":305},16,[160,307,308],{"class":173},"def",[160,310,311],{"class":238}," detect",[160,313,242],{"class":184},[160,315,316],{"class":173},"*",[160,318,319],{"class":184},", plain_flag: ",[160,321,322],{"class":177},"bool",[160,324,325],{"class":173}," =",[160,327,328],{"class":177}," False",[160,330,331],{"class":184},", stream",[160,333,249],{"class":173},[160,335,336],{"class":177},"None",[160,338,339],{"class":184},") -> OutputMode:\n",[160,341,343,346,348,351,354],{"class":162,"line":342},17,[160,344,345],{"class":184},"    stream ",[160,347,249],{"class":173},[160,349,350],{"class":184}," stream ",[160,352,353],{"class":173},"or",[160,355,356],{"class":184}," sys.stdout\n",[160,358,360,363,365],{"class":162,"line":359},18,[160,361,362],{"class":184},"    env ",[160,364,249],{"class":173},[160,366,367],{"class":184}," os.environ\n",[160,369,371,374,376],{"class":162,"line":370},19,[160,372,373],{"class":184},"    tty ",[160,375,249],{"class":173},[160,377,378],{"class":184}," stream.isatty()\n",[160,380,382,385,387,390,394,397,400,403,406],{"class":162,"line":381},20,[160,383,384],{"class":184},"    dumb ",[160,386,249],{"class":173},[160,388,389],{"class":184}," env.get(",[160,391,393],{"class":392},"sZZnC","\"TERM\"",[160,395,396],{"class":184},", ",[160,398,399],{"class":392},"\"\"",[160,401,402],{"class":184},") ",[160,404,405],{"class":173},"==",[160,407,408],{"class":392}," \"dumb\"\n",[160,410,412,415,417,420,422,424,427,429,431,433,436,439,442,444,446,449],{"class":162,"line":411},21,[160,413,414],{"class":184},"    plain ",[160,416,249],{"class":173},[160,418,419],{"class":184}," plain_flag ",[160,421,353],{"class":173},[160,423,389],{"class":184},[160,425,426],{"class":392},"\"MYTOOL_PLAIN\"",[160,428,396],{"class":184},[160,430,399],{"class":392},[160,432,402],{"class":184},[160,434,435],{"class":173},"not",[160,437,438],{"class":173}," in",[160,440,441],{"class":184}," (",[160,443,399],{"class":392},[160,445,396],{"class":184},[160,447,448],{"class":392},"\"0\"",[160,450,255],{"class":184},[160,452,454,457,459,462,464,467,470,473,476,479,482],{"class":162,"line":453},22,[160,455,456],{"class":184},"    no_color ",[160,458,249],{"class":173},[160,460,461],{"class":392}," \"NO_COLOR\"",[160,463,438],{"class":173},[160,465,466],{"class":184}," env ",[160,468,469],{"class":173},"and",[160,471,472],{"class":184}," env[",[160,474,475],{"class":392},"\"NO_COLOR\"",[160,477,478],{"class":184},"] ",[160,480,481],{"class":173},"!=",[160,483,484],{"class":392}," \"\"\n",[160,486,488,491],{"class":162,"line":487},23,[160,489,490],{"class":173},"    return",[160,492,493],{"class":184}," OutputMode(\n",[160,495,497,500,502,505,507,510,513,515,517],{"class":162,"line":496},24,[160,498,499],{"class":245},"        color",[160,501,249],{"class":173},[160,503,504],{"class":184},"tty ",[160,506,469],{"class":173},[160,508,509],{"class":173}," not",[160,511,512],{"class":184}," dumb ",[160,514,469],{"class":173},[160,516,509],{"class":173},[160,518,519],{"class":184}," no_color,\n",[160,521,523,526,528,530,532,534,536,538,540],{"class":162,"line":522},25,[160,524,525],{"class":245},"        animate",[160,527,249],{"class":173},[160,529,504],{"class":184},[160,531,469],{"class":173},[160,533,509],{"class":173},[160,535,512],{"class":184},[160,537,469],{"class":173},[160,539,509],{"class":173},[160,541,542],{"class":184}," plain,\n",[160,544,546,549,552],{"class":162,"line":545},26,[160,547,548],{"class":245},"        decorate",[160,550,551],{"class":173},"=not",[160,553,542],{"class":184},[160,555,557],{"class":162,"line":556},27,[160,558,559],{"class":184},"    )\n",[10,561,562],{},"Then make every piece of fancy output consult it. Progress is the most important, because it is the noisiest when it goes wrong:",[151,564,566],{"className":153,"code":565,"language":155,"meta":156,"style":156},"# src\u002Fmytool\u002Fprogress.py\nfrom __future__ import annotations\n\nfrom collections.abc import Iterable, Iterator\nfrom typing import TypeVar\n\nfrom rich.console import Console\nfrom rich.progress import track\n\nfrom mytool.outputmode import OutputMode\n\nT = TypeVar(\"T\")\n\n\ndef progress(items: Iterable[T], total: int, label: str, mode: OutputMode,\n             console: Console) -> Iterator[T]:\n    if mode.animate:\n        yield from track(items, total=total, description=label, console=console)\n        return\n    step = max(1, total \u002F\u002F 4)                  # a few milestones, not one line per item\n    for done, item in enumerate(items, start=1):\n        yield item\n        if done % step == 0 or done == total:\n            console.print(f\"{label}: {done} of {total} done\")\n",[14,567,568,573,583,587,599,611,615,627,639,643,655,659,674,678,682,704,709,717,749,754,784,811,819,848],{"__ignoreMap":156},[160,569,570],{"class":162,"line":163},[160,571,572],{"class":166},"# src\u002Fmytool\u002Fprogress.py\n",[160,574,575,577,579,581],{"class":162,"line":170},[160,576,174],{"class":173},[160,578,178],{"class":177},[160,580,181],{"class":173},[160,582,185],{"class":184},[160,584,585],{"class":162,"line":188},[160,586,192],{"emptyLinePlaceholder":191},[160,588,589,591,594,596],{"class":162,"line":195},[160,590,174],{"class":173},[160,592,593],{"class":184}," collections.abc ",[160,595,198],{"class":173},[160,597,598],{"class":184}," Iterable, Iterator\n",[160,600,601,603,606,608],{"class":162,"line":204},[160,602,174],{"class":173},[160,604,605],{"class":184}," typing ",[160,607,198],{"class":173},[160,609,610],{"class":184}," TypeVar\n",[160,612,613],{"class":162,"line":212},[160,614,192],{"emptyLinePlaceholder":191},[160,616,617,619,622,624],{"class":162,"line":225},[160,618,174],{"class":173},[160,620,621],{"class":184}," rich.console ",[160,623,198],{"class":173},[160,625,626],{"class":184}," Console\n",[160,628,629,631,634,636],{"class":162,"line":230},[160,630,174],{"class":173},[160,632,633],{"class":184}," rich.progress ",[160,635,198],{"class":173},[160,637,638],{"class":184}," track\n",[160,640,641],{"class":162,"line":235},[160,642,192],{"emptyLinePlaceholder":191},[160,644,645,647,650,652],{"class":162,"line":258},[160,646,174],{"class":173},[160,648,649],{"class":184}," mytool.outputmode ",[160,651,198],{"class":173},[160,653,654],{"class":184}," OutputMode\n",[160,656,657],{"class":162,"line":270},[160,658,192],{"emptyLinePlaceholder":191},[160,660,661,664,666,669,672],{"class":162,"line":279},[160,662,663],{"class":184},"T ",[160,665,249],{"class":173},[160,667,668],{"class":184}," TypeVar(",[160,670,671],{"class":392},"\"T\"",[160,673,255],{"class":184},[160,675,676],{"class":162,"line":287},[160,677,192],{"emptyLinePlaceholder":191},[160,679,680],{"class":162,"line":295},[160,681,192],{"emptyLinePlaceholder":191},[160,683,684,686,689,692,695,698,701],{"class":162,"line":300},[160,685,308],{"class":173},[160,687,688],{"class":238}," progress",[160,690,691],{"class":184},"(items: Iterable[T], total: ",[160,693,694],{"class":177},"int",[160,696,697],{"class":184},", label: ",[160,699,700],{"class":177},"str",[160,702,703],{"class":184},", mode: OutputMode,\n",[160,705,706],{"class":162,"line":305},[160,707,708],{"class":184},"             console: Console) -> Iterator[T]:\n",[160,710,711,714],{"class":162,"line":342},[160,712,713],{"class":173},"    if",[160,715,716],{"class":184}," mode.animate:\n",[160,718,719,722,725,728,730,733,736,738,741,744,746],{"class":162,"line":359},[160,720,721],{"class":173},"        yield from",[160,723,724],{"class":184}," track(items, ",[160,726,727],{"class":245},"total",[160,729,249],{"class":173},[160,731,732],{"class":184},"total, ",[160,734,735],{"class":245},"description",[160,737,249],{"class":173},[160,739,740],{"class":184},"label, ",[160,742,743],{"class":245},"console",[160,745,249],{"class":173},[160,747,748],{"class":184},"console)\n",[160,750,751],{"class":162,"line":370},[160,752,753],{"class":173},"        return\n",[160,755,756,759,761,764,766,769,772,775,778,781],{"class":162,"line":381},[160,757,758],{"class":184},"    step ",[160,760,249],{"class":173},[160,762,763],{"class":177}," max",[160,765,242],{"class":184},[160,767,768],{"class":177},"1",[160,770,771],{"class":184},", total ",[160,773,774],{"class":173},"\u002F\u002F",[160,776,777],{"class":177}," 4",[160,779,780],{"class":184},")                  ",[160,782,783],{"class":166},"# a few milestones, not one line per item\n",[160,785,786,789,792,795,798,801,804,806,808],{"class":162,"line":411},[160,787,788],{"class":173},"    for",[160,790,791],{"class":184}," done, item ",[160,793,794],{"class":173},"in",[160,796,797],{"class":177}," enumerate",[160,799,800],{"class":184},"(items, ",[160,802,803],{"class":245},"start",[160,805,249],{"class":173},[160,807,768],{"class":177},[160,809,810],{"class":184},"):\n",[160,812,813,816],{"class":162,"line":453},[160,814,815],{"class":173},"        yield",[160,817,818],{"class":184}," item\n",[160,820,821,824,827,830,833,835,838,841,843,845],{"class":162,"line":487},[160,822,823],{"class":173},"        if",[160,825,826],{"class":184}," done ",[160,828,829],{"class":173},"%",[160,831,832],{"class":184}," step ",[160,834,405],{"class":173},[160,836,837],{"class":177}," 0",[160,839,840],{"class":173}," or",[160,842,826],{"class":184},[160,844,405],{"class":173},[160,846,847],{"class":184}," total:\n",[160,849,850,853,856,859,862,865,868,871,873,876,878,881,883,885,887,890],{"class":162,"line":496},[160,851,852],{"class":184},"            console.print(",[160,854,855],{"class":173},"f",[160,857,858],{"class":392},"\"",[160,860,861],{"class":177},"{",[160,863,864],{"class":184},"label",[160,866,867],{"class":177},"}",[160,869,870],{"class":392},": ",[160,872,861],{"class":177},[160,874,875],{"class":184},"done",[160,877,867],{"class":177},[160,879,880],{"class":392}," of ",[160,882,861],{"class":177},[160,884,727],{"class":184},[160,886,867],{"class":177},[160,888,889],{"class":392}," done\"",[160,891,255],{"class":184},[10,893,894,895,898,899,902,903,907],{},"In plain mode, the user hears or reads a handful of meaningful milestones — \"upload: 25 of 100 done\" — instead of a stream of redraws. Tables get the same treatment: with ",[14,896,897],{},"decorate=False",", render records as ",[14,900,901],{},"name: value"," lines or tab-separated rows instead of a box-drawn grid; the record-and-renderer design from ",[28,904,906],{"href":905},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002F","the output formats topic"," makes that a matter of choosing a different renderer.",[151,909,911],{"className":153,"code":910,"language":155,"meta":156,"style":156},"# src\u002Fmytool\u002Fcli.py\nimport time\n\nimport typer\nfrom rich.console import Console\n\nfrom mytool.outputmode import detect\nfrom mytool.progress import progress\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main(ctx: typer.Context,\n         plain: bool = typer.Option(False, \"--plain\", help=\"Linear output: no animation, no box drawing.\")) -> None:\n    \"\"\"Uploader.\"\"\"\n    mode = detect(plain_flag=plain)\n    ctx.obj = {\"mode\": mode, \"console\": Console(no_color=not mode.color, emoji=mode.decorate)}\n\n\n@app.command()\ndef upload(ctx: typer.Context, count: int = 8) -> None:\n    \"\"\"Upload files.\"\"\"\n    mode, console = ctx.obj[\"mode\"], ctx.obj[\"console\"]\n    for _ in progress(range(count), count, \"upload\", mode, console):\n        time.sleep(0.01)\n    console.print(\"OK: uploaded all files\" if not mode.decorate else \"✔ uploaded all files\")\n",[14,912,913,918,925,929,936,946,950,961,973,977,987,991,995,1003,1013,1050,1055,1073,1111,1115,1119,1126,1150,1155,1175,1199,1209],{"__ignoreMap":156},[160,914,915],{"class":162,"line":163},[160,916,917],{"class":166},"# src\u002Fmytool\u002Fcli.py\n",[160,919,920,922],{"class":162,"line":170},[160,921,198],{"class":173},[160,923,924],{"class":184}," time\n",[160,926,927],{"class":162,"line":188},[160,928,192],{"emptyLinePlaceholder":191},[160,930,931,933],{"class":162,"line":195},[160,932,198],{"class":173},[160,934,935],{"class":184}," typer\n",[160,937,938,940,942,944],{"class":162,"line":204},[160,939,174],{"class":173},[160,941,621],{"class":184},[160,943,198],{"class":173},[160,945,626],{"class":184},[160,947,948],{"class":162,"line":212},[160,949,192],{"emptyLinePlaceholder":191},[160,951,952,954,956,958],{"class":162,"line":225},[160,953,174],{"class":173},[160,955,649],{"class":184},[160,957,198],{"class":173},[160,959,960],{"class":184}," detect\n",[160,962,963,965,968,970],{"class":162,"line":230},[160,964,174],{"class":173},[160,966,967],{"class":184}," mytool.progress ",[160,969,198],{"class":173},[160,971,972],{"class":184}," progress\n",[160,974,975],{"class":162,"line":235},[160,976,192],{"emptyLinePlaceholder":191},[160,978,979,982,984],{"class":162,"line":258},[160,980,981],{"class":184},"app ",[160,983,249],{"class":173},[160,985,986],{"class":184}," typer.Typer()\n",[160,988,989],{"class":162,"line":270},[160,990,192],{"emptyLinePlaceholder":191},[160,992,993],{"class":162,"line":279},[160,994,192],{"emptyLinePlaceholder":191},[160,996,997,1000],{"class":162,"line":287},[160,998,999],{"class":238},"@app.callback",[160,1001,1002],{"class":184},"()\n",[160,1004,1005,1007,1010],{"class":162,"line":295},[160,1006,308],{"class":173},[160,1008,1009],{"class":238}," main",[160,1011,1012],{"class":184},"(ctx: typer.Context,\n",[160,1014,1015,1018,1020,1022,1025,1028,1030,1033,1035,1038,1040,1043,1046,1048],{"class":162,"line":300},[160,1016,1017],{"class":184},"         plain: ",[160,1019,322],{"class":177},[160,1021,325],{"class":173},[160,1023,1024],{"class":184}," typer.Option(",[160,1026,1027],{"class":177},"False",[160,1029,396],{"class":184},[160,1031,1032],{"class":392},"\"--plain\"",[160,1034,396],{"class":184},[160,1036,1037],{"class":245},"help",[160,1039,249],{"class":173},[160,1041,1042],{"class":392},"\"Linear output: no animation, no box drawing.\"",[160,1044,1045],{"class":184},")) -> ",[160,1047,336],{"class":177},[160,1049,267],{"class":184},[160,1051,1052],{"class":162,"line":305},[160,1053,1054],{"class":392},"    \"\"\"Uploader.\"\"\"\n",[160,1056,1057,1060,1062,1065,1068,1070],{"class":162,"line":342},[160,1058,1059],{"class":184},"    mode ",[160,1061,249],{"class":173},[160,1063,1064],{"class":184}," detect(",[160,1066,1067],{"class":245},"plain_flag",[160,1069,249],{"class":173},[160,1071,1072],{"class":184},"plain)\n",[160,1074,1075,1078,1080,1083,1086,1089,1092,1095,1098,1100,1103,1106,1108],{"class":162,"line":359},[160,1076,1077],{"class":184},"    ctx.obj ",[160,1079,249],{"class":173},[160,1081,1082],{"class":184}," {",[160,1084,1085],{"class":392},"\"mode\"",[160,1087,1088],{"class":184},": mode, ",[160,1090,1091],{"class":392},"\"console\"",[160,1093,1094],{"class":184},": Console(",[160,1096,1097],{"class":245},"no_color",[160,1099,551],{"class":173},[160,1101,1102],{"class":184}," mode.color, ",[160,1104,1105],{"class":245},"emoji",[160,1107,249],{"class":173},[160,1109,1110],{"class":184},"mode.decorate)}\n",[160,1112,1113],{"class":162,"line":370},[160,1114,192],{"emptyLinePlaceholder":191},[160,1116,1117],{"class":162,"line":381},[160,1118,192],{"emptyLinePlaceholder":191},[160,1120,1121,1124],{"class":162,"line":411},[160,1122,1123],{"class":238},"@app.command",[160,1125,1002],{"class":184},[160,1127,1128,1130,1133,1136,1138,1140,1143,1146,1148],{"class":162,"line":453},[160,1129,308],{"class":173},[160,1131,1132],{"class":238}," upload",[160,1134,1135],{"class":184},"(ctx: typer.Context, count: ",[160,1137,694],{"class":177},[160,1139,325],{"class":173},[160,1141,1142],{"class":177}," 8",[160,1144,1145],{"class":184},") -> ",[160,1147,336],{"class":177},[160,1149,267],{"class":184},[160,1151,1152],{"class":162,"line":487},[160,1153,1154],{"class":392},"    \"\"\"Upload files.\"\"\"\n",[160,1156,1157,1160,1162,1165,1167,1170,1172],{"class":162,"line":496},[160,1158,1159],{"class":184},"    mode, console ",[160,1161,249],{"class":173},[160,1163,1164],{"class":184}," ctx.obj[",[160,1166,1085],{"class":392},[160,1168,1169],{"class":184},"], ctx.obj[",[160,1171,1091],{"class":392},[160,1173,1174],{"class":184},"]\n",[160,1176,1177,1179,1182,1184,1187,1190,1193,1196],{"class":162,"line":522},[160,1178,788],{"class":173},[160,1180,1181],{"class":184}," _ ",[160,1183,794],{"class":173},[160,1185,1186],{"class":184}," progress(",[160,1188,1189],{"class":177},"range",[160,1191,1192],{"class":184},"(count), count, ",[160,1194,1195],{"class":392},"\"upload\"",[160,1197,1198],{"class":184},", mode, console):\n",[160,1200,1201,1204,1207],{"class":162,"line":545},[160,1202,1203],{"class":184},"        time.sleep(",[160,1205,1206],{"class":177},"0.01",[160,1208,255],{"class":184},[160,1210,1211,1214,1217,1220,1222,1225,1228,1231],{"class":162,"line":556},[160,1212,1213],{"class":184},"    console.print(",[160,1215,1216],{"class":392},"\"OK: uploaded all files\"",[160,1218,1219],{"class":173}," if",[160,1221,509],{"class":173},[160,1223,1224],{"class":184}," mode.decorate ",[160,1226,1227],{"class":173},"else",[160,1229,1230],{"class":392}," \"✔ uploaded all files\"",[160,1232,255],{"class":184},[61,1234],{"name":1235},"plain-terminal",[10,1237,1238,1239,1242],{},"Note the final line: in plain mode the status word ",[14,1240,1241],{},"OK"," carries the meaning that the check mark carries visually. Every status message should work without colour and without symbols.",[34,1244,1246],{"id":1245},"ux-considerations","UX considerations",[39,1248,1249,1263,1269,1275,1285],{},[42,1250,1251,1254,1255,1258,1259,1262],{},[23,1252,1253],{},"Offer both a flag and an environment variable."," ",[14,1256,1257],{},"--plain"," for one run, ",[14,1260,1261],{},"MYTOOL_PLAIN=1"," in a shell profile for users who always need it. Mention both in help and in an accessibility section of the docs.",[42,1264,1265,1268],{},[23,1266,1267],{},"Words over symbols and colour."," \"failed\", \"warning\", \"3 of 10 done\" — never a red ✘ alone.",[42,1270,1271,1274],{},[23,1272,1273],{},"Fewer, more meaningful lines."," In linear mode, every line is read aloud; milestones beat per-item updates, and summaries beat repetition.",[42,1276,1277,1280,1281,32],{},[23,1278,1279],{},"No interactive surprises."," Prompts are fine for screen readers, but full-screen TUIs often are not; make sure every TUI feature has a plain command path, as in ",[28,1282,1284],{"href":1283},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui\u002F","choosing between a CLI, a prompt flow and a TUI",[42,1286,1287,1290],{},[23,1288,1289],{},"Ask users."," Accessibility needs vary; an issue template that invites feedback from assistive-technology users finds problems no test will.",[61,1292],{"name":1293},"plain-matrix",[34,1295,1297],{"id":1296},"testing-the-behaviour","Testing the behaviour",[10,1299,1300],{},"Test the policy as a pure function across environments, and test that plain mode never emits escape sequences or carriage returns:",[151,1302,1304],{"className":153,"code":1303,"language":155,"meta":156,"style":156},"# tests\u002Ftest_plain.py\nimport io\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom mytool.cli import app\nfrom mytool.outputmode import detect\n\n\nclass FakeTTY(io.StringIO):\n    def isatty(self) -> bool:\n        return True\n\n\n@pytest.mark.parametrize(\"env,flag,expected\", [\n    ({\"TERM\": \"xterm-256color\"}, False, (True, True, True)),\n    ({\"TERM\": \"dumb\"}, False, (False, False, True)),\n    ({\"TERM\": \"xterm-256color\", \"NO_COLOR\": \"1\"}, False, (False, True, True)),\n    ({\"TERM\": \"xterm-256color\"}, True, (True, False, False)),\n    ({\"TERM\": \"xterm-256color\", \"MYTOOL_PLAIN\": \"1\"}, False, (True, False, False)),\n])\ndef test_policy(monkeypatch, env, flag, expected):\n    for var in (\"TERM\", \"NO_COLOR\", \"MYTOOL_PLAIN\"):\n        monkeypatch.delenv(var, raising=False)\n    for key, value in env.items():\n        monkeypatch.setenv(key, value)\n    mode = detect(plain_flag=flag, stream=FakeTTY())\n    assert (mode.color, mode.animate, mode.decorate) == expected\n\n\ndef test_plain_output_is_linear():\n    result = CliRunner().invoke(app, [\"--plain\", \"upload\", \"--count\", \"8\"])\n    assert result.exit_code == 0\n    assert \"\\x1b\" not in result.output and \"\\r\" not in result.output\n    assert result.output.splitlines() == [\"upload: 2 of 8 done\", \"upload: 4 of 8 done\",\n                                          \"upload: 6 of 8 done\", \"upload: 8 of 8 done\",\n                                          \"OK: uploaded all files\"]\n",[14,1305,1306,1311,1318,1322,1329,1341,1345,1357,1367,1371,1375,1394,1409,1417,1421,1425,1438,1471,1500,1537,1565,1601,1606,1616,1639,1653,1665,1670,1694,1708,1713,1718,1729,1758,1771,1807,1831,1844],{"__ignoreMap":156},[160,1307,1308],{"class":162,"line":163},[160,1309,1310],{"class":166},"# tests\u002Ftest_plain.py\n",[160,1312,1313,1315],{"class":162,"line":170},[160,1314,198],{"class":173},[160,1316,1317],{"class":184}," io\n",[160,1319,1320],{"class":162,"line":188},[160,1321,192],{"emptyLinePlaceholder":191},[160,1323,1324,1326],{"class":162,"line":195},[160,1325,198],{"class":173},[160,1327,1328],{"class":184}," pytest\n",[160,1330,1331,1333,1336,1338],{"class":162,"line":204},[160,1332,174],{"class":173},[160,1334,1335],{"class":184}," typer.testing ",[160,1337,198],{"class":173},[160,1339,1340],{"class":184}," CliRunner\n",[160,1342,1343],{"class":162,"line":212},[160,1344,192],{"emptyLinePlaceholder":191},[160,1346,1347,1349,1352,1354],{"class":162,"line":225},[160,1348,174],{"class":173},[160,1350,1351],{"class":184}," mytool.cli ",[160,1353,198],{"class":173},[160,1355,1356],{"class":184}," app\n",[160,1358,1359,1361,1363,1365],{"class":162,"line":230},[160,1360,174],{"class":173},[160,1362,649],{"class":184},[160,1364,198],{"class":173},[160,1366,960],{"class":184},[160,1368,1369],{"class":162,"line":235},[160,1370,192],{"emptyLinePlaceholder":191},[160,1372,1373],{"class":162,"line":258},[160,1374,192],{"emptyLinePlaceholder":191},[160,1376,1377,1379,1382,1384,1387,1389,1392],{"class":162,"line":270},[160,1378,261],{"class":173},[160,1380,1381],{"class":238}," FakeTTY",[160,1383,242],{"class":184},[160,1385,1386],{"class":238},"io",[160,1388,32],{"class":184},[160,1390,1391],{"class":238},"StringIO",[160,1393,810],{"class":184},[160,1395,1396,1399,1402,1405,1407],{"class":162,"line":279},[160,1397,1398],{"class":173},"    def",[160,1400,1401],{"class":238}," isatty",[160,1403,1404],{"class":184},"(self) -> ",[160,1406,322],{"class":177},[160,1408,267],{"class":184},[160,1410,1411,1414],{"class":162,"line":287},[160,1412,1413],{"class":173},"        return",[160,1415,1416],{"class":177}," True\n",[160,1418,1419],{"class":162,"line":295},[160,1420,192],{"emptyLinePlaceholder":191},[160,1422,1423],{"class":162,"line":300},[160,1424,192],{"emptyLinePlaceholder":191},[160,1426,1427,1430,1432,1435],{"class":162,"line":305},[160,1428,1429],{"class":238},"@pytest.mark.parametrize",[160,1431,242],{"class":184},[160,1433,1434],{"class":392},"\"env,flag,expected\"",[160,1436,1437],{"class":184},", [\n",[160,1439,1440,1443,1445,1447,1450,1453,1455,1458,1460,1462,1464,1466,1468],{"class":162,"line":342},[160,1441,1442],{"class":184},"    ({",[160,1444,393],{"class":392},[160,1446,870],{"class":184},[160,1448,1449],{"class":392},"\"xterm-256color\"",[160,1451,1452],{"class":184},"}, ",[160,1454,1027],{"class":177},[160,1456,1457],{"class":184},", (",[160,1459,252],{"class":177},[160,1461,396],{"class":184},[160,1463,252],{"class":177},[160,1465,396],{"class":184},[160,1467,252],{"class":177},[160,1469,1470],{"class":184},")),\n",[160,1472,1473,1475,1477,1479,1482,1484,1486,1488,1490,1492,1494,1496,1498],{"class":162,"line":359},[160,1474,1442],{"class":184},[160,1476,393],{"class":392},[160,1478,870],{"class":184},[160,1480,1481],{"class":392},"\"dumb\"",[160,1483,1452],{"class":184},[160,1485,1027],{"class":177},[160,1487,1457],{"class":184},[160,1489,1027],{"class":177},[160,1491,396],{"class":184},[160,1493,1027],{"class":177},[160,1495,396],{"class":184},[160,1497,252],{"class":177},[160,1499,1470],{"class":184},[160,1501,1502,1504,1506,1508,1510,1512,1514,1516,1519,1521,1523,1525,1527,1529,1531,1533,1535],{"class":162,"line":370},[160,1503,1442],{"class":184},[160,1505,393],{"class":392},[160,1507,870],{"class":184},[160,1509,1449],{"class":392},[160,1511,396],{"class":184},[160,1513,475],{"class":392},[160,1515,870],{"class":184},[160,1517,1518],{"class":392},"\"1\"",[160,1520,1452],{"class":184},[160,1522,1027],{"class":177},[160,1524,1457],{"class":184},[160,1526,1027],{"class":177},[160,1528,396],{"class":184},[160,1530,252],{"class":177},[160,1532,396],{"class":184},[160,1534,252],{"class":177},[160,1536,1470],{"class":184},[160,1538,1539,1541,1543,1545,1547,1549,1551,1553,1555,1557,1559,1561,1563],{"class":162,"line":381},[160,1540,1442],{"class":184},[160,1542,393],{"class":392},[160,1544,870],{"class":184},[160,1546,1449],{"class":392},[160,1548,1452],{"class":184},[160,1550,252],{"class":177},[160,1552,1457],{"class":184},[160,1554,252],{"class":177},[160,1556,396],{"class":184},[160,1558,1027],{"class":177},[160,1560,396],{"class":184},[160,1562,1027],{"class":177},[160,1564,1470],{"class":184},[160,1566,1567,1569,1571,1573,1575,1577,1579,1581,1583,1585,1587,1589,1591,1593,1595,1597,1599],{"class":162,"line":411},[160,1568,1442],{"class":184},[160,1570,393],{"class":392},[160,1572,870],{"class":184},[160,1574,1449],{"class":392},[160,1576,396],{"class":184},[160,1578,426],{"class":392},[160,1580,870],{"class":184},[160,1582,1518],{"class":392},[160,1584,1452],{"class":184},[160,1586,1027],{"class":177},[160,1588,1457],{"class":184},[160,1590,252],{"class":177},[160,1592,396],{"class":184},[160,1594,1027],{"class":177},[160,1596,396],{"class":184},[160,1598,1027],{"class":177},[160,1600,1470],{"class":184},[160,1602,1603],{"class":162,"line":453},[160,1604,1605],{"class":184},"])\n",[160,1607,1608,1610,1613],{"class":162,"line":487},[160,1609,308],{"class":173},[160,1611,1612],{"class":238}," test_policy",[160,1614,1615],{"class":184},"(monkeypatch, env, flag, expected):\n",[160,1617,1618,1620,1623,1625,1627,1629,1631,1633,1635,1637],{"class":162,"line":496},[160,1619,788],{"class":173},[160,1621,1622],{"class":184}," var ",[160,1624,794],{"class":173},[160,1626,441],{"class":184},[160,1628,393],{"class":392},[160,1630,396],{"class":184},[160,1632,475],{"class":392},[160,1634,396],{"class":184},[160,1636,426],{"class":392},[160,1638,810],{"class":184},[160,1640,1641,1644,1647,1649,1651],{"class":162,"line":522},[160,1642,1643],{"class":184},"        monkeypatch.delenv(var, ",[160,1645,1646],{"class":245},"raising",[160,1648,249],{"class":173},[160,1650,1027],{"class":177},[160,1652,255],{"class":184},[160,1654,1655,1657,1660,1662],{"class":162,"line":545},[160,1656,788],{"class":173},[160,1658,1659],{"class":184}," key, value ",[160,1661,794],{"class":173},[160,1663,1664],{"class":184}," env.items():\n",[160,1666,1667],{"class":162,"line":556},[160,1668,1669],{"class":184},"        monkeypatch.setenv(key, value)\n",[160,1671,1673,1675,1677,1679,1681,1683,1686,1689,1691],{"class":162,"line":1672},28,[160,1674,1059],{"class":184},[160,1676,249],{"class":173},[160,1678,1064],{"class":184},[160,1680,1067],{"class":245},[160,1682,249],{"class":173},[160,1684,1685],{"class":184},"flag, ",[160,1687,1688],{"class":245},"stream",[160,1690,249],{"class":173},[160,1692,1693],{"class":184},"FakeTTY())\n",[160,1695,1697,1700,1703,1705],{"class":162,"line":1696},29,[160,1698,1699],{"class":173},"    assert",[160,1701,1702],{"class":184}," (mode.color, mode.animate, mode.decorate) ",[160,1704,405],{"class":173},[160,1706,1707],{"class":184}," expected\n",[160,1709,1711],{"class":162,"line":1710},30,[160,1712,192],{"emptyLinePlaceholder":191},[160,1714,1716],{"class":162,"line":1715},31,[160,1717,192],{"emptyLinePlaceholder":191},[160,1719,1721,1723,1726],{"class":162,"line":1720},32,[160,1722,308],{"class":173},[160,1724,1725],{"class":238}," test_plain_output_is_linear",[160,1727,1728],{"class":184},"():\n",[160,1730,1732,1735,1737,1740,1742,1744,1746,1748,1751,1753,1756],{"class":162,"line":1731},33,[160,1733,1734],{"class":184},"    result ",[160,1736,249],{"class":173},[160,1738,1739],{"class":184}," CliRunner().invoke(app, [",[160,1741,1032],{"class":392},[160,1743,396],{"class":184},[160,1745,1195],{"class":392},[160,1747,396],{"class":184},[160,1749,1750],{"class":392},"\"--count\"",[160,1752,396],{"class":184},[160,1754,1755],{"class":392},"\"8\"",[160,1757,1605],{"class":184},[160,1759,1761,1763,1766,1768],{"class":162,"line":1760},34,[160,1762,1699],{"class":173},[160,1764,1765],{"class":184}," result.exit_code ",[160,1767,405],{"class":173},[160,1769,1770],{"class":177}," 0\n",[160,1772,1774,1776,1779,1782,1784,1786,1788,1791,1793,1795,1798,1800,1802,1804],{"class":162,"line":1773},35,[160,1775,1699],{"class":173},[160,1777,1778],{"class":392}," \"",[160,1780,1781],{"class":177},"\\x1b",[160,1783,858],{"class":392},[160,1785,509],{"class":173},[160,1787,438],{"class":173},[160,1789,1790],{"class":184}," result.output ",[160,1792,469],{"class":173},[160,1794,1778],{"class":392},[160,1796,1797],{"class":177},"\\r",[160,1799,858],{"class":392},[160,1801,509],{"class":173},[160,1803,438],{"class":173},[160,1805,1806],{"class":184}," result.output\n",[160,1808,1810,1812,1815,1817,1820,1823,1825,1828],{"class":162,"line":1809},36,[160,1811,1699],{"class":173},[160,1813,1814],{"class":184}," result.output.splitlines() ",[160,1816,405],{"class":173},[160,1818,1819],{"class":184}," [",[160,1821,1822],{"class":392},"\"upload: 2 of 8 done\"",[160,1824,396],{"class":184},[160,1826,1827],{"class":392},"\"upload: 4 of 8 done\"",[160,1829,1830],{"class":184},",\n",[160,1832,1834,1837,1839,1842],{"class":162,"line":1833},37,[160,1835,1836],{"class":392},"                                          \"upload: 6 of 8 done\"",[160,1838,396],{"class":184},[160,1840,1841],{"class":392},"\"upload: 8 of 8 done\"",[160,1843,1830],{"class":184},[160,1845,1847,1850],{"class":162,"line":1846},38,[160,1848,1849],{"class":392},"                                          \"OK: uploaded all files\"",[160,1851,1174],{"class":184},[10,1853,1854,1855,32],{},"The second test is the contract plain mode promises: no escape sequences, no carriage returns, and a short, readable transcript. It is also a good candidate for a ",[28,1856,1858],{"href":1857},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands\u002F","transcript test",[34,1860,1862],{"id":1861},"conclusion","Conclusion",[10,1864,1865,1866,1868,1869,1871,1872,1875],{},"Rich output and accessible output are compatible if the choice is explicit. Separate colour, animation and decoration; let Rich handle ",[14,1867,16],{}," and non-TTY output, and add a ",[14,1870,1257],{}," flag plus ",[14,1873,1874],{},"MYTOOL_PLAIN"," for people who need linear output on capable terminals; replace spinners with milestone lines, box tables with plain records and symbols with words; and test that plain mode emits nothing but text. Users of Emacs, IDE consoles, logs and screen readers all benefit.",[34,1877,1879],{"id":1878},"frequently-asked-questions","Frequently asked questions",[1881,1882,1884,1885,1887],"h3",{"id":1883},"is-termdumb-enough-for-screen-reader-users","Is ",[14,1886,16],{}," enough for screen-reader users?",[10,1889,1890],{},"Usually not. Setting it globally degrades other programs that the user relies on, such as editors. A tool-specific switch lets users opt into linear output for your CLI only.",[1881,1892,1894],{"id":1893},"do-screen-readers-handle-colour-codes","Do screen readers handle colour codes?",[10,1896,1897],{},"The escape sequences themselves are normally not spoken, but colour carries no meaning for a non-visual user. Plain mode can keep colour on; what matters is that words, not colours, carry the status.",[1881,1899,1901],{"id":1900},"what-about-progress-for-very-long-operations","What about progress for very long operations?",[10,1903,1904],{},"Milestones by percentage (25%, 50%, …) or by time (one line every 30 seconds) both work. Include an estimate of what remains when you can, since that is the information a progress bar conveys visually.",[1881,1906,1908],{"id":1907},"should-plain-mode-change-json-output","Should plain mode change JSON output?",[10,1910,1911],{},"No. Machine-readable formats are already linear and undecorated; plain mode only affects human-oriented output.",[1881,1913,1915],{"id":1914},"how-do-i-check-the-experience-with-a-real-screen-reader","How do I check the experience with a real screen reader?",[10,1917,1918],{},"Try it: VoiceOver is built into macOS, NVDA is free on Windows, and Orca ships with many Linux desktops. Run a few common commands in both modes and listen. Ten minutes of this usually reveals more than any checklist — a header row read before every table line, a spinner announced as punctuation, a status that only exists as a colour.",[1881,1920,1922],{"id":1921},"should-plain-mode-be-the-default-anywhere","Should plain mode be the default anywhere?",[10,1924,1925,1926,1929],{},"It is effectively the default whenever output is not a terminal, because Rich already stops animating and colouring there. Some teams also make it the default in CI logs for readability; the ",[14,1927,1928],{},"CI"," detection from the cross-platform topic makes that a one-line rule.",[34,1931,1933],{"id":1932},"related","Related",[39,1935,1936,1942,1947,1953,1959],{},[42,1937,1938,1939],{},"Up: ",[28,1940,1941],{"href":30},"Cross-platform terminal compatibility",[42,1943,1944],{},[28,1945,1946],{"href":54},"Respecting NO_COLOR and FORCE_COLOR",[42,1948,1949],{},[28,1950,1952],{"href":1951},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","Detecting CI environments and non-interactive shells",[42,1954,1955],{},[28,1956,1958],{"href":1957},"\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",[42,1960,1961],{},[28,1962,1964],{"href":1963},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles\u002F","Handling ANSI escape codes on Windows consoles",[1966,1967,1968],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":156,"searchDepth":170,"depth":170,"links":1970},[1971,1972,1973,1974,1975,1976,1977,1978,1987],{"id":36,"depth":170,"text":37},{"id":58,"depth":170,"text":59},{"id":108,"depth":170,"text":109},{"id":145,"depth":170,"text":146},{"id":1245,"depth":170,"text":1246},{"id":1296,"depth":170,"text":1297},{"id":1861,"depth":170,"text":1862},{"id":1878,"depth":170,"text":1879,"children":1979},[1980,1982,1983,1984,1985,1986],{"id":1883,"depth":188,"text":1981},"Is TERM=dumb enough for screen-reader users?",{"id":1893,"depth":188,"text":1894},{"id":1900,"depth":188,"text":1901},{"id":1907,"depth":188,"text":1908},{"id":1914,"depth":188,"text":1915},{"id":1921,"depth":188,"text":1922},{"id":1932,"depth":170,"text":1933},"2026-10-02","Make CLI output work without cursor tricks: honour TERM=dumb, offer a --plain mode for screen-reader users, replace spinners and box tables with linear text, and test both modes.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers",{"title":5,"description":1989},"advanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers\u002Findex",[1998,1999,2000,2001,2002],"accessibility","terminals","rich","output","ux","M-XYEpZwk5ZLDCQjIsZW10YKr5DzbtQ6SjFc212GS7o",[2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2168,2171,2174,2177,2180,2183,2186,2189,2192,2195,2198,2201,2204,2207,2210,2213,2216,2219,2222,2225,2228,2231,2234,2237,2240,2243,2246,2249,2252,2255,2258,2261,2264,2267,2270,2273,2276,2279,2282,2285,2288,2291,2294,2297,2300,2303,2306,2309,2312,2315,2318,2321,2324,2327,2330,2333,2336,2339,2342,2345,2348,2351,2354,2357,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2795,2798,2801,2804,2807,2810,2813,2816,2819,2822,2825,2828,2831,2834,2837,2840,2843,2846],{"path":2006,"title":2007},"\u002Fabout","About Python CLI Toolcraft",{"path":2009,"title":2010},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2012,"title":2013},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2015,"title":2016},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":2018,"title":2019},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2021,"title":2022},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2024,"title":2025},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2027,"title":2028},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2030,"title":2031},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2033,"title":2034},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2036,"title":2037},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2039,"title":2040},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2042,"title":2043},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":2045,"title":2046},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2048,"title":2049},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2051,"title":2052},"\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":2054,"title":2055},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2057,"title":2058},"\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":2060,"title":2061},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2063,"title":2064},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":2066,"title":2067},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2069,"title":2070},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2072,"title":2073},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2075,"title":2076},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2078,"title":2079},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2081,"title":2082},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2084,"title":2085},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2087,"title":2088},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1994,"title":5},{"path":2091,"title":2092},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2094,"title":2095},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2097,"title":2098},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":2100,"title":2101},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2103,"title":2104},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2106,"title":2107},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2109,"title":2110},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2112,"title":2113},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2115,"title":2116},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2118,"title":2119},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2121,"title":2122},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2124,"title":2125},"\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":2127,"title":2128},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2130,"title":2131},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2133,"title":2134},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2136,"title":2137},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2139,"title":2140},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2142,"title":2143},"\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":2145,"title":2146},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2148,"title":2149},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2151,"title":2152},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2154,"title":2155},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2157,"title":2158},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":2160,"title":2161},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2163,"title":2164},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2166,"title":2167},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2169,"title":2170},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":2172,"title":2173},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":2175,"title":2176},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2178,"title":2179},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":2181,"title":2182},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":2184,"title":2185},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2187,"title":2188},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2190,"title":2191},"\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":2193,"title":2194},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2196,"title":2197},"\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":2199,"title":2200},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2202,"title":2203},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2205,"title":2206},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2208,"title":2209},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2211,"title":2212},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2214,"title":2215},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2217,"title":2218},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":2220,"title":2221},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2223,"title":2224},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2226,"title":2227},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2229,"title":2230},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2232,"title":2233},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2235,"title":2236},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2238,"title":2239},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":2241,"title":2242},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2244,"title":2245},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2247,"title":2248},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2250,"title":2251},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2253,"title":2254},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2256,"title":2257},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2259,"title":2260},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2262,"title":2263},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2265,"title":2266},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2268,"title":2269},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2271,"title":2272},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2274,"title":2275},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2277,"title":2278},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2280,"title":2281},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2283,"title":2284},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2286,"title":2287},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2289,"title":2290},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2292,"title":2293},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2295,"title":2296},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2298,"title":2299},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2301,"title":2302},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2304,"title":2305},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2307,"title":2308},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2310,"title":2311},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2313,"title":2314},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2316,"title":2317},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2319,"title":2320},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2322,"title":2323},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2325,"title":2326},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2328,"title":2329},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2331,"title":2332},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2334,"title":2335},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2337,"title":2338},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2340,"title":2341},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2343,"title":2344},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2346,"title":2347},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2349,"title":2350},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2352,"title":2353},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2355,"title":2356},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2358,"title":2359},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2361,"title":2362},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2364,"title":2365},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2367,"title":2368},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2370,"title":2371},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2373,"title":2374},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2376,"title":2377},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2379,"title":2380},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2382,"title":2383},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2385,"title":2386},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2388,"title":2389},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2391,"title":2392},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2394,"title":2395},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2397,"title":2398},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2400,"title":2401},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2403,"title":2404},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2406,"title":2407},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2409,"title":2410},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2412,"title":2413},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2415,"title":2416},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2418,"title":2419},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":2421,"title":2422},"\u002F","Python CLI Toolcraft",{"path":2424,"title":2425},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2427,"title":2428},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2430,"title":2431},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2433,"title":2434},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2436,"title":2437},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2439,"title":2440},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2442,"title":2443},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2445,"title":2446},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2448,"title":2449},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2451,"title":2452},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2454,"title":2455},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2457,"title":2458},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2460,"title":2461},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2463,"title":2464},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2466,"title":2467},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2469,"title":2470},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2472,"title":2473},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2475,"title":2476},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2478,"title":2479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2481,"title":2482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2484,"title":2485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2487,"title":2488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2490,"title":2491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2493,"title":2494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2496,"title":2497},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2499,"title":2500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2502,"title":2503},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2505,"title":2506},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2508,"title":2509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2511,"title":2512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2514,"title":2515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2517,"title":2518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2520,"title":2521},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2523,"title":2524},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2526,"title":2527},"\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":2529,"title":2530},"\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":2532,"title":2533},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2535,"title":2536},"\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":2538,"title":2539},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2541,"title":2542},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2544,"title":2545},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2547,"title":2548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2550,"title":2551},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2553,"title":2554},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2556,"title":2557},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2559,"title":2560},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2562,"title":2563},"\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":2565,"title":2566},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2568,"title":2569},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2571,"title":2572},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2574,"title":2575},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2577,"title":2578},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2580,"title":2581},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2583,"title":2584},"\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":2586,"title":2587},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2589,"title":2590},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2592,"title":2593},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2595,"title":2596},"\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":2598,"title":2599},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2601,"title":2602},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2604,"title":2605},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2607,"title":2608},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2610,"title":2611},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2613,"title":2614},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2616,"title":2617},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2619,"title":2620},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2622,"title":2623},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2625,"title":2626},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2628,"title":2629},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2631,"title":2632},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2634,"title":2635},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2637,"title":2638},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2640,"title":2641},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2643,"title":2644},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2646,"title":2647},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2649,"title":2650},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2652,"title":2653},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2655,"title":2656},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2658,"title":2659},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2661,"title":2662},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2664,"title":2665},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2667,"title":2668},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2670,"title":2671},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2673,"title":2674},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2676,"title":2677},"\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":2679,"title":2680},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2682,"title":2683},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2685,"title":2686},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2688,"title":2689},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":2691,"title":2692},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2694,"title":2695},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":2697,"title":2698},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":2700,"title":2701},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2703,"title":2704},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2706,"title":2707},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":2709,"title":2710},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2712,"title":2713},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2715,"title":2716},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2718,"title":2719},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2721,"title":2722},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":2724,"title":2725},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2727,"title":2728},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2730,"title":2731},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":2733,"title":2734},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2736,"title":2737},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2739,"title":2740},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2742,"title":2743},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2745,"title":2746},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2748,"title":2749},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2751,"title":2752},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2754,"title":2755},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2757,"title":2758},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2760,"title":2761},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2763,"title":2764},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2766,"title":2767},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2769,"title":2770},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2772,"title":2773},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":2775,"title":2776},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":2778,"title":2779},"\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":2781,"title":2782},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2784,"title":2785},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":2787,"title":2788},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2790,"title":2791},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":2793,"title":2794},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":2796,"title":2797},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2799,"title":2800},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2802,"title":2803},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2805,"title":2806},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":2808,"title":2809},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2811,"title":2812},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":2814,"title":2815},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2817,"title":2818},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":2820,"title":2821},"\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":2823,"title":2824},"\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":2826,"title":2827},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2829,"title":2830},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":2832,"title":2833},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":2835,"title":2836},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2838,"title":2839},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2841,"title":2842},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2844,"title":2845},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2847,"title":2848},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537203]