[{"data":1,"prerenderedAt":1695},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F":3,"content-directory":1448},{"id":4,"title":5,"body":6,"date":1432,"description":1433,"difficulty":1434,"draft":1435,"extension":1436,"meta":1437,"navigation":217,"path":1438,"seo":1439,"stem":1440,"tags":1441,"updated":1432,"__hash__":1447},"content\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002Findex.md","Detecting a TTY and Adapting Output",{"type":7,"value":8,"toc":1406},"minimark",[9,13,18,78,82,90,94,104,144,148,159,162,166,172,563,566,576,580,583,590,597,607,610,702,706,709,715,721,853,859,865,869,886,892,902,916,920,933,1172,1175,1234,1238,1241,1245,1253,1258,1265,1268,1275,1287,1295,1304,1308,1318,1322,1341,1345,1354,1358,1364,1368,1402],[10,11,12],"p",{},"The same command should behave differently when a person is watching and when it is running inside\na pipeline or a CI job. Colour, progress bars, prompts, table widths and pagers all depend on that\none question, and getting it wrong is how a tool ends up writing escape sequences into a log file\nor hanging a build waiting for an answer.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,33,47,60,67],"ul",{},[22,23,24,28,29,32],"li",{},[25,26,27],"code",{},"sys.stdout.isatty()"," for output decisions; ",[25,30,31],{},"sys.stdin.isatty()"," for input ones — they differ.",[22,34,35,36,42,43,46],{},"Honour ",[37,38,39],"strong",{},[25,40,41],{},"NO_COLOR"," and an explicit ",[25,44,45],{},"--color\u002F--no-color",", in that precedence.",[22,48,49,50,55,56,59],{},"Treat ",[37,51,52],{},[25,53,54],{},"CI"," and ",[25,57,58],{},"TERM=dumb"," as strong hints that nobody is watching.",[22,61,62,63,66],{},"Decide ",[37,64,65],{},"once",", in one helper, and let every renderer follow it.",[22,68,69,70,73,74,77],{},"Test both branches: under the runner, ",[25,71,72],{},"isatty"," is always ",[25,75,76],{},"False",".",[14,79,81],{"id":80},"prerequisites","Prerequisites",[10,83,84,85,77],{},"A CLI that prints something more elaborate than plain text — colour, a table, a progress display —\nand the stream discipline from\n",[86,87,89],"a",{"href":88},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",[14,91,93],{"id":92},"which-stream-to-ask","Which stream to ask",[10,95,96,97,55,100,103],{},"The mistake worth avoiding first: ",[25,98,99],{},"stdout",[25,101,102],{},"stdin"," answer different questions and are checked\nfor different reasons.",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-python shiki shiki-themes github-light github-dark","sys.stdout.isatty()     # should the RESULT be formatted for a human?\nsys.stderr.isatty()     # should PROGRESS be a live display?\nsys.stdin.isatty()      # is there a person who can answer a prompt?\n","python","",[25,112,113,126,135],{"__ignoreMap":110},[114,115,118,122],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"sVt8B","sys.stdout.isatty()     ",[114,123,125],{"class":124},"sJ8bj","# should the RESULT be formatted for a human?\n",[114,127,129,132],{"class":116,"line":128},2,[114,130,131],{"class":120},"sys.stderr.isatty()     ",[114,133,134],{"class":124},"# should PROGRESS be a live display?\n",[114,136,138,141],{"class":116,"line":137},3,[114,139,140],{"class":120},"sys.stdin.isatty()      ",[114,142,143],{"class":124},"# is there a person who can answer a prompt?\n",[145,146],"inline-diagram",{"name":147},"stream-question-matrix",[10,149,150,151,154,155,158],{},"They genuinely differ in practice. ",[25,152,153],{},"mytool export > data.json"," has a redirected stdout and a\nterminal stderr, so results should be plain while a progress bar is still welcome.\n",[25,156,157],{},"cat jobs.json | mytool apply -"," has piped stdin and terminal output, so prompting is impossible\neven though colour is fine.",[145,160],{"name":161},"tty-behaviour-matrix",[14,163,165],{"id":164},"one-helper-consulted-everywhere","One helper, consulted everywhere",[10,167,168,169,171],{},"Scattering ",[25,170,72],{}," checks through the codebase guarantees they drift. Decide once:",[105,173,175],{"className":107,"code":174,"language":109,"meta":110,"style":110},"# src\u002Fmytool\u002Fenvironment.py\nimport os\nimport sys\nfrom dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass Presentation:\n    colour: bool\n    live_progress: bool\n    can_prompt: bool\n    width: int | None          # None means \"fit the terminal\"\n\ndef detect(colour_flag: bool | None = None) -> Presentation:\n    ci = bool(os.environ.get(\"CI\"))\n    dumb = os.environ.get(\"TERM\") == \"dumb\"\n    no_colour_env = os.environ.get(\"NO_COLOR\") is not None\n\n    if colour_flag is not None:\n        colour = colour_flag                    # an explicit flag always wins\n    else:\n        colour = sys.stdout.isatty() and not (no_colour_env or dumb)\n\n    return Presentation(\n        colour=colour,\n        live_progress=sys.stderr.isatty() and not (ci or dumb),\n        can_prompt=sys.stdin.isatty() and not ci,\n        width=None if sys.stdout.isatty() else 80,\n    )\n",[25,176,177,182,191,198,212,219,243,255,264,272,280,298,303,330,351,374,398,403,420,434,442,466,471,480,491,514,532,557],{"__ignoreMap":110},[114,178,179],{"class":116,"line":117},[114,180,181],{"class":124},"# src\u002Fmytool\u002Fenvironment.py\n",[114,183,184,188],{"class":116,"line":128},[114,185,187],{"class":186},"szBVR","import",[114,189,190],{"class":120}," os\n",[114,192,193,195],{"class":116,"line":137},[114,194,187],{"class":186},[114,196,197],{"class":120}," sys\n",[114,199,201,204,207,209],{"class":116,"line":200},4,[114,202,203],{"class":186},"from",[114,205,206],{"class":120}," dataclasses ",[114,208,187],{"class":186},[114,210,211],{"class":120}," dataclass\n",[114,213,215],{"class":116,"line":214},5,[114,216,218],{"emptyLinePlaceholder":217},true,"\n",[114,220,222,226,229,233,236,240],{"class":116,"line":221},6,[114,223,225],{"class":224},"sScJk","@dataclass",[114,227,228],{"class":120},"(",[114,230,232],{"class":231},"s4XuR","frozen",[114,234,235],{"class":186},"=",[114,237,239],{"class":238},"sj4cs","True",[114,241,242],{"class":120},")\n",[114,244,246,249,252],{"class":116,"line":245},7,[114,247,248],{"class":186},"class",[114,250,251],{"class":224}," Presentation",[114,253,254],{"class":120},":\n",[114,256,258,261],{"class":116,"line":257},8,[114,259,260],{"class":120},"    colour: ",[114,262,263],{"class":238},"bool\n",[114,265,267,270],{"class":116,"line":266},9,[114,268,269],{"class":120},"    live_progress: ",[114,271,263],{"class":238},[114,273,275,278],{"class":116,"line":274},10,[114,276,277],{"class":120},"    can_prompt: ",[114,279,263],{"class":238},[114,281,283,286,289,292,295],{"class":116,"line":282},11,[114,284,285],{"class":120},"    width: ",[114,287,288],{"class":238},"int",[114,290,291],{"class":186}," |",[114,293,294],{"class":238}," None",[114,296,297],{"class":124},"          # None means \"fit the terminal\"\n",[114,299,301],{"class":116,"line":300},12,[114,302,218],{"emptyLinePlaceholder":217},[114,304,306,309,312,315,318,320,322,325,327],{"class":116,"line":305},13,[114,307,308],{"class":186},"def",[114,310,311],{"class":224}," detect",[114,313,314],{"class":120},"(colour_flag: ",[114,316,317],{"class":238},"bool",[114,319,291],{"class":186},[114,321,294],{"class":238},[114,323,324],{"class":186}," =",[114,326,294],{"class":238},[114,328,329],{"class":120},") -> Presentation:\n",[114,331,333,336,338,341,344,348],{"class":116,"line":332},14,[114,334,335],{"class":120},"    ci ",[114,337,235],{"class":186},[114,339,340],{"class":238}," bool",[114,342,343],{"class":120},"(os.environ.get(",[114,345,347],{"class":346},"sZZnC","\"CI\"",[114,349,350],{"class":120},"))\n",[114,352,354,357,359,362,365,368,371],{"class":116,"line":353},15,[114,355,356],{"class":120},"    dumb ",[114,358,235],{"class":186},[114,360,361],{"class":120}," os.environ.get(",[114,363,364],{"class":346},"\"TERM\"",[114,366,367],{"class":120},") ",[114,369,370],{"class":186},"==",[114,372,373],{"class":346}," \"dumb\"\n",[114,375,377,380,382,384,387,389,392,395],{"class":116,"line":376},16,[114,378,379],{"class":120},"    no_colour_env ",[114,381,235],{"class":186},[114,383,361],{"class":120},[114,385,386],{"class":346},"\"NO_COLOR\"",[114,388,367],{"class":120},[114,390,391],{"class":186},"is",[114,393,394],{"class":186}," not",[114,396,397],{"class":238}," None\n",[114,399,401],{"class":116,"line":400},17,[114,402,218],{"emptyLinePlaceholder":217},[114,404,406,409,412,414,416,418],{"class":116,"line":405},18,[114,407,408],{"class":186},"    if",[114,410,411],{"class":120}," colour_flag ",[114,413,391],{"class":186},[114,415,394],{"class":186},[114,417,294],{"class":238},[114,419,254],{"class":120},[114,421,423,426,428,431],{"class":116,"line":422},19,[114,424,425],{"class":120},"        colour ",[114,427,235],{"class":186},[114,429,430],{"class":120}," colour_flag                    ",[114,432,433],{"class":124},"# an explicit flag always wins\n",[114,435,437,440],{"class":116,"line":436},20,[114,438,439],{"class":186},"    else",[114,441,254],{"class":120},[114,443,445,447,449,452,455,457,460,463],{"class":116,"line":444},21,[114,446,425],{"class":120},[114,448,235],{"class":186},[114,450,451],{"class":120}," sys.stdout.isatty() ",[114,453,454],{"class":186},"and",[114,456,394],{"class":186},[114,458,459],{"class":120}," (no_colour_env ",[114,461,462],{"class":186},"or",[114,464,465],{"class":120}," dumb)\n",[114,467,469],{"class":116,"line":468},22,[114,470,218],{"emptyLinePlaceholder":217},[114,472,474,477],{"class":116,"line":473},23,[114,475,476],{"class":186},"    return",[114,478,479],{"class":120}," Presentation(\n",[114,481,483,486,488],{"class":116,"line":482},24,[114,484,485],{"class":231},"        colour",[114,487,235],{"class":186},[114,489,490],{"class":120},"colour,\n",[114,492,494,497,499,502,504,506,509,511],{"class":116,"line":493},25,[114,495,496],{"class":231},"        live_progress",[114,498,235],{"class":186},[114,500,501],{"class":120},"sys.stderr.isatty() ",[114,503,454],{"class":186},[114,505,394],{"class":186},[114,507,508],{"class":120}," (ci ",[114,510,462],{"class":186},[114,512,513],{"class":120}," dumb),\n",[114,515,517,520,522,525,527,529],{"class":116,"line":516},26,[114,518,519],{"class":231},"        can_prompt",[114,521,235],{"class":186},[114,523,524],{"class":120},"sys.stdin.isatty() ",[114,526,454],{"class":186},[114,528,394],{"class":186},[114,530,531],{"class":120}," ci,\n",[114,533,535,538,540,543,546,548,551,554],{"class":116,"line":534},27,[114,536,537],{"class":231},"        width",[114,539,235],{"class":186},[114,541,542],{"class":238},"None",[114,544,545],{"class":186}," if",[114,547,451],{"class":120},[114,549,550],{"class":186},"else",[114,552,553],{"class":238}," 80",[114,555,556],{"class":120},",\n",[114,558,560],{"class":116,"line":559},28,[114,561,562],{"class":120},"    )\n",[145,564],{"name":565},"tty-env-overrides",[10,567,568,569,571,572,575],{},"Build it once in the callback, put it on the context, and let renderers read it. Nothing else in\nthe program calls ",[25,570,72],{}," — which also means the whole behaviour can be tested by constructing a\n",[25,573,574],{},"Presentation"," directly, with no patching.",[14,577,579],{"id":578},"honouring-the-conventions","Honouring the conventions",[10,581,582],{},"Three environment variables are worth respecting because users already expect them:",[10,584,585,589],{},[37,586,587,77],{},[25,588,41],{}," Any value, including empty, means \"no colour anywhere\". It is a cross-tool\nconvention, it costs one line, and users who set it have usually set it because something in their\npipeline chokes on escape sequences.",[10,591,592,596],{},[37,593,594,77],{},[25,595,54],{}," Set by essentially every CI provider. It is not a statement about terminals — some CI\nrunners do allocate a TTY — but it is a reliable signal that nobody is watching interactively, which\nis what matters for live progress and prompts.",[10,598,599,603,604,606],{},[37,600,601,77],{},[25,602,58],{}," Set by editors' embedded terminals and some tooling. It means \"this terminal\ncannot handle cursor movement\", so it should disable live displays even if ",[25,605,72],{}," says true.",[10,608,609],{},"An explicit flag beats all of them:",[105,611,613],{"className":107,"code":612,"language":109,"meta":110,"style":110},"@app.callback()\ndef main(\n    ctx: typer.Context,\n    color: Annotated[bool | None, typer.Option(\"--color\u002F--no-color\", help=\"Force colour on or off.\")] = None,\n) -> None:\n    ctx.obj = detect(colour_flag=color)\n",[25,614,615,623,633,638,675,684],{"__ignoreMap":110},[114,616,617,620],{"class":116,"line":117},[114,618,619],{"class":224},"@app.callback",[114,621,622],{"class":120},"()\n",[114,624,625,627,630],{"class":116,"line":128},[114,626,308],{"class":186},[114,628,629],{"class":224}," main",[114,631,632],{"class":120},"(\n",[114,634,635],{"class":116,"line":137},[114,636,637],{"class":120},"    ctx: typer.Context,\n",[114,639,640,643,645,647,649,652,655,658,661,663,666,669,671,673],{"class":116,"line":200},[114,641,642],{"class":120},"    color: Annotated[",[114,644,317],{"class":238},[114,646,291],{"class":186},[114,648,294],{"class":238},[114,650,651],{"class":120},", typer.Option(",[114,653,654],{"class":346},"\"--color\u002F--no-color\"",[114,656,657],{"class":120},", ",[114,659,660],{"class":231},"help",[114,662,235],{"class":186},[114,664,665],{"class":346},"\"Force colour on or off.\"",[114,667,668],{"class":120},")] ",[114,670,235],{"class":186},[114,672,294],{"class":238},[114,674,556],{"class":120},[114,676,677,680,682],{"class":116,"line":214},[114,678,679],{"class":120},") -> ",[114,681,542],{"class":238},[114,683,254],{"class":120},[114,685,686,689,691,694,697,699],{"class":116,"line":221},[114,687,688],{"class":120},"    ctx.obj ",[114,690,235],{"class":186},[114,692,693],{"class":120}," detect(",[114,695,696],{"class":231},"colour_flag",[114,698,235],{"class":186},[114,700,701],{"class":120},"color)\n",[14,703,705],{"id":704},"what-each-branch-should-do","What each branch should do",[145,707],{"name":708},"output-mode-flow",[10,710,711,714],{},[37,712,713],{},"Colour off"," rather than \"colour on regardless\". Plain output is always readable; escape\nsequences in a log file are not.",[10,716,717,720],{},[37,718,719],{},"Progress becomes periodic lines"," rather than disappearing entirely. A CI job running a\ntwenty-minute command with no output looks hung to whoever is watching the build; one line a minute\nis enough:",[105,722,724],{"className":107,"code":723,"language":109,"meta":110,"style":110},"if presentation.live_progress:\n    with Progress(console=err) as progress:\n        ...\nelse:\n    for index, item in enumerate(items, start=1):\n        if index % 500 == 0:\n            err.print(f\"{index}\u002F{len(items)} processed\")\n",[25,725,726,734,756,761,767,795,817],{"__ignoreMap":110},[114,727,728,731],{"class":116,"line":117},[114,729,730],{"class":186},"if",[114,732,733],{"class":120}," presentation.live_progress:\n",[114,735,736,739,742,745,747,750,753],{"class":116,"line":128},[114,737,738],{"class":186},"    with",[114,740,741],{"class":120}," Progress(",[114,743,744],{"class":231},"console",[114,746,235],{"class":186},[114,748,749],{"class":120},"err) ",[114,751,752],{"class":186},"as",[114,754,755],{"class":120}," progress:\n",[114,757,758],{"class":116,"line":137},[114,759,760],{"class":238},"        ...\n",[114,762,763,765],{"class":116,"line":200},[114,764,550],{"class":186},[114,766,254],{"class":120},[114,768,769,772,775,778,781,784,787,789,792],{"class":116,"line":214},[114,770,771],{"class":186},"    for",[114,773,774],{"class":120}," index, item ",[114,776,777],{"class":186},"in",[114,779,780],{"class":238}," enumerate",[114,782,783],{"class":120},"(items, ",[114,785,786],{"class":231},"start",[114,788,235],{"class":186},[114,790,791],{"class":238},"1",[114,793,794],{"class":120},"):\n",[114,796,797,800,803,806,809,812,815],{"class":116,"line":221},[114,798,799],{"class":186},"        if",[114,801,802],{"class":120}," index ",[114,804,805],{"class":186},"%",[114,807,808],{"class":238}," 500",[114,810,811],{"class":186}," ==",[114,813,814],{"class":238}," 0",[114,816,254],{"class":120},[114,818,819,822,825,828,831,834,837,840,843,846,848,851],{"class":116,"line":245},[114,820,821],{"class":120},"            err.print(",[114,823,824],{"class":186},"f",[114,826,827],{"class":346},"\"",[114,829,830],{"class":238},"{",[114,832,833],{"class":120},"index",[114,835,836],{"class":238},"}",[114,838,839],{"class":346},"\u002F",[114,841,842],{"class":238},"{len",[114,844,845],{"class":120},"(items)",[114,847,836],{"class":238},[114,849,850],{"class":346}," processed\"",[114,852,242],{"class":120},[10,854,855,858],{},[37,856,857],{},"Prompts fail fast",", naming the flag that supplies the value, rather than blocking. A prompt in\nCI hangs the job until the pipeline times out, and the log gives no clue why.",[10,860,861,864],{},[37,862,863],{},"Tables get a fixed width"," so redirected output is stable and diffable. This is also what makes\nsnapshot testing of output possible at all.",[14,866,868],{"id":867},"ux-considerations","UX considerations",[10,870,871,874,875,878,879,882,883,77],{},[37,872,873],{},"Never make the human path the only path."," Every interactive behaviour needs a non-interactive\nequivalent: ",[25,876,877],{},"--yes"," for a confirmation, a flag for a prompted value, ",[25,880,881],{},"--format json"," for a table.\nTools that fail this are the ones people wrap in ",[25,884,885],{},"expect",[10,887,888,891],{},[37,889,890],{},"Do not detect a \"smart terminal\" and use it as licence."," Unicode box characters and 24-bit\ncolour render beautifully somewhere and badly elsewhere. Rich degrades sensibly; hand-rolled\ndetection usually does not.",[10,893,894,897,898,901],{},[37,895,896],{},"Make the decision visible."," A ",[25,899,900],{},"doctor"," command that prints the detected presentation — colour on\nor off, and why — turns \"why is my output plain\" into a five-second answer instead of a support\nthread.",[10,903,904,907,908,911,912,915],{},[37,905,906],{},"Respect the user's choice permanently."," If someone passes ",[25,909,910],{},"--no-color",", nothing later in the run\nmay re-enable it, including a library you call. Constructing the console once with ",[25,913,914],{},"no_color=True","\nis what enforces that.",[14,917,919],{"id":918},"testing-the-behaviour","Testing the behaviour",[10,921,922,923,926,927,73,930,932],{},"Under ",[25,924,925],{},"CliRunner",", streams are captured, so ",[25,928,929],{},"isatty()",[25,931,76],{}," — which means the\ninteractive branch is never exercised unless you make it so. Testing the helper directly is the\nclean way:",[105,934,936],{"className":107,"code":935,"language":109,"meta":110,"style":110},"import pytest\n\n@pytest.mark.parametrize(\"flag,tty,no_color_env,expected\", [\n    (None,  True,  False, True),     # terminal, nothing set: colour on\n    (None,  False, False, False),    # piped: colour off\n    (None,  True,  True,  False),    # NO_COLOR beats the terminal\n    (True,  False, True,  True),     # an explicit flag beats everything\n    (False, True,  False, False),\n])\ndef test_colour_decision(monkeypatch, flag, tty, no_color_env, expected):\n    monkeypatch.setattr(\"sys.stdout.isatty\", lambda: tty)\n    monkeypatch.delenv(\"NO_COLOR\", raising=False)\n    if no_color_env:\n        monkeypatch.setenv(\"NO_COLOR\", \"1\")\n\n    assert detect(colour_flag=flag).colour is expected\n",[25,937,938,945,949,962,988,1012,1035,1058,1079,1084,1094,1110,1128,1135,1149,1153],{"__ignoreMap":110},[114,939,940,942],{"class":116,"line":117},[114,941,187],{"class":186},[114,943,944],{"class":120}," pytest\n",[114,946,947],{"class":116,"line":128},[114,948,218],{"emptyLinePlaceholder":217},[114,950,951,954,956,959],{"class":116,"line":137},[114,952,953],{"class":224},"@pytest.mark.parametrize",[114,955,228],{"class":120},[114,957,958],{"class":346},"\"flag,tty,no_color_env,expected\"",[114,960,961],{"class":120},", [\n",[114,963,964,967,969,972,974,976,978,980,982,985],{"class":116,"line":200},[114,965,966],{"class":120},"    (",[114,968,542],{"class":238},[114,970,971],{"class":120},",  ",[114,973,239],{"class":238},[114,975,971],{"class":120},[114,977,76],{"class":238},[114,979,657],{"class":120},[114,981,239],{"class":238},[114,983,984],{"class":120},"),     ",[114,986,987],{"class":124},"# terminal, nothing set: colour on\n",[114,989,990,992,994,996,998,1000,1002,1004,1006,1009],{"class":116,"line":214},[114,991,966],{"class":120},[114,993,542],{"class":238},[114,995,971],{"class":120},[114,997,76],{"class":238},[114,999,657],{"class":120},[114,1001,76],{"class":238},[114,1003,657],{"class":120},[114,1005,76],{"class":238},[114,1007,1008],{"class":120},"),    ",[114,1010,1011],{"class":124},"# piped: colour off\n",[114,1013,1014,1016,1018,1020,1022,1024,1026,1028,1030,1032],{"class":116,"line":221},[114,1015,966],{"class":120},[114,1017,542],{"class":238},[114,1019,971],{"class":120},[114,1021,239],{"class":238},[114,1023,971],{"class":120},[114,1025,239],{"class":238},[114,1027,971],{"class":120},[114,1029,76],{"class":238},[114,1031,1008],{"class":120},[114,1033,1034],{"class":124},"# NO_COLOR beats the terminal\n",[114,1036,1037,1039,1041,1043,1045,1047,1049,1051,1053,1055],{"class":116,"line":245},[114,1038,966],{"class":120},[114,1040,239],{"class":238},[114,1042,971],{"class":120},[114,1044,76],{"class":238},[114,1046,657],{"class":120},[114,1048,239],{"class":238},[114,1050,971],{"class":120},[114,1052,239],{"class":238},[114,1054,984],{"class":120},[114,1056,1057],{"class":124},"# an explicit flag beats everything\n",[114,1059,1060,1062,1064,1066,1068,1070,1072,1074,1076],{"class":116,"line":257},[114,1061,966],{"class":120},[114,1063,76],{"class":238},[114,1065,657],{"class":120},[114,1067,239],{"class":238},[114,1069,971],{"class":120},[114,1071,76],{"class":238},[114,1073,657],{"class":120},[114,1075,76],{"class":238},[114,1077,1078],{"class":120},"),\n",[114,1080,1081],{"class":116,"line":266},[114,1082,1083],{"class":120},"])\n",[114,1085,1086,1088,1091],{"class":116,"line":274},[114,1087,308],{"class":186},[114,1089,1090],{"class":224}," test_colour_decision",[114,1092,1093],{"class":120},"(monkeypatch, flag, tty, no_color_env, expected):\n",[114,1095,1096,1099,1102,1104,1107],{"class":116,"line":282},[114,1097,1098],{"class":120},"    monkeypatch.setattr(",[114,1100,1101],{"class":346},"\"sys.stdout.isatty\"",[114,1103,657],{"class":120},[114,1105,1106],{"class":186},"lambda",[114,1108,1109],{"class":120},": tty)\n",[114,1111,1112,1115,1117,1119,1122,1124,1126],{"class":116,"line":300},[114,1113,1114],{"class":120},"    monkeypatch.delenv(",[114,1116,386],{"class":346},[114,1118,657],{"class":120},[114,1120,1121],{"class":231},"raising",[114,1123,235],{"class":186},[114,1125,76],{"class":238},[114,1127,242],{"class":120},[114,1129,1130,1132],{"class":116,"line":305},[114,1131,408],{"class":186},[114,1133,1134],{"class":120}," no_color_env:\n",[114,1136,1137,1140,1142,1144,1147],{"class":116,"line":332},[114,1138,1139],{"class":120},"        monkeypatch.setenv(",[114,1141,386],{"class":346},[114,1143,657],{"class":120},[114,1145,1146],{"class":346},"\"1\"",[114,1148,242],{"class":120},[114,1150,1151],{"class":116,"line":353},[114,1152,218],{"emptyLinePlaceholder":217},[114,1154,1155,1158,1160,1162,1164,1167,1169],{"class":116,"line":376},[114,1156,1157],{"class":186},"    assert",[114,1159,693],{"class":120},[114,1161,696],{"class":231},[114,1163,235],{"class":186},[114,1165,1166],{"class":120},"flag).colour ",[114,1168,391],{"class":186},[114,1170,1171],{"class":120}," expected\n",[10,1173,1174],{},"Five cases, no runner, and they pin the precedence rule that is otherwise easy to get backwards.\nThen one runner test proves the wiring:",[105,1176,1178],{"className":107,"code":1177,"language":109,"meta":110,"style":110},"def test_no_color_flag_reaches_the_output(cli):\n    result = cli.invoke(app, [\"--no-color\", \"status\"])\n    assert \"\\x1b[\" not in result.stdout          # no escape sequences at all\n",[25,1179,1180,1190,1210],{"__ignoreMap":110},[114,1181,1182,1184,1187],{"class":116,"line":117},[114,1183,308],{"class":186},[114,1185,1186],{"class":224}," test_no_color_flag_reaches_the_output",[114,1188,1189],{"class":120},"(cli):\n",[114,1191,1192,1195,1197,1200,1203,1205,1208],{"class":116,"line":128},[114,1193,1194],{"class":120},"    result ",[114,1196,235],{"class":186},[114,1198,1199],{"class":120}," cli.invoke(app, [",[114,1201,1202],{"class":346},"\"--no-color\"",[114,1204,657],{"class":120},[114,1206,1207],{"class":346},"\"status\"",[114,1209,1083],{"class":120},[114,1211,1212,1214,1217,1220,1223,1225,1228,1231],{"class":116,"line":137},[114,1213,1157],{"class":186},[114,1215,1216],{"class":346}," \"",[114,1218,1219],{"class":238},"\\x1b",[114,1221,1222],{"class":346},"[\"",[114,1224,394],{"class":186},[114,1226,1227],{"class":186}," in",[114,1229,1230],{"class":120}," result.stdout          ",[114,1232,1233],{"class":124},"# no escape sequences at all\n",[14,1235,1237],{"id":1236},"conclusion","Conclusion",[10,1239,1240],{},"One helper, built once from the streams and three environment variables, decides colour, progress,\nprompts and width for the whole program. That single decision point is what keeps behaviour\nconsistent between commands, makes both branches testable without patching, and stops your tool\nwriting escape sequences into somebody's log file.",[14,1242,1244],{"id":1243},"frequently-asked-questions","Frequently asked questions",[1246,1247,1249,1250,1252],"h3",{"id":1248},"why-is-isatty-false-in-my-tests","Why is ",[25,1251,72],{}," False in my tests?",[10,1254,1255,1256,77],{},"Because the runner replaces the streams with in-memory buffers, which are not terminals. That is\ncorrect — it makes the non-interactive path the default in tests — and it is why the interactive\nbranch needs an explicit override or a directly constructed ",[25,1257,574],{},[1246,1259,1261,1262,1264],{"id":1260},"should-i-check-isatty-on-stdout-or-stderr-for-progress","Should I check ",[25,1263,72],{}," on stdout or stderr for progress?",[10,1266,1267],{},"stderr, because that is where progress is written. A command whose stdout is redirected to a file\nbut whose stderr is a terminal should still show a live progress bar — that combination is\nextremely common, and getting it wrong means users lose progress output whenever they redirect\nresults.",[1246,1269,1271,1272,1274],{"id":1270},"does-no_color-need-a-specific-value","Does ",[25,1273,41],{}," need a specific value?",[10,1276,1277,1278,1282,1283,1286],{},"No. The convention is that the variable being ",[1279,1280,1281],"em",{},"present"," — even empty — disables colour. Checking for\ntruthiness rather than presence is a common bug, because ",[25,1284,1285],{},"NO_COLOR="," (empty) is a legitimate way to\nset it.",[1246,1288,1290,1291,1294],{"id":1289},"what-about-force_color","What about ",[25,1292,1293],{},"FORCE_COLOR","?",[10,1296,1297,1298,1301,1302,77],{},"Some ecosystems honour it to force colour on when output is piped, typically so a CI system can\nrender coloured logs. It is reasonable to support: treat it exactly like ",[25,1299,1300],{},"--color",", taking\nprecedence over detection but not over an explicit ",[25,1303,910],{},[1246,1305,1307],{"id":1306},"how-do-i-test-the-interactive-branch","How do I test the interactive branch?",[10,1309,1310,1311,1313,1314,1317],{},"Construct the ",[25,1312,574],{}," directly, or monkeypatch the one helper. Trying to allocate a real\npseudo-terminal in a test is possible with ",[25,1315,1316],{},"pty"," and rarely worth it — the thing worth testing is\nyour decision logic, not the operating system's.",[1246,1319,1321],{"id":1320},"does-this-interact-with-the-pager","Does this interact with the pager?",[10,1323,1324,1325,1328,1329,1332,1333,1336,1337,1340],{},"Yes, and the same rule applies: page only when stdout is a terminal. Click's ",[25,1326,1327],{},"echo_via_pager","\nalready checks, but a hand-rolled call to ",[25,1330,1331],{},"less"," will happily launch inside a CI job and block the\nbuild. If you page, honour ",[25,1334,1335],{},"PAGER"," and provide a ",[25,1338,1339],{},"--no-pager"," flag, because someone will want the\nraw output at some point.",[1246,1342,1344],{"id":1343},"what-about-tools-that-call-my-tool","What about tools that call my tool?",[10,1346,1347,1348,1350,1351,1353],{},"Treat them as any other non-interactive caller: they get plain output, no prompts and no live\nprogress, because their stdout is a pipe. If a wrapper genuinely needs coloured output — a build\nsystem that renders logs — ",[25,1349,1293],{}," or an explicit ",[25,1352,1300],{}," is the documented way to ask.",[1246,1355,1357],{"id":1356},"should-the-tool-remember-a-colour-preference-between-runs","Should the tool remember a colour preference between runs?",[10,1359,1360,1361,1363],{},"No. Configuration files are for settings that describe intent; presentation should follow the\nenvironment it is running in right now, because the same install is used interactively and from\nscripts. ",[25,1362,41],{}," in a shell profile is already the durable way for a user to express the\npreference, and it works across every tool rather than only yours.",[14,1365,1367],{"id":1366},"related","Related",[19,1369,1370,1376,1383,1389,1396],{},[22,1371,1372,1373],{},"Up: ",[86,1374,1375],{"href":88},"Working with stdin, stdout and pipes",[22,1377,1378,1379],{},"Sideways: ",[86,1380,1382],{"href":1381},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe\u002F","Handling broken pipe and SIGPIPE",[22,1384,1378,1385],{},[86,1386,1388],{"href":1387},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis\u002F","Reading piped input in Python CLIs",[22,1390,1391,1392],{},"Related: ",[86,1393,1395],{"href":1394},"\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",[22,1397,1391,1398],{},[86,1399,1401],{"href":1400},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags\u002F","Adding verbose and quiet logging flags",[1403,1404,1405],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":110,"searchDepth":128,"depth":128,"links":1407},[1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1431],{"id":16,"depth":128,"text":17},{"id":80,"depth":128,"text":81},{"id":92,"depth":128,"text":93},{"id":164,"depth":128,"text":165},{"id":578,"depth":128,"text":579},{"id":704,"depth":128,"text":705},{"id":867,"depth":128,"text":868},{"id":918,"depth":128,"text":919},{"id":1236,"depth":128,"text":1237},{"id":1243,"depth":128,"text":1244,"children":1418},[1419,1421,1423,1425,1427,1428,1429,1430],{"id":1248,"depth":137,"text":1420},"Why is isatty False in my tests?",{"id":1260,"depth":137,"text":1422},"Should I check isatty on stdout or stderr for progress?",{"id":1270,"depth":137,"text":1424},"Does NO_COLOR need a specific value?",{"id":1289,"depth":137,"text":1426},"What about FORCE_COLOR?",{"id":1306,"depth":137,"text":1307},{"id":1320,"depth":137,"text":1321},{"id":1343,"depth":137,"text":1344},{"id":1356,"depth":137,"text":1357},{"id":1366,"depth":128,"text":1367},"2026-08-01","Adapt a Python CLI to its environment - isatty checks, NO_COLOR and CI variables, one interactive() helper, and testing both branches properly.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output",{"title":5,"description":1433},"advanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002Findex",[1442,1443,1444,1445,1446],"tty","colour","output","ci","ux","Y1y-3l3mp5ETkP95dxXINcnw16Pw4NzuyqtaLTcbrpA",[1449,1452,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1488,1491,1494,1497,1500,1503,1506,1509,1512,1515,1518,1521,1524,1527,1528,1531,1534,1537,1540,1542,1545,1548,1551,1554,1557,1560,1563,1566,1569,1572,1575,1578,1581,1584,1587,1590,1593,1596,1599,1602,1605,1608,1611,1614,1617,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,1674,1677,1680,1683,1686,1689,1692],{"path":1450,"title":1451},"\u002Fabout","About Python CLI Toolcraft",{"path":1453,"title":1454},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1456,"title":1457},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1459,"title":1460},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1462,"title":1463},"\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":1465,"title":1466},"\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":1468,"title":1469},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1471,"title":1472},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1474,"title":1475},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1477,"title":1478},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1480,"title":1481},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1483,"title":1484},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1486,"title":1487},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1489,"title":1490},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1492,"title":1493},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1495,"title":1496},"\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":1498,"title":1499},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1501,"title":1502},"\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":1504,"title":1505},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1507,"title":1508},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1510,"title":1511},"\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":1513,"title":1514},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1516,"title":1517},"\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":1519,"title":1520},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1522,"title":1523},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1525,"title":1526},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1438,"title":5},{"path":1529,"title":1530},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1532,"title":1533},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1535,"title":1536},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1538,"title":1539},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":839,"title":1541},"Python CLI Toolcraft",{"path":1543,"title":1544},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1546,"title":1547},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1549,"title":1550},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1552,"title":1553},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1555,"title":1556},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1558,"title":1559},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1561,"title":1562},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1564,"title":1565},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1567,"title":1568},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1570,"title":1571},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1573,"title":1574},"\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":1576,"title":1577},"\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":1579,"title":1580},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1582,"title":1583},"\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":1585,"title":1586},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1588,"title":1589},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1591,"title":1592},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1594,"title":1595},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1597,"title":1598},"\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":1600,"title":1601},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1603,"title":1604},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1606,"title":1607},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1609,"title":1610},"\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":1612,"title":1613},"\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":1615,"title":1616},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1618,"title":1619},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1621,"title":1622},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1624,"title":1625},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1627,"title":1628},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1630,"title":1631},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1633,"title":1634},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1636,"title":1637},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1639,"title":1640},"\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":1642,"title":1643},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1645,"title":1646},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1648,"title":1649},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1651,"title":1652},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1654,"title":1655},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1657,"title":1658},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1660,"title":1661},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1663,"title":1664},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1666,"title":1667},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1669,"title":1670},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1672,"title":1673},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1675,"title":1676},"\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":1678,"title":1679},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1681,"title":1682},"\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":1684,"title":1685},"\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":1687,"title":1688},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1690,"title":1691},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1693,"title":1694},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690030]