[{"data":1,"prerenderedAt":2528},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002F":3,"content-directory":1981},{"id":4,"title":5,"body":6,"date":1967,"description":1968,"difficulty":1969,"draft":1970,"extension":1971,"meta":1972,"navigation":209,"path":1973,"seo":1974,"stem":1975,"tags":1976,"updated":1967,"__hash__":1980},"content\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Findex.md","Cross-Platform Terminal Compatibility for Python CLIs",{"type":7,"value":8,"toc":1941},"minimark",[9,26,48,52,57,121,125,128,147,150,154,165,168,797,808,813,816,1086,1111,1115,1118,1154,1177,1195,1199,1220,1306,1325,1329,1336,1352,1375,1378,1397,1401,1415,1419,1436,1447,1458,1462,1465,1499,1503,1514,1758,1769,1772,1776,1812,1816,1820,1823,1827,1837,1841,1844,1848,1866,1873,1876,1880,1897,1901,1937],[10,11,12,13,17,18,21,22,25],"p",{},"A command-line tool is developed in one terminal and used in dozens. The author's macOS terminal is wide, dark-themed, UTF-8 and full of colour. The tool's users run it in Windows Terminal, in the legacy Windows console, inside VS Code's integrated terminal, over SSH into an 80-column server session, inside tmux, in GitHub Actions logs, in cron jobs whose output goes to email, and piped into ",[14,15,16],"code",{},"jq",", ",[14,19,20],{},"less"," or a file. Each of those environments differs in what it can display, how wide it is, whether a person is watching, and what bytes it expects. Output that looks perfect on the author's machine crashes with ",[14,23,24],{},"UnicodeEncodeError"," on Windows, fills CI logs with thousands of progress-bar redraws, writes ANSI escape codes into data files, or wraps into an unreadable mess at 80 columns.",[10,27,28,29,32,33,36,37,42,43,47],{},"This topic covers making a Python CLI's terminal behaviour portable: text encoding (especially on Windows), colour and the ",[14,30,31],{},"NO_COLOR","\u002F",[14,34,35],{},"FORCE_COLOR"," conventions, adapting to terminal width, and detecting CI and non-interactive sessions — all tied together by one decision made at startup about how to render. It sits in the ",[38,39,41],"a",{"href":40},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing & User Experience"," section and builds on ",[38,44,46],{"href":45},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",", which covers the stream-level rules.",[49,50],"inline-diagram",{"name":51},"xp-topic-map",[53,54,56],"h2",{"id":55},"tldr","TL;DR",[58,59,60,76,92,102,115],"ul",{},[61,62,63,67,68,71,72,75],"li",{},[64,65,66],"strong",{},"Encode explicitly."," Write files with ",[14,69,70],{},"encoding=\"utf-8\"",", reconfigure stdout\u002Fstderr to UTF-8 with ",[14,73,74],{},"errors=\"replace\""," on Windows, and never let a symbol crash a command.",[61,77,78,81,82,85,86,88,89,91],{},[64,79,80],{},"Colour follows conventions."," A ",[14,83,84],{},"--color"," flag beats ",[14,87,31],{},", which beats ",[14,90,35],{},", which beats automatic TTY detection.",[61,93,94,97,98,101],{},[64,95,96],{},"Width comes from the terminal",", with ",[14,99,100],{},"COLUMNS"," and a fallback of 80; human views adapt, machine output never truncates.",[61,103,104,107,108,17,111,114],{},[64,105,106],{},"Detect non-interactive sessions"," — no TTY, ",[14,109,110],{},"CI=true",[14,112,113],{},"TERM=dumb"," — and switch off prompts, spinners and pagers.",[61,116,117,120],{},[64,118,119],{},"Decide once, at startup",", and pass the decision down, instead of checking environment variables in every print.",[53,122,124],{"id":123},"where-your-output-ends-up","Where your output ends up",[49,126],{"name":127},"xp-environments",[10,129,130,131,134,135,138,139,142,143,146],{},"It helps to think of each environment in terms of three questions: ",[64,132,133],{},"Is it a terminal?"," (a TTY, as reported by ",[14,136,137],{},"isatty()","), ",[64,140,141],{},"what can it display?"," (colours, Unicode, cursor movement), and ",[64,144,145],{},"is a person watching?"," Interactive terminals answer yes to all three; pipes and files answer no to all three; CI logs are the awkward middle — not a TTY, often rendered by a viewer that understands colour codes, and watched by a person only after the fact.",[10,148,149],{},"The failure pattern is always the same: code written for the first case running in one of the others. The fix is also always the same: detect the environment, and choose behaviour from it.",[53,151,153],{"id":152},"one-render-decision","One render decision",[10,155,156,157,160,161,164],{},"Scattering ",[14,158,159],{},"if sys.stdout.isatty()"," and ",[14,162,163],{},"os.environ.get(\"NO_COLOR\")"," checks through a codebase guarantees they disagree. Make the decision once, early, and store it:",[49,166],{"name":167},"xp-detect-flow",[169,170,175],"pre",{"className":171,"code":172,"language":173,"meta":174,"style":174},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fterminal.py\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport sys\nfrom dataclasses import dataclass\nfrom typing import Literal\n\nColorChoice = Literal[\"auto\", \"always\", \"never\"]\n\n\n@dataclass(frozen=True)\nclass RenderMode:\n    color: bool          # emit colour\u002Fstyles\n    interactive: bool    # a person at a terminal: prompts, spinners, live updates allowed\n    width: int           # columns for human-readable layout\n    ci: bool             # running under a CI system\n\n\ndef _truthy(name: str) -> bool:\n    return os.environ.get(name, \"\").lower() not in (\"\", \"0\", \"false\", \"no\")\n\n\ndef detect(color: ColorChoice = \"auto\", stream=None) -> RenderMode:\n    stream = stream or sys.stdout\n    is_tty = hasattr(stream, \"isatty\") and stream.isatty()\n    dumb = os.environ.get(\"TERM\") == \"dumb\"\n    ci = _truthy(\"CI\")\n    if color == \"always\":\n        use_color = True\n    elif color == \"never\" or os.environ.get(\"NO_COLOR\"):     # any non-empty value\n        use_color = False\n    elif _truthy(\"FORCE_COLOR\"):\n        use_color = True\n    else:\n        use_color = is_tty and not dumb\n    width = shutil.get_terminal_size(fallback=(80, 24)).columns\n    interactive = is_tty and sys.stdin.isatty() and not dumb and not ci\n    return RenderMode(color=use_color, interactive=interactive, width=max(width, 40), ci=ci)\n","python","",[14,176,177,186,204,211,220,228,236,249,262,267,296,301,306,328,340,352,363,375,386,391,396,418,461,466,471,498,515,541,563,579,595,606,633,643,656,665,673,691,720,749],{"__ignoreMap":174},[178,179,182],"span",{"class":180,"line":181},"line",1,[178,183,185],{"class":184},"sJ8bj","# src\u002Fmytool\u002Fterminal.py\n",[178,187,189,193,197,200],{"class":180,"line":188},2,[178,190,192],{"class":191},"szBVR","from",[178,194,196],{"class":195},"sj4cs"," __future__",[178,198,199],{"class":191}," import",[178,201,203],{"class":202},"sVt8B"," annotations\n",[178,205,207],{"class":180,"line":206},3,[178,208,210],{"emptyLinePlaceholder":209},true,"\n",[178,212,214,217],{"class":180,"line":213},4,[178,215,216],{"class":191},"import",[178,218,219],{"class":202}," os\n",[178,221,223,225],{"class":180,"line":222},5,[178,224,216],{"class":191},[178,226,227],{"class":202}," shutil\n",[178,229,231,233],{"class":180,"line":230},6,[178,232,216],{"class":191},[178,234,235],{"class":202}," sys\n",[178,237,239,241,244,246],{"class":180,"line":238},7,[178,240,192],{"class":191},[178,242,243],{"class":202}," dataclasses ",[178,245,216],{"class":191},[178,247,248],{"class":202}," dataclass\n",[178,250,252,254,257,259],{"class":180,"line":251},8,[178,253,192],{"class":191},[178,255,256],{"class":202}," typing ",[178,258,216],{"class":191},[178,260,261],{"class":202}," Literal\n",[178,263,265],{"class":180,"line":264},9,[178,266,210],{"emptyLinePlaceholder":209},[178,268,270,273,276,279,283,285,288,290,293],{"class":180,"line":269},10,[178,271,272],{"class":202},"ColorChoice ",[178,274,275],{"class":191},"=",[178,277,278],{"class":202}," Literal[",[178,280,282],{"class":281},"sZZnC","\"auto\"",[178,284,17],{"class":202},[178,286,287],{"class":281},"\"always\"",[178,289,17],{"class":202},[178,291,292],{"class":281},"\"never\"",[178,294,295],{"class":202},"]\n",[178,297,299],{"class":180,"line":298},11,[178,300,210],{"emptyLinePlaceholder":209},[178,302,304],{"class":180,"line":303},12,[178,305,210],{"emptyLinePlaceholder":209},[178,307,309,313,316,320,322,325],{"class":180,"line":308},13,[178,310,312],{"class":311},"sScJk","@dataclass",[178,314,315],{"class":202},"(",[178,317,319],{"class":318},"s4XuR","frozen",[178,321,275],{"class":191},[178,323,324],{"class":195},"True",[178,326,327],{"class":202},")\n",[178,329,331,334,337],{"class":180,"line":330},14,[178,332,333],{"class":191},"class",[178,335,336],{"class":311}," RenderMode",[178,338,339],{"class":202},":\n",[178,341,343,346,349],{"class":180,"line":342},15,[178,344,345],{"class":202},"    color: ",[178,347,348],{"class":195},"bool",[178,350,351],{"class":184},"          # emit colour\u002Fstyles\n",[178,353,355,358,360],{"class":180,"line":354},16,[178,356,357],{"class":202},"    interactive: ",[178,359,348],{"class":195},[178,361,362],{"class":184},"    # a person at a terminal: prompts, spinners, live updates allowed\n",[178,364,366,369,372],{"class":180,"line":365},17,[178,367,368],{"class":202},"    width: ",[178,370,371],{"class":195},"int",[178,373,374],{"class":184},"           # columns for human-readable layout\n",[178,376,378,381,383],{"class":180,"line":377},18,[178,379,380],{"class":202},"    ci: ",[178,382,348],{"class":195},[178,384,385],{"class":184},"             # running under a CI system\n",[178,387,389],{"class":180,"line":388},19,[178,390,210],{"emptyLinePlaceholder":209},[178,392,394],{"class":180,"line":393},20,[178,395,210],{"emptyLinePlaceholder":209},[178,397,399,402,405,408,411,414,416],{"class":180,"line":398},21,[178,400,401],{"class":191},"def",[178,403,404],{"class":311}," _truthy",[178,406,407],{"class":202},"(name: ",[178,409,410],{"class":195},"str",[178,412,413],{"class":202},") -> ",[178,415,348],{"class":195},[178,417,339],{"class":202},[178,419,421,424,427,430,433,436,439,442,444,446,449,451,454,456,459],{"class":180,"line":420},22,[178,422,423],{"class":191},"    return",[178,425,426],{"class":202}," os.environ.get(name, ",[178,428,429],{"class":281},"\"\"",[178,431,432],{"class":202},").lower() ",[178,434,435],{"class":191},"not",[178,437,438],{"class":191}," in",[178,440,441],{"class":202}," (",[178,443,429],{"class":281},[178,445,17],{"class":202},[178,447,448],{"class":281},"\"0\"",[178,450,17],{"class":202},[178,452,453],{"class":281},"\"false\"",[178,455,17],{"class":202},[178,457,458],{"class":281},"\"no\"",[178,460,327],{"class":202},[178,462,464],{"class":180,"line":463},23,[178,465,210],{"emptyLinePlaceholder":209},[178,467,469],{"class":180,"line":468},24,[178,470,210],{"emptyLinePlaceholder":209},[178,472,474,476,479,482,484,487,490,492,495],{"class":180,"line":473},25,[178,475,401],{"class":191},[178,477,478],{"class":311}," detect",[178,480,481],{"class":202},"(color: ColorChoice ",[178,483,275],{"class":191},[178,485,486],{"class":281}," \"auto\"",[178,488,489],{"class":202},", stream",[178,491,275],{"class":191},[178,493,494],{"class":195},"None",[178,496,497],{"class":202},") -> RenderMode:\n",[178,499,501,504,506,509,512],{"class":180,"line":500},26,[178,502,503],{"class":202},"    stream ",[178,505,275],{"class":191},[178,507,508],{"class":202}," stream ",[178,510,511],{"class":191},"or",[178,513,514],{"class":202}," sys.stdout\n",[178,516,518,521,523,526,529,532,535,538],{"class":180,"line":517},27,[178,519,520],{"class":202},"    is_tty ",[178,522,275],{"class":191},[178,524,525],{"class":195}," hasattr",[178,527,528],{"class":202},"(stream, ",[178,530,531],{"class":281},"\"isatty\"",[178,533,534],{"class":202},") ",[178,536,537],{"class":191},"and",[178,539,540],{"class":202}," stream.isatty()\n",[178,542,544,547,549,552,555,557,560],{"class":180,"line":543},28,[178,545,546],{"class":202},"    dumb ",[178,548,275],{"class":191},[178,550,551],{"class":202}," os.environ.get(",[178,553,554],{"class":281},"\"TERM\"",[178,556,534],{"class":202},[178,558,559],{"class":191},"==",[178,561,562],{"class":281}," \"dumb\"\n",[178,564,566,569,571,574,577],{"class":180,"line":565},29,[178,567,568],{"class":202},"    ci ",[178,570,275],{"class":191},[178,572,573],{"class":202}," _truthy(",[178,575,576],{"class":281},"\"CI\"",[178,578,327],{"class":202},[178,580,582,585,588,590,593],{"class":180,"line":581},30,[178,583,584],{"class":191},"    if",[178,586,587],{"class":202}," color ",[178,589,559],{"class":191},[178,591,592],{"class":281}," \"always\"",[178,594,339],{"class":202},[178,596,598,601,603],{"class":180,"line":597},31,[178,599,600],{"class":202},"        use_color ",[178,602,275],{"class":191},[178,604,605],{"class":195}," True\n",[178,607,609,612,614,616,619,622,624,627,630],{"class":180,"line":608},32,[178,610,611],{"class":191},"    elif",[178,613,587],{"class":202},[178,615,559],{"class":191},[178,617,618],{"class":281}," \"never\"",[178,620,621],{"class":191}," or",[178,623,551],{"class":202},[178,625,626],{"class":281},"\"NO_COLOR\"",[178,628,629],{"class":202},"):     ",[178,631,632],{"class":184},"# any non-empty value\n",[178,634,636,638,640],{"class":180,"line":635},33,[178,637,600],{"class":202},[178,639,275],{"class":191},[178,641,642],{"class":195}," False\n",[178,644,646,648,650,653],{"class":180,"line":645},34,[178,647,611],{"class":191},[178,649,573],{"class":202},[178,651,652],{"class":281},"\"FORCE_COLOR\"",[178,654,655],{"class":202},"):\n",[178,657,659,661,663],{"class":180,"line":658},35,[178,660,600],{"class":202},[178,662,275],{"class":191},[178,664,605],{"class":195},[178,666,668,671],{"class":180,"line":667},36,[178,669,670],{"class":191},"    else",[178,672,339],{"class":202},[178,674,676,678,680,683,685,688],{"class":180,"line":675},37,[178,677,600],{"class":202},[178,679,275],{"class":191},[178,681,682],{"class":202}," is_tty ",[178,684,537],{"class":191},[178,686,687],{"class":191}," not",[178,689,690],{"class":202}," dumb\n",[178,692,694,697,699,702,705,707,709,712,714,717],{"class":180,"line":693},38,[178,695,696],{"class":202},"    width ",[178,698,275],{"class":191},[178,700,701],{"class":202}," shutil.get_terminal_size(",[178,703,704],{"class":318},"fallback",[178,706,275],{"class":191},[178,708,315],{"class":202},[178,710,711],{"class":195},"80",[178,713,17],{"class":202},[178,715,716],{"class":195},"24",[178,718,719],{"class":202},")).columns\n",[178,721,723,726,728,730,732,735,737,739,742,744,746],{"class":180,"line":722},39,[178,724,725],{"class":202},"    interactive ",[178,727,275],{"class":191},[178,729,682],{"class":202},[178,731,537],{"class":191},[178,733,734],{"class":202}," sys.stdin.isatty() ",[178,736,537],{"class":191},[178,738,687],{"class":191},[178,740,741],{"class":202}," dumb ",[178,743,537],{"class":191},[178,745,687],{"class":191},[178,747,748],{"class":202}," ci\n",[178,750,752,754,757,760,762,765,768,770,773,776,778,781,784,787,789,792,794],{"class":180,"line":751},40,[178,753,423],{"class":191},[178,755,756],{"class":202}," RenderMode(",[178,758,759],{"class":318},"color",[178,761,275],{"class":191},[178,763,764],{"class":202},"use_color, ",[178,766,767],{"class":318},"interactive",[178,769,275],{"class":191},[178,771,772],{"class":202},"interactive, ",[178,774,775],{"class":318},"width",[178,777,275],{"class":191},[178,779,780],{"class":195},"max",[178,782,783],{"class":202},"(width, ",[178,785,786],{"class":195},"40",[178,788,138],{"class":202},[178,790,791],{"class":318},"ci",[178,793,275],{"class":191},[178,795,796],{"class":202},"ci)\n",[10,798,799,800,803,804,807],{},"Created once in the CLI's callback and stored on the context, ",[14,801,802],{},"RenderMode"," drives everything downstream: whether the Rich ",[14,805,806],{},"Console"," uses colour, whether a progress bar is shown or replaced by periodic log lines, whether prompts are allowed, and how wide tables may be. Each piece is explored in its own guide below.",[809,810,812],"h3",{"id":811},"wiring-the-decision-into-rich-and-the-command-layer","Wiring the decision into Rich and the command layer",[10,814,815],{},"The render mode is only useful if every piece of output goes through it. In practice that means building the tool's two consoles — one for results on stdout, one for everything else on stderr — from the mode, in the top-level callback, and passing them down:",[169,817,819],{"className":171,"code":818,"language":173,"meta":174,"style":174},"from typing import Annotated\n\nimport typer\nfrom rich.console import Console\n\nfrom mytool.terminal import ColorChoice, RenderMode, detect\n\napp = typer.Typer()\n\n\nclass UI:\n    def __init__(self, mode: RenderMode) -> None:\n        self.mode = mode\n        self.out = Console(no_color=not mode.color, width=mode.width, highlight=False)\n        self.err = Console(stderr=True, no_color=not mode.color, width=mode.width)\n\n\n@app.callback()\ndef main(\n    ctx: typer.Context,\n    color: Annotated[str, typer.Option(\"--color\", help=\"auto, always or never.\")] = \"auto\",\n) -> None:\n    ctx.obj = UI(detect(color))           # the one place the environment is inspected\n",[14,820,821,832,836,843,855,859,871,875,885,889,893,902,917,930,968,1001,1005,1009,1017,1027,1032,1065,1073],{"__ignoreMap":174},[178,822,823,825,827,829],{"class":180,"line":181},[178,824,192],{"class":191},[178,826,256],{"class":202},[178,828,216],{"class":191},[178,830,831],{"class":202}," Annotated\n",[178,833,834],{"class":180,"line":188},[178,835,210],{"emptyLinePlaceholder":209},[178,837,838,840],{"class":180,"line":206},[178,839,216],{"class":191},[178,841,842],{"class":202}," typer\n",[178,844,845,847,850,852],{"class":180,"line":213},[178,846,192],{"class":191},[178,848,849],{"class":202}," rich.console ",[178,851,216],{"class":191},[178,853,854],{"class":202}," Console\n",[178,856,857],{"class":180,"line":222},[178,858,210],{"emptyLinePlaceholder":209},[178,860,861,863,866,868],{"class":180,"line":230},[178,862,192],{"class":191},[178,864,865],{"class":202}," mytool.terminal ",[178,867,216],{"class":191},[178,869,870],{"class":202}," ColorChoice, RenderMode, detect\n",[178,872,873],{"class":180,"line":238},[178,874,210],{"emptyLinePlaceholder":209},[178,876,877,880,882],{"class":180,"line":251},[178,878,879],{"class":202},"app ",[178,881,275],{"class":191},[178,883,884],{"class":202}," typer.Typer()\n",[178,886,887],{"class":180,"line":264},[178,888,210],{"emptyLinePlaceholder":209},[178,890,891],{"class":180,"line":269},[178,892,210],{"emptyLinePlaceholder":209},[178,894,895,897,900],{"class":180,"line":298},[178,896,333],{"class":191},[178,898,899],{"class":311}," UI",[178,901,339],{"class":202},[178,903,904,907,910,913,915],{"class":180,"line":303},[178,905,906],{"class":191},"    def",[178,908,909],{"class":195}," __init__",[178,911,912],{"class":202},"(self, mode: RenderMode) -> ",[178,914,494],{"class":195},[178,916,339],{"class":202},[178,918,919,922,925,927],{"class":180,"line":308},[178,920,921],{"class":195},"        self",[178,923,924],{"class":202},".mode ",[178,926,275],{"class":191},[178,928,929],{"class":202}," mode\n",[178,931,932,934,937,939,942,945,948,951,953,955,958,961,963,966],{"class":180,"line":330},[178,933,921],{"class":195},[178,935,936],{"class":202},".out ",[178,938,275],{"class":191},[178,940,941],{"class":202}," Console(",[178,943,944],{"class":318},"no_color",[178,946,947],{"class":191},"=not",[178,949,950],{"class":202}," mode.color, ",[178,952,775],{"class":318},[178,954,275],{"class":191},[178,956,957],{"class":202},"mode.width, ",[178,959,960],{"class":318},"highlight",[178,962,275],{"class":191},[178,964,965],{"class":195},"False",[178,967,327],{"class":202},[178,969,970,972,975,977,979,982,984,986,988,990,992,994,996,998],{"class":180,"line":342},[178,971,921],{"class":195},[178,973,974],{"class":202},".err ",[178,976,275],{"class":191},[178,978,941],{"class":202},[178,980,981],{"class":318},"stderr",[178,983,275],{"class":191},[178,985,324],{"class":195},[178,987,17],{"class":202},[178,989,944],{"class":318},[178,991,947],{"class":191},[178,993,950],{"class":202},[178,995,775],{"class":318},[178,997,275],{"class":191},[178,999,1000],{"class":202},"mode.width)\n",[178,1002,1003],{"class":180,"line":354},[178,1004,210],{"emptyLinePlaceholder":209},[178,1006,1007],{"class":180,"line":365},[178,1008,210],{"emptyLinePlaceholder":209},[178,1010,1011,1014],{"class":180,"line":377},[178,1012,1013],{"class":311},"@app.callback",[178,1015,1016],{"class":202},"()\n",[178,1018,1019,1021,1024],{"class":180,"line":388},[178,1020,401],{"class":191},[178,1022,1023],{"class":311}," main",[178,1025,1026],{"class":202},"(\n",[178,1028,1029],{"class":180,"line":393},[178,1030,1031],{"class":202},"    ctx: typer.Context,\n",[178,1033,1034,1037,1039,1042,1045,1047,1050,1052,1055,1058,1060,1062],{"class":180,"line":398},[178,1035,1036],{"class":202},"    color: Annotated[",[178,1038,410],{"class":195},[178,1040,1041],{"class":202},", typer.Option(",[178,1043,1044],{"class":281},"\"--color\"",[178,1046,17],{"class":202},[178,1048,1049],{"class":318},"help",[178,1051,275],{"class":191},[178,1053,1054],{"class":281},"\"auto, always or never.\"",[178,1056,1057],{"class":202},")] ",[178,1059,275],{"class":191},[178,1061,486],{"class":281},[178,1063,1064],{"class":202},",\n",[178,1066,1067,1069,1071],{"class":180,"line":420},[178,1068,413],{"class":202},[178,1070,494],{"class":195},[178,1072,339],{"class":202},[178,1074,1075,1078,1080,1083],{"class":180,"line":463},[178,1076,1077],{"class":202},"    ctx.obj ",[178,1079,275],{"class":191},[178,1081,1082],{"class":202}," UI(detect(color))           ",[178,1084,1085],{"class":184},"# the one place the environment is inspected\n",[10,1087,1088,1089,1092,1093,1096,1097,1100,1101,1104,1105,1107,1108,1110],{},"Commands then call ",[14,1090,1091],{},"ctx.obj.out.print(...)"," for results and ",[14,1094,1095],{},"ctx.obj.err.print(...)"," for narration, and ask ",[14,1098,1099],{},"ctx.obj.mode.interactive"," before prompting or starting a live display. Nothing below the callback reads ",[14,1102,1103],{},"os.environ"," or calls ",[14,1106,137],{}," again, so the behaviour is consistent across commands and trivially testable: construct a ",[14,1109,802],{}," by hand and pass it in. The same approach keeps libraries honest — core modules never print, so they cannot bypass the decision.",[53,1112,1114],{"id":1113},"pagers-long-output-and-line-endings","Pagers, long output and line endings",[10,1116,1117],{},"Two more portability details catch tools that produce a lot of output.",[10,1119,1120,1123,1124,1126,1127,1130,1131,1134,1135,1138,1139,1142,1143,1146,1147,1150,1151,1153],{},[64,1121,1122],{},"Pagers."," Long help text, logs or reports are easier to read through ",[14,1125,20],{},", and Click offers ",[14,1128,1129],{},"click.echo_via_pager()",". Only page when the mode is interactive; honour the user's ",[14,1132,1133],{},"PAGER"," variable (and ",[14,1136,1137],{},"LESS"," options); and let ",[14,1140,1141],{},"--no-pager"," or ",[14,1144,1145],{},"PAGER=cat"," disable it. A pager opened in CI, or in a pipe, either hangs or dumps control sequences into the log. On Windows, ",[14,1148,1149],{},"more"," is the fallback pager and behaves differently from ",[14,1152,20],{},", which is another reason to keep paging optional.",[10,1155,1156,1159,1160,1163,1164,1167,1168,1171,1172,1176],{},[64,1157,1158],{},"Line endings."," Text written to stdout in text mode on Windows gets ",[14,1161,1162],{},"\\r\\n"," line endings, which is what Windows users expect in a console and in files they open in Notepad — but not what a Linux tool downstream in a pipeline expects. For machine-readable output that may cross platforms, such as NDJSON or CSV consumed by other programs, consider writing ",[14,1165,1166],{},"\\n"," explicitly by reconfiguring the stream with ",[14,1169,1170],{},"newline=\"\\n\"",", and document the choice. Files your tool generates should follow the same rule, as discussed in ",[38,1173,1175],{"href":1174},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002F","filesystem paths and atomic writes",".",[10,1178,1179,1182,1183,1186,1187,1190,1191,1176],{},[64,1180,1181],{},"Paths in output."," Printing ",[14,1184,1185],{},"str(path)"," gives backslashes on Windows, which is right for users but breaks consumers that parse paths. Use native paths in human output and ",[14,1188,1189],{},"Path.as_posix()"," in machine output, as described in ",[38,1192,1194],{"href":1193},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib\u002F","cross-platform paths with pathlib",[53,1196,1198],{"id":1197},"encoding-the-windows-problem","Encoding: the Windows problem",[10,1200,1201,1202,1205,1206,1209,1210,1142,1213,1216,1217,1219],{},"On Linux and macOS, Python's standard streams are UTF-8 in practically every environment. On Windows the story is more complicated. The modern console handles Unicode — Python writes to it with wide-character APIs — but when output is ",[64,1203,1204],{},"redirected"," to a file or pipe, Python encodes it with the locale's legacy code page, typically ",[14,1207,1208],{},"cp1252"," in Western Europe and the Americas. Printing ",[14,1211,1212],{},"✓",[14,1214,1215],{},"→"," or a user's name with characters outside that code page then raises ",[14,1218,24],{}," — only when redirected, which is precisely the case that escapes manual testing.",[169,1221,1223],{"className":171,"code":1222,"language":173,"meta":174,"style":174},"import sys\n\nfor stream in (sys.stdout, sys.stderr):\n    if hasattr(stream, \"reconfigure\") and (stream.encoding or \"\").lower() != \"utf-8\":\n        stream.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n",[14,1224,1225,1231,1235,1248,1281],{"__ignoreMap":174},[178,1226,1227,1229],{"class":180,"line":181},[178,1228,216],{"class":191},[178,1230,235],{"class":202},[178,1232,1233],{"class":180,"line":188},[178,1234,210],{"emptyLinePlaceholder":209},[178,1236,1237,1240,1242,1245],{"class":180,"line":206},[178,1238,1239],{"class":191},"for",[178,1241,508],{"class":202},[178,1243,1244],{"class":191},"in",[178,1246,1247],{"class":202}," (sys.stdout, sys.stderr):\n",[178,1249,1250,1252,1254,1256,1259,1261,1263,1266,1268,1271,1273,1276,1279],{"class":180,"line":213},[178,1251,584],{"class":191},[178,1253,525],{"class":195},[178,1255,528],{"class":202},[178,1257,1258],{"class":281},"\"reconfigure\"",[178,1260,534],{"class":202},[178,1262,537],{"class":191},[178,1264,1265],{"class":202}," (stream.encoding ",[178,1267,511],{"class":191},[178,1269,1270],{"class":281}," \"\"",[178,1272,432],{"class":202},[178,1274,1275],{"class":191},"!=",[178,1277,1278],{"class":281}," \"utf-8\"",[178,1280,339],{"class":202},[178,1282,1283,1286,1289,1291,1294,1296,1299,1301,1304],{"class":180,"line":222},[178,1284,1285],{"class":202},"        stream.reconfigure(",[178,1287,1288],{"class":318},"encoding",[178,1290,275],{"class":191},[178,1292,1293],{"class":281},"\"utf-8\"",[178,1295,17],{"class":202},[178,1297,1298],{"class":318},"errors",[178,1300,275],{"class":191},[178,1302,1303],{"class":281},"\"replace\"",[178,1305,327],{"class":202},[10,1307,1308,1309,1311,1312,1315,1316,1320,1321,1324],{},"Reconfiguring the streams early in the entry point, combined with explicit ",[14,1310,70],{}," on every file your tool opens, eliminates the whole class of crash. Users can also opt the entire interpreter into UTF-8 with ",[14,1313,1314],{},"PYTHONUTF8=1",", and Python 3.15 is planned to make UTF-8 mode the default — but CLIs support older Python versions for years. ",[38,1317,1319],{"href":1318},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows\u002F","Fixing Unicode and encoding errors on Windows"," covers reading input as well as writing, ",[14,1322,1323],{},"subprocess"," output, and ASCII fallbacks for symbols.",[53,1326,1328],{"id":1327},"colour-by-convention","Colour, by convention",[10,1330,1331,1332,1335],{},"Colour makes interactive output easier to scan and makes everything else worse: escape codes in files, in ",[14,1333,1334],{},"grep"," output, in data piped to other programs. Two community conventions settle the question of who decides:",[58,1337,1338,1345],{},[61,1339,1340,1344],{},[64,1341,1342],{},[14,1343,31],{}," — when set to a non-empty value, the user never wants colour, from any tool.",[61,1346,1347,1351],{},[64,1348,1349],{},[14,1350,35],{}," — when set, emit colour even though the output is not a terminal; useful for CI log viewers that render ANSI codes.",[10,1353,1354,1355,1358,1359,1362,1363,1366,1367,1369,1370,1374],{},"A tool should honour both, let an explicit ",[14,1356,1357],{},"--color=always|never|auto"," flag override them, and otherwise colour only when writing to a terminal whose ",[14,1360,1361],{},"TERM"," is not ",[14,1364,1365],{},"dumb",". Rich's ",[14,1368,806],{}," implements this logic already; the mistake to avoid is writing raw escape codes that bypass it. ",[38,1371,1373],{"href":1372},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color\u002F","Respecting NO_COLOR and FORCE_COLOR"," builds the flag and wires it into Rich and Click.",[53,1376,1377],{"id":775},"Width",[10,1379,1380,1381,1384,1385,1387,1388,1391,1392,1396],{},"Human-readable output — tables, wrapped help text, progress bars — needs to know how wide the terminal is. ",[14,1382,1383],{},"shutil.get_terminal_size()"," checks the ",[14,1386,100],{}," environment variable, then asks the terminal, and falls back to a default when there is no terminal. Layouts should adapt: wrap long text, truncate identifiers with an ellipsis (with a ",[14,1389,1390],{},"--wide"," option to show them in full), drop low-priority columns, or switch to a vertical layout on very narrow screens. Machine-readable output must never be truncated to fit a width. ",[38,1393,1395],{"href":1394},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width\u002F","Adapting output to terminal width"," works through each strategy with Rich tables.",[53,1398,1400],{"id":1399},"nobody-watching-ci-and-non-interactive-shells","Nobody watching: CI and non-interactive shells",[10,1402,1403,1404,1406,1407,1409,1410,1414],{},"The most expensive terminal mistake is a prompt in a CI job: nothing fails, the job simply waits until its timeout, often an hour later. Close behind are progress bars and spinners that redraw hundreds of times per second in a log file, and pagers that wait for a keypress nobody will press. A non-interactive session is signalled by stdin or stdout not being a TTY, by ",[14,1405,110],{}," (set by GitHub Actions, GitLab CI and most others), and by ",[14,1408,113],{},". In that mode a CLI should fail fast rather than prompt, replace animated progress with occasional timestamped lines, disable pagers, and make exit codes say exactly what happened. ",[38,1411,1413],{"href":1412},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","Detecting CI environments and non-interactive shells"," implements it, including the GitHub Actions log grouping and annotation syntax that makes CI output genuinely nicer.",[53,1416,1418],{"id":1417},"richer-capabilities-colour-depth-and-hyperlinks","Richer capabilities: colour depth and hyperlinks",[10,1420,1421,1422,1425,1426,17,1429,17,1432,1435],{},"Terminals differ in more than \"colour or not\". Some support only the basic 8 or 16 ANSI colours, many support 256, and most modern ones support 24-bit truecolour, usually advertised with ",[14,1423,1424],{},"COLORTERM=truecolor",". Rich detects the depth and downgrades colours automatically, so a palette designed in truecolour still produces sensible output on a 16-colour console — one more reason to route styling through Rich rather than hand-written escape codes. When choosing colours, prefer the named ANSI colours (",[14,1427,1428],{},"red",[14,1430,1431],{},"green",[14,1433,1434],{},"yellow",") for status, since terminal themes remap them to fit light and dark backgrounds, and reserve exact RGB values for decoration.",[10,1437,1438,1439,1442,1443,1446],{},"Modern terminals also support ",[64,1440,1441],{},"clickable hyperlinks"," via the OSC 8 escape sequence, which Rich exposes as ",[14,1444,1445],{},"[link=https:\u002F\u002F...]text[\u002Flink]",". Linking a build ID to its web page or an error to its documentation is a genuine convenience — and in terminals that do not support links the text simply appears without them. As with colour, hyperlinks are markup that must never reach files or pipes, so they belong to the same render decision.",[10,1448,1449,1450,1453,1454,1176],{},"Finally, remember that the ",[64,1451,1452],{},"terminal theme is not yours to choose",". Users run light and dark themes, high-contrast themes and custom palettes. Avoid dim grey text for important information, never use colour as the only signal, and test output in both a light and a dark theme at least once. These are small habits, but they are the difference between output that is pleasant everywhere and output that is illegible on half your users' screens. The broader styling approach is covered in ",[38,1455,1457],{"href":1456},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently\u002F","theming Rich output consistently",[53,1459,1461],{"id":1460},"symbols-glyphs-and-fonts","Symbols, glyphs and fonts",[10,1463,1464],{},"Beyond encoding, there is a quieter compatibility question: can the user's terminal font display the characters you print? Box-drawing characters (used by Rich tables and panels) are safe almost everywhere. Arrows, check marks and ballot boxes are safe in modern terminals. Emoji are the risky category: they render at inconsistent widths, break column alignment, and show as empty boxes in older consoles and many server fonts. A few rules keep output legible everywhere:",[58,1466,1467,1473,1479],{},[61,1468,1469,1472],{},[64,1470,1471],{},"Never let a symbol carry meaning alone."," \"✓ passed\" and \"✗ failed\" read correctly even if the symbol renders as a box; a bare \"✓\" does not.",[61,1474,1475,1478],{},[64,1476,1477],{},"Prefer simple symbols to emoji"," in anything tabular, where width consistency matters.",[61,1480,1481,1484,1485,160,1488,1491,1492,160,1495,1498],{},[64,1482,1483],{},"Offer an ASCII mode."," When the output encoding cannot represent a symbol, fall back to ",[14,1486,1487],{},"[ok]",[14,1489,1490],{},"[FAIL]","; Rich's ",[14,1493,1494],{},"safe_box",[14,1496,1497],{},"legacy_windows"," handling cover tables and panels automatically.",[53,1500,1502],{"id":1501},"testing-across-environments","Testing across environments",[10,1504,1505,1506,1509,1510,1513],{},"Terminal behaviour depends on environment variables and stream types, both of which tests can control. ",[14,1507,1508],{},"CliRunner"," gives your command non-TTY streams and accepts an ",[14,1511,1512],{},"env="," mapping, so every mode is reachable from a unit test:",[169,1515,1517],{"className":171,"code":1516,"language":173,"meta":174,"style":174},"from typer.testing import CliRunner\n\nfrom mytool.cli import app\n\nrunner = CliRunner()\n\n\ndef test_no_color_means_no_escape_codes():\n    result = runner.invoke(app, [\"status\", \"--color\", \"auto\"], env={\"NO_COLOR\": \"1\"})\n    assert \"\\x1b[\" not in result.output\n\n\ndef test_force_color_in_ci():\n    result = runner.invoke(app, [\"status\"], env={\"FORCE_COLOR\": \"1\", \"CI\": \"true\"})\n    assert \"\\x1b[\" in result.output\n\n\ndef test_narrow_terminal_does_not_crash():\n    assert runner.invoke(app, [\"status\"], env={\"COLUMNS\": \"40\"}).exit_code == 0\n",[14,1518,1519,1531,1535,1547,1551,1561,1565,1569,1579,1622,1643,1647,1651,1660,1695,1709,1713,1717,1726],{"__ignoreMap":174},[178,1520,1521,1523,1526,1528],{"class":180,"line":181},[178,1522,192],{"class":191},[178,1524,1525],{"class":202}," typer.testing ",[178,1527,216],{"class":191},[178,1529,1530],{"class":202}," CliRunner\n",[178,1532,1533],{"class":180,"line":188},[178,1534,210],{"emptyLinePlaceholder":209},[178,1536,1537,1539,1542,1544],{"class":180,"line":206},[178,1538,192],{"class":191},[178,1540,1541],{"class":202}," mytool.cli ",[178,1543,216],{"class":191},[178,1545,1546],{"class":202}," app\n",[178,1548,1549],{"class":180,"line":213},[178,1550,210],{"emptyLinePlaceholder":209},[178,1552,1553,1556,1558],{"class":180,"line":222},[178,1554,1555],{"class":202},"runner ",[178,1557,275],{"class":191},[178,1559,1560],{"class":202}," CliRunner()\n",[178,1562,1563],{"class":180,"line":230},[178,1564,210],{"emptyLinePlaceholder":209},[178,1566,1567],{"class":180,"line":238},[178,1568,210],{"emptyLinePlaceholder":209},[178,1570,1571,1573,1576],{"class":180,"line":251},[178,1572,401],{"class":191},[178,1574,1575],{"class":311}," test_no_color_means_no_escape_codes",[178,1577,1578],{"class":202},"():\n",[178,1580,1581,1584,1586,1589,1592,1594,1596,1598,1600,1603,1606,1608,1611,1613,1616,1619],{"class":180,"line":264},[178,1582,1583],{"class":202},"    result ",[178,1585,275],{"class":191},[178,1587,1588],{"class":202}," runner.invoke(app, [",[178,1590,1591],{"class":281},"\"status\"",[178,1593,17],{"class":202},[178,1595,1044],{"class":281},[178,1597,17],{"class":202},[178,1599,282],{"class":281},[178,1601,1602],{"class":202},"], ",[178,1604,1605],{"class":318},"env",[178,1607,275],{"class":191},[178,1609,1610],{"class":202},"{",[178,1612,626],{"class":281},[178,1614,1615],{"class":202},": ",[178,1617,1618],{"class":281},"\"1\"",[178,1620,1621],{"class":202},"})\n",[178,1623,1624,1627,1630,1633,1636,1638,1640],{"class":180,"line":269},[178,1625,1626],{"class":191},"    assert",[178,1628,1629],{"class":281}," \"",[178,1631,1632],{"class":195},"\\x1b",[178,1634,1635],{"class":281},"[\"",[178,1637,687],{"class":191},[178,1639,438],{"class":191},[178,1641,1642],{"class":202}," result.output\n",[178,1644,1645],{"class":180,"line":298},[178,1646,210],{"emptyLinePlaceholder":209},[178,1648,1649],{"class":180,"line":303},[178,1650,210],{"emptyLinePlaceholder":209},[178,1652,1653,1655,1658],{"class":180,"line":308},[178,1654,401],{"class":191},[178,1656,1657],{"class":311}," test_force_color_in_ci",[178,1659,1578],{"class":202},[178,1661,1662,1664,1666,1668,1670,1672,1674,1676,1678,1680,1682,1684,1686,1688,1690,1693],{"class":180,"line":330},[178,1663,1583],{"class":202},[178,1665,275],{"class":191},[178,1667,1588],{"class":202},[178,1669,1591],{"class":281},[178,1671,1602],{"class":202},[178,1673,1605],{"class":318},[178,1675,275],{"class":191},[178,1677,1610],{"class":202},[178,1679,652],{"class":281},[178,1681,1615],{"class":202},[178,1683,1618],{"class":281},[178,1685,17],{"class":202},[178,1687,576],{"class":281},[178,1689,1615],{"class":202},[178,1691,1692],{"class":281},"\"true\"",[178,1694,1621],{"class":202},[178,1696,1697,1699,1701,1703,1705,1707],{"class":180,"line":342},[178,1698,1626],{"class":191},[178,1700,1629],{"class":281},[178,1702,1632],{"class":195},[178,1704,1635],{"class":281},[178,1706,438],{"class":191},[178,1708,1642],{"class":202},[178,1710,1711],{"class":180,"line":354},[178,1712,210],{"emptyLinePlaceholder":209},[178,1714,1715],{"class":180,"line":365},[178,1716,210],{"emptyLinePlaceholder":209},[178,1718,1719,1721,1724],{"class":180,"line":377},[178,1720,401],{"class":191},[178,1722,1723],{"class":311}," test_narrow_terminal_does_not_crash",[178,1725,1578],{"class":202},[178,1727,1728,1730,1732,1734,1736,1738,1740,1742,1745,1747,1750,1753,1755],{"class":180,"line":388},[178,1729,1626],{"class":191},[178,1731,1588],{"class":202},[178,1733,1591],{"class":281},[178,1735,1602],{"class":202},[178,1737,1605],{"class":318},[178,1739,275],{"class":191},[178,1741,1610],{"class":202},[178,1743,1744],{"class":281},"\"COLUMNS\"",[178,1746,1615],{"class":202},[178,1748,1749],{"class":281},"\"40\"",[178,1751,1752],{"class":202},"}).exit_code ",[178,1754,559],{"class":191},[178,1756,1757],{"class":195}," 0\n",[10,1759,1760,1761,1764,1765,1176],{},"Add a Windows job to CI — ideally one that runs with output redirected and ",[14,1762,1763],{},"PYTHONUTF8=0"," — because encoding failures cannot be reproduced on Linux or macOS. The matrix setup is in ",[38,1766,1768],{"href":1767},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions\u002F","testing a CLI across Python versions with GitHub Actions",[49,1770],{"name":1771},"xp-checklist",[53,1773,1775],{"id":1774},"key-takeaways","Key takeaways",[58,1777,1778,1781,1787,1800,1803,1806],{},[61,1779,1780],{},"Output meets many environments; decide how to render once, at startup, from the stream type and environment.",[61,1782,1783,1784,1786],{},"Reconfigure standard streams to UTF-8 with ",[14,1785,74],{}," and open files with explicit encodings.",[61,1788,1789,1790,1792,1793,1792,1795,1797,1798,1176],{},"Honour ",[14,1791,84],{},", then ",[14,1794,31],{},[14,1796,35],{},", then TTY detection — ideally all through one Rich ",[14,1799,806],{},[61,1801,1802],{},"Read the terminal width, adapt human views, never truncate machine output.",[61,1804,1805],{},"In CI and non-interactive shells: no prompts, no animations, no pagers, clear exit codes.",[61,1807,1808,1809,1811],{},"Do not rely on symbols or colour alone to carry meaning; test every mode with ",[14,1810,1508],{}," and a Windows CI job.",[53,1813,1815],{"id":1814},"frequently-asked-questions","Frequently asked questions",[809,1817,1819],{"id":1818},"do-i-still-need-colorama-on-windows","Do I still need colorama on Windows?",[10,1821,1822],{},"Rarely. Windows 10 and later support ANSI escape sequences in the console once virtual-terminal processing is enabled, which Rich and Click handle for you. colorama remains useful only for very old Windows versions or code writing raw escape sequences.",[809,1824,1826],{"id":1825},"should-the-tool-detect-specific-terminals-like-vs-code-or-tmux","Should the tool detect specific terminals like VS Code or tmux?",[10,1828,1829,1830,1832,1833,1836],{},"Generally no. Detect capabilities — TTY, ",[14,1831,1361],{},", colour depth via ",[14,1834,1835],{},"COLORTERM"," — rather than products. Product checks break as terminals change; capability checks keep working.",[809,1838,1840],{"id":1839},"what-about-right-to-left-text-and-combining-characters","What about right-to-left text and combining characters?",[10,1842,1843],{},"Terminals vary widely in how they render bidirectional text and combining marks, and alignment is hard to get right. Rich measures cell widths correctly for most scripts; for data with lots of such text, prefer layouts that do not depend on column alignment.",[809,1845,1847],{"id":1846},"why-does-output-look-different-inside-docker","Why does output look different inside Docker?",[10,1849,1850,1853,1854,1857,1858,1861,1862,1865],{},[14,1851,1852],{},"docker run"," without ",[14,1855,1856],{},"-t"," gives the container no TTY, so a well-behaved CLI switches to plain, uncoloured output — which surprises people who expected the same output as on their laptop. That is correct behaviour; ",[14,1859,1860],{},"docker run -it"," allocates a terminal and restores the interactive mode, and ",[14,1863,1864],{},"FORCE_COLOR=1"," restores colour for log collectors that render it.",[809,1867,1869,1870,1872],{"id":1868},"is-termdumb-still-relevant","Is ",[14,1871,113],{}," still relevant?",[10,1874,1875],{},"Yes: it is set by Emacs shell buffers, some IDE consoles and several CI systems, and it means \"no cursor movement or colour\". Treat it like a non-interactive terminal for rendering purposes.",[809,1877,1879],{"id":1878},"how-do-i-let-users-override-automatic-detection","How do I let users override automatic detection?",[10,1881,1882,1883,17,1886,17,1889,1892,1893,1896],{},"With flags that beat the environment: ",[14,1884,1885],{},"--color=always|never",[14,1887,1888],{},"--no-input",[14,1890,1891],{},"--width N",", and ",[14,1894,1895],{},"--progress\u002F--no-progress",". Detection gives good defaults; flags give control.",[53,1898,1900],{"id":1899},"related","Related",[58,1902,1903,1908,1913,1917,1921,1925,1931],{},[61,1904,1905,1906],{},"Up: ",[38,1907,41],{"href":40},[61,1909,1910,1911],{},"Down: ",[38,1912,1319],{"href":1318},[61,1914,1910,1915],{},[38,1916,1373],{"href":1372},[61,1918,1910,1919],{},[38,1920,1395],{"href":1394},[61,1922,1910,1923],{},[38,1924,1413],{"href":1412},[61,1926,1927,1928],{},"Sideways: ",[38,1929,1930],{"href":45},"Working with stdin, stdout and pipes",[61,1932,1927,1933],{},[38,1934,1936],{"href":1935},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","Interactive terminal UI with Rich",[1938,1939,1940],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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":174,"searchDepth":188,"depth":188,"links":1942},[1943,1944,1945,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1966],{"id":55,"depth":188,"text":56},{"id":123,"depth":188,"text":124},{"id":152,"depth":188,"text":153,"children":1946},[1947],{"id":811,"depth":206,"text":812},{"id":1113,"depth":188,"text":1114},{"id":1197,"depth":188,"text":1198},{"id":1327,"depth":188,"text":1328},{"id":775,"depth":188,"text":1377},{"id":1399,"depth":188,"text":1400},{"id":1417,"depth":188,"text":1418},{"id":1460,"depth":188,"text":1461},{"id":1501,"depth":188,"text":1502},{"id":1774,"depth":188,"text":1775},{"id":1814,"depth":188,"text":1815,"children":1958},[1959,1960,1961,1962,1963,1965],{"id":1818,"depth":206,"text":1819},{"id":1825,"depth":206,"text":1826},{"id":1839,"depth":206,"text":1840},{"id":1846,"depth":206,"text":1847},{"id":1868,"depth":206,"text":1964},"Is TERM=dumb still relevant?",{"id":1878,"depth":206,"text":1879},{"id":1899,"depth":188,"text":1900},"2026-09-18","Make Python CLI output work everywhere: Windows encodings, colour conventions, terminal width, CI and non-interactive shells, and one render decision made at startup.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility",{"title":5,"description":1968},"advanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Findex",[1977,1978,1288,1979,791],"windows","terminal","colour","sWhq0VYZlqa3waq0UmIOBTRMA1soHAJIa6tf4rMEsV4",[1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,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],{"path":1983,"title":1984},"\u002Fabout","About Python CLI Toolcraft",{"path":1986,"title":1987},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1989,"title":1990},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1992,"title":1993},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1995,"title":1996},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1998,"title":1999},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2001,"title":2002},"\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":2004,"title":2005},"\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":2007,"title":2008},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2010,"title":2011},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2013,"title":2014},"\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":2016,"title":2017},"\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":2019,"title":2020},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2022,"title":2023},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2025,"title":2026},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2028,"title":2029},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2031,"title":2032},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2034,"title":2035},"\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":1973,"title":5},{"path":2038,"title":2039},"\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":2041,"title":2042},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2044,"title":2045},"\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":2047,"title":2048},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2050,"title":2051},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2053,"title":2054},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2056,"title":2057},"\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":2059,"title":2060},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2062,"title":2063},"\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":2065,"title":2066},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2068,"title":2069},"\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":2071,"title":2072},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2074,"title":2075},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2077,"title":2078},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2080,"title":2081},"\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":2083,"title":2084},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2086,"title":2087},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2089,"title":2090},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2092,"title":2093},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2095,"title":2096},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2098,"title":2099},"\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":2101,"title":2102},"\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":2104,"title":2105},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2107,"title":2108},"\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":2110,"title":2111},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2113,"title":2114},"\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":2116,"title":2117},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2119,"title":2120},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2122,"title":2123},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2125,"title":2126},"\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":2128,"title":2129},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2131,"title":2132},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2134,"title":2135},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2137,"title":2138},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2140,"title":2141},"\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":2143,"title":2144},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2146,"title":2147},"\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":2149,"title":2150},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2152,"title":2153},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2155,"title":2156},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2158,"title":2159},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2161,"title":2162},"\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":2164,"title":2165},"\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":2167,"title":2168},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2170,"title":2171},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2173,"title":2174},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2176,"title":2177},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2179,"title":2180},"\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":2182,"title":2183},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2185,"title":2186},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2188,"title":2189},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2191,"title":2192},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2194,"title":2195},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2197,"title":2198},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2200,"title":2201},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2203,"title":2204},"\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":2206,"title":2207},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2209,"title":2210},"\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":2212,"title":2213},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2215,"title":2216},"\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":2218,"title":2219},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2221,"title":2222},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2224,"title":2225},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2227,"title":2228},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2230,"title":2231},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2233,"title":2234},"\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":2236,"title":2237},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2239,"title":2240},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2242,"title":2243},"\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":2245,"title":2246},"\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":2248,"title":2249},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2251,"title":2252},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":32,"title":2254},"Python CLI Toolcraft",{"path":2256,"title":2257},"\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":2259,"title":2260},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2262,"title":2263},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2265,"title":2266},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2268,"title":2269},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2271,"title":2272},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2274,"title":2275},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2277,"title":2278},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2280,"title":2281},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2283,"title":2284},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2286,"title":2287},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2289,"title":2290},"\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":2292,"title":2293},"\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":2295,"title":2296},"\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":2298,"title":2299},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2301,"title":2302},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2304,"title":2305},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2307,"title":2308},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2310,"title":2311},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2313,"title":2314},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2316,"title":2317},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2319,"title":2320},"\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":2322,"title":2323},"\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":2325,"title":2326},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2328,"title":2329},"\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":2331,"title":2332},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2334,"title":2335},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2337,"title":2338},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2340,"title":2341},"\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":2343,"title":2344},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2346,"title":2347},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2349,"title":2350},"\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":2352,"title":2353},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2355,"title":2356},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2358,"title":2359},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2361,"title":2362},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2364,"title":2365},"\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":2367,"title":2368},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2370,"title":2371},"\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":2373,"title":2374},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2376,"title":2377},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2379,"title":2380},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2382,"title":2383},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2385,"title":2386},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2388,"title":2389},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2391,"title":2392},"\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":2394,"title":2395},"\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":2397,"title":2398},"\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":2400,"title":2401},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2403,"title":2404},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2406,"title":2407},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2409,"title":2410},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2412,"title":2413},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2415,"title":2416},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2418,"title":2419},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2421,"title":2422},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2424,"title":2425},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2427,"title":2428},"\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":2430,"title":2431},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2433,"title":2434},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2436,"title":2437},"\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":2439,"title":2440},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2442,"title":2443},"\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":2445,"title":2446},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2448,"title":2449},"\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":2451,"title":2452},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2454,"title":2455},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2457,"title":2458},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2460,"title":2461},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2463,"title":2464},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2466,"title":2467},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2469,"title":2470},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2472,"title":2473},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2475,"title":2476},"\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":2478,"title":2479},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2481,"title":2482},"\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":2484,"title":2485},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2487,"title":2488},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2490,"title":2491},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2493,"title":2494},"\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":2496,"title":2497},"\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":2499,"title":2500},"\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":2502,"title":2503},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2505,"title":2506},"\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":2508,"title":2509},"\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":2511,"title":2512},"\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":2514,"title":2515},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2517,"title":2518},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2520,"title":2521},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2523,"title":2524},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2526,"title":2527},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905044]