[{"data":1,"prerenderedAt":2882},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002F":3,"content-directory":2334},{"id":4,"title":5,"body":6,"date":2319,"description":2320,"difficulty":2321,"draft":2322,"extension":2323,"meta":2324,"navigation":259,"path":2325,"seo":2326,"stem":2327,"tags":2328,"updated":2319,"__hash__":2333},"content\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Findex.md","Building Terminal UIs with Textual for Python CLIs",{"type":7,"value":8,"toc":2297},"minimark",[9,34,48,52,57,103,107,110,118,146,150,153,215,218,958,980,984,987,1013,1052,1066,1265,1272,1276,1279,1282,1695,1718,1722,1733,1955,1975,1979,1982,2004,2015,2021,2025,2047,2067,2071,2082,2086,2089,2168,2172,2195,2199,2204,2219,2223,2226,2230,2236,2240,2243,2247,2250,2254,2257,2261,2293],[10,11,12,13,17,18,17,21,24,25,29,30,33],"p",{},"Most of a CLI's life is spent as plain commands: arguments in, output out, scriptable and composable. But some tasks do not fit that shape well. Browsing three hundred deployments to find the one that failed, watching a queue drain, triaging a list of alerts, or picking a pull request to check out — these are exploratory, and with plain commands they become a loop of ",[14,15,16],"code",{},"list",", ",[14,19,20],{},"grep",[14,22,23],{},"show",", repeat. A full-screen ",[26,27,28],"strong",{},"terminal user interface"," (TUI), where the user moves through data with the keyboard and details update as they go, can turn that loop into one screen. ",[26,31,32],{},"Textual",", from the makers of Rich, brings a modern component model to the terminal: widgets composed into a tree, CSS for layout, events and messages, async by default, and a first-class testing API.",[10,35,36,37,42,43,47],{},"This topic covers adding a TUI to a Python CLI without letting it take over: when a TUI is worth it, how a Textual app is structured, how it plugs into a Typer or Click command, how to test it, and the costs to plan for. It sits in the ",[38,39,41],"a",{"href":40},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing & User Experience"," section beside ",[38,44,46],{"href":45},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","interactive terminal UI with Rich",", which covers the lighter-weight option of rich output and prompts within ordinary commands.",[49,50],"inline-diagram",{"name":51},"tui-topic-map",[53,54,56],"h2",{"id":55},"tldr","TL;DR",[58,59,60,67,77,87,97],"ul",{},[61,62,63,66],"li",{},[26,64,65],{},"Add a TUI for exploring or watching data",", never as the only way to do something. Every action must also exist as a scriptable command.",[61,68,69,72,73,76],{},[26,70,71],{},"Make the TUI one command"," (",[14,74,75],{},"mytool browse",") that loads data through the same core functions as the other commands and returns a result.",[61,78,79,82,83,86],{},[26,80,81],{},"Structure the app like a web front end",": compose widgets, style with CSS, react to messages in ",[14,84,85],{},"on_*"," handlers, bind keys to actions.",[61,88,89,92,93,96],{},[26,90,91],{},"Keep logic out of widgets"," so most of it is tested with ordinary unit tests; use Textual's ",[14,94,95],{},"run_test"," and Pilot for interaction tests.",[61,98,99,102],{},[26,100,101],{},"Import Textual lazily"," inside the command that needs it, so the rest of the CLI starts fast.",[53,104,106],{"id":105},"when-a-tui-is-the-right-tool","When a TUI is the right tool",[49,108],{"name":109},"tui-when",[10,111,112,113,117],{},"A TUI trades scriptability for interactivity. That trade is worth it when the user does not know in advance what they are looking for, when the data is too large for one screen of output but not so large that a query language is needed, or when the data changes while they watch. It is not worth it for tasks people repeat, automate, or run in CI — those need commands with flags and machine-readable output. ",[38,114,116],{"href":115},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui\u002F","Choosing between a CLI, a prompt flow and a TUI"," turns this into a concrete decision guide.",[10,119,120,121,17,124,17,127,17,130,133,134,138,139,141,142,145],{},"Tools that get the balance right — ",[14,122,123],{},"htop",[14,125,126],{},"k9s",[14,128,129],{},"lazygit",[14,131,132],{},"gh dash"," — share a pattern: the TUI is a ",[135,136,137],"em",{},"view"," over capabilities that also exist non-interactively. ",[14,140,126],{}," does nothing ",[14,143,144],{},"kubectl"," cannot; it makes browsing faster.",[53,147,149],{"id":148},"how-a-textual-app-is-structured","How a Textual app is structured",[49,151],{"name":152},"tui-architecture",[10,154,155,156,159,160,163,164,167,168,17,171,17,174,17,177,17,180,17,183,186,187,190,191,194,195,198,199,202,203,206,207,210,211,214],{},"An ",[14,157,158],{},"App"," subclass is the program. Its ",[14,161,162],{},"compose"," method yields ",[26,165,166],{},"widgets"," — Textual ships a large library: ",[14,169,170],{},"DataTable",[14,172,173],{},"Tree",[14,175,176],{},"Input",[14,178,179],{},"Log",[14,181,182],{},"Markdown",[14,184,185],{},"TabbedContent"," and more — which form a DOM-like tree. ",[26,188,189],{},"CSS"," (Textual's variant, often in a ",[14,192,193],{},".tcss"," file) lays them out and styles them. User actions and state changes produce ",[26,196,197],{},"messages"," that bubble up the tree; the app or a widget reacts by defining a handler named after the message. ",[26,200,201],{},"Bindings"," map keys to ",[26,204,205],{},"actions",", methods named ",[14,208,209],{},"action_*",". For multi-view apps, ",[26,212,213],{},"screens"," are pushed and popped like pages.",[10,216,217],{},"Here is a compact but complete browser for deployments:",[219,220,225],"pre",{"className":221,"code":222,"language":223,"meta":224,"style":224},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Ftui\u002Fbrowser.py\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass\n\nfrom textual.app import App, ComposeResult\nfrom textual.containers import Horizontal\nfrom textual.widgets import DataTable, Footer, Header, Static\n\n\n@dataclass(frozen=True)\nclass Deploy:\n    id: int\n    project: str\n    env: str\n    status: str\n    author: str\n\n\nclass DeployBrowser(App[Deploy | None]):\n    \"\"\"Browse deploys; Enter returns the selected one, q quits with None.\"\"\"\n\n    CSS = \"\"\"\n    Horizontal { height: 1fr; }\n    DataTable { width: 2fr; }\n    #detail { width: 1fr; padding: 1 2; border-left: solid $accent; }\n    \"\"\"\n    BINDINGS = [(\"q\", \"quit\", \"Quit\"), (\"r\", \"refresh\", \"Refresh\")]\n\n    def __init__(self, deploys: list[Deploy]) -> None:\n        super().__init__()\n        self.deploys = {str(d.id): d for d in deploys}\n\n    def compose(self) -> ComposeResult:\n        yield Header()\n        with Horizontal():\n            yield DataTable(cursor_type=\"row\")\n            yield Static(\"Select a deploy\", id=\"detail\")\n        yield Footer()\n\n    def on_mount(self) -> None:\n        table = self.query_one(DataTable)\n        table.add_columns(\"ID\", \"Project\", \"Env\", \"Status\")\n        for key, d in self.deploys.items():\n            table.add_row(str(d.id), d.project, d.env, d.status, key=key)\n        table.focus()\n\n    def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:\n        d = self.deploys[event.row_key.value]\n        self.query_one(\"#detail\", Static).update(\n            f\"#{d.id} {d.project}\\nenv: {d.env}\\nstatus: {d.status}\\nby: {d.author}\")\n\n    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:\n        self.exit(self.deploys[event.row_key.value])\n\n    def action_refresh(self) -> None:\n        self.notify(\"refreshed\")\n","python","",[14,226,227,236,254,261,275,280,293,306,319,324,329,352,364,376,385,393,401,409,414,419,439,446,451,463,469,475,481,487,530,535,552,567,599,604,615,624,633,652,675,683,688,703,717,743,759,778,784,789,804,817,831,892,897,912,926,931,945],{"__ignoreMap":224},[228,229,232],"span",{"class":230,"line":231},"line",1,[228,233,235],{"class":234},"sJ8bj","# src\u002Fmytool\u002Ftui\u002Fbrowser.py\n",[228,237,239,243,247,250],{"class":230,"line":238},2,[228,240,242],{"class":241},"szBVR","from",[228,244,246],{"class":245},"sj4cs"," __future__",[228,248,249],{"class":241}," import",[228,251,253],{"class":252},"sVt8B"," annotations\n",[228,255,257],{"class":230,"line":256},3,[228,258,260],{"emptyLinePlaceholder":259},true,"\n",[228,262,264,266,269,272],{"class":230,"line":263},4,[228,265,242],{"class":241},[228,267,268],{"class":252}," dataclasses ",[228,270,271],{"class":241},"import",[228,273,274],{"class":252}," dataclass\n",[228,276,278],{"class":230,"line":277},5,[228,279,260],{"emptyLinePlaceholder":259},[228,281,283,285,288,290],{"class":230,"line":282},6,[228,284,242],{"class":241},[228,286,287],{"class":252}," textual.app ",[228,289,271],{"class":241},[228,291,292],{"class":252}," App, ComposeResult\n",[228,294,296,298,301,303],{"class":230,"line":295},7,[228,297,242],{"class":241},[228,299,300],{"class":252}," textual.containers ",[228,302,271],{"class":241},[228,304,305],{"class":252}," Horizontal\n",[228,307,309,311,314,316],{"class":230,"line":308},8,[228,310,242],{"class":241},[228,312,313],{"class":252}," textual.widgets ",[228,315,271],{"class":241},[228,317,318],{"class":252}," DataTable, Footer, Header, Static\n",[228,320,322],{"class":230,"line":321},9,[228,323,260],{"emptyLinePlaceholder":259},[228,325,327],{"class":230,"line":326},10,[228,328,260],{"emptyLinePlaceholder":259},[228,330,332,336,339,343,346,349],{"class":230,"line":331},11,[228,333,335],{"class":334},"sScJk","@dataclass",[228,337,338],{"class":252},"(",[228,340,342],{"class":341},"s4XuR","frozen",[228,344,345],{"class":241},"=",[228,347,348],{"class":245},"True",[228,350,351],{"class":252},")\n",[228,353,355,358,361],{"class":230,"line":354},12,[228,356,357],{"class":241},"class",[228,359,360],{"class":334}," Deploy",[228,362,363],{"class":252},":\n",[228,365,367,370,373],{"class":230,"line":366},13,[228,368,369],{"class":245},"    id",[228,371,372],{"class":252},": ",[228,374,375],{"class":245},"int\n",[228,377,379,382],{"class":230,"line":378},14,[228,380,381],{"class":252},"    project: ",[228,383,384],{"class":245},"str\n",[228,386,388,391],{"class":230,"line":387},15,[228,389,390],{"class":252},"    env: ",[228,392,384],{"class":245},[228,394,396,399],{"class":230,"line":395},16,[228,397,398],{"class":252},"    status: ",[228,400,384],{"class":245},[228,402,404,407],{"class":230,"line":403},17,[228,405,406],{"class":252},"    author: ",[228,408,384],{"class":245},[228,410,412],{"class":230,"line":411},18,[228,413,260],{"emptyLinePlaceholder":259},[228,415,417],{"class":230,"line":416},19,[228,418,260],{"emptyLinePlaceholder":259},[228,420,422,424,427,430,433,436],{"class":230,"line":421},20,[228,423,357],{"class":241},[228,425,426],{"class":334}," DeployBrowser",[228,428,429],{"class":252},"(App[Deploy ",[228,431,432],{"class":241},"|",[228,434,435],{"class":245}," None",[228,437,438],{"class":252},"]):\n",[228,440,442],{"class":230,"line":441},21,[228,443,445],{"class":444},"sZZnC","    \"\"\"Browse deploys; Enter returns the selected one, q quits with None.\"\"\"\n",[228,447,449],{"class":230,"line":448},22,[228,450,260],{"emptyLinePlaceholder":259},[228,452,454,457,460],{"class":230,"line":453},23,[228,455,456],{"class":245},"    CSS",[228,458,459],{"class":241}," =",[228,461,462],{"class":444}," \"\"\"\n",[228,464,466],{"class":230,"line":465},24,[228,467,468],{"class":444},"    Horizontal { height: 1fr; }\n",[228,470,472],{"class":230,"line":471},25,[228,473,474],{"class":444},"    DataTable { width: 2fr; }\n",[228,476,478],{"class":230,"line":477},26,[228,479,480],{"class":444},"    #detail { width: 1fr; padding: 1 2; border-left: solid $accent; }\n",[228,482,484],{"class":230,"line":483},27,[228,485,486],{"class":444},"    \"\"\"\n",[228,488,490,493,495,498,501,503,506,508,511,514,517,519,522,524,527],{"class":230,"line":489},28,[228,491,492],{"class":245},"    BINDINGS",[228,494,459],{"class":241},[228,496,497],{"class":252}," [(",[228,499,500],{"class":444},"\"q\"",[228,502,17],{"class":252},[228,504,505],{"class":444},"\"quit\"",[228,507,17],{"class":252},[228,509,510],{"class":444},"\"Quit\"",[228,512,513],{"class":252},"), (",[228,515,516],{"class":444},"\"r\"",[228,518,17],{"class":252},[228,520,521],{"class":444},"\"refresh\"",[228,523,17],{"class":252},[228,525,526],{"class":444},"\"Refresh\"",[228,528,529],{"class":252},")]\n",[228,531,533],{"class":230,"line":532},29,[228,534,260],{"emptyLinePlaceholder":259},[228,536,538,541,544,547,550],{"class":230,"line":537},30,[228,539,540],{"class":241},"    def",[228,542,543],{"class":245}," __init__",[228,545,546],{"class":252},"(self, deploys: list[Deploy]) -> ",[228,548,549],{"class":245},"None",[228,551,363],{"class":252},[228,553,555,558,561,564],{"class":230,"line":554},31,[228,556,557],{"class":245},"        super",[228,559,560],{"class":252},"().",[228,562,563],{"class":245},"__init__",[228,565,566],{"class":252},"()\n",[228,568,570,573,576,578,581,584,587,590,593,596],{"class":230,"line":569},32,[228,571,572],{"class":245},"        self",[228,574,575],{"class":252},".deploys ",[228,577,345],{"class":241},[228,579,580],{"class":252}," {",[228,582,583],{"class":245},"str",[228,585,586],{"class":252},"(d.id): d ",[228,588,589],{"class":241},"for",[228,591,592],{"class":252}," d ",[228,594,595],{"class":241},"in",[228,597,598],{"class":252}," deploys}\n",[228,600,602],{"class":230,"line":601},33,[228,603,260],{"emptyLinePlaceholder":259},[228,605,607,609,612],{"class":230,"line":606},34,[228,608,540],{"class":241},[228,610,611],{"class":334}," compose",[228,613,614],{"class":252},"(self) -> ComposeResult:\n",[228,616,618,621],{"class":230,"line":617},35,[228,619,620],{"class":241},"        yield",[228,622,623],{"class":252}," Header()\n",[228,625,627,630],{"class":230,"line":626},36,[228,628,629],{"class":241},"        with",[228,631,632],{"class":252}," Horizontal():\n",[228,634,636,639,642,645,647,650],{"class":230,"line":635},37,[228,637,638],{"class":241},"            yield",[228,640,641],{"class":252}," DataTable(",[228,643,644],{"class":341},"cursor_type",[228,646,345],{"class":241},[228,648,649],{"class":444},"\"row\"",[228,651,351],{"class":252},[228,653,655,657,660,663,665,668,670,673],{"class":230,"line":654},38,[228,656,638],{"class":241},[228,658,659],{"class":252}," Static(",[228,661,662],{"class":444},"\"Select a deploy\"",[228,664,17],{"class":252},[228,666,667],{"class":341},"id",[228,669,345],{"class":241},[228,671,672],{"class":444},"\"detail\"",[228,674,351],{"class":252},[228,676,678,680],{"class":230,"line":677},39,[228,679,620],{"class":241},[228,681,682],{"class":252}," Footer()\n",[228,684,686],{"class":230,"line":685},40,[228,687,260],{"emptyLinePlaceholder":259},[228,689,691,693,696,699,701],{"class":230,"line":690},41,[228,692,540],{"class":241},[228,694,695],{"class":334}," on_mount",[228,697,698],{"class":252},"(self) -> ",[228,700,549],{"class":245},[228,702,363],{"class":252},[228,704,706,709,711,714],{"class":230,"line":705},42,[228,707,708],{"class":252},"        table ",[228,710,345],{"class":241},[228,712,713],{"class":245}," self",[228,715,716],{"class":252},".query_one(DataTable)\n",[228,718,720,723,726,728,731,733,736,738,741],{"class":230,"line":719},43,[228,721,722],{"class":252},"        table.add_columns(",[228,724,725],{"class":444},"\"ID\"",[228,727,17],{"class":252},[228,729,730],{"class":444},"\"Project\"",[228,732,17],{"class":252},[228,734,735],{"class":444},"\"Env\"",[228,737,17],{"class":252},[228,739,740],{"class":444},"\"Status\"",[228,742,351],{"class":252},[228,744,746,749,752,754,756],{"class":230,"line":745},44,[228,747,748],{"class":241},"        for",[228,750,751],{"class":252}," key, d ",[228,753,595],{"class":241},[228,755,713],{"class":245},[228,757,758],{"class":252},".deploys.items():\n",[228,760,762,765,767,770,773,775],{"class":230,"line":761},45,[228,763,764],{"class":252},"            table.add_row(",[228,766,583],{"class":245},[228,768,769],{"class":252},"(d.id), d.project, d.env, d.status, ",[228,771,772],{"class":341},"key",[228,774,345],{"class":241},[228,776,777],{"class":252},"key)\n",[228,779,781],{"class":230,"line":780},46,[228,782,783],{"class":252},"        table.focus()\n",[228,785,787],{"class":230,"line":786},47,[228,788,260],{"emptyLinePlaceholder":259},[228,790,792,794,797,800,802],{"class":230,"line":791},48,[228,793,540],{"class":241},[228,795,796],{"class":334}," on_data_table_row_highlighted",[228,798,799],{"class":252},"(self, event: DataTable.RowHighlighted) -> ",[228,801,549],{"class":245},[228,803,363],{"class":252},[228,805,807,810,812,814],{"class":230,"line":806},49,[228,808,809],{"class":252},"        d ",[228,811,345],{"class":241},[228,813,713],{"class":245},[228,815,816],{"class":252},".deploys[event.row_key.value]\n",[228,818,820,822,825,828],{"class":230,"line":819},50,[228,821,572],{"class":245},[228,823,824],{"class":252},".query_one(",[228,826,827],{"class":444},"\"#detail\"",[228,829,830],{"class":252},", Static).update(\n",[228,832,834,837,840,843,846,849,851,854,857,860,862,865,867,870,872,875,877,880,882,885,887,890],{"class":230,"line":833},51,[228,835,836],{"class":241},"            f",[228,838,839],{"class":444},"\"#",[228,841,842],{"class":245},"{",[228,844,845],{"class":252},"d.id",[228,847,848],{"class":245},"}",[228,850,580],{"class":245},[228,852,853],{"class":252},"d.project",[228,855,856],{"class":245},"}\\n",[228,858,859],{"class":444},"env: ",[228,861,842],{"class":245},[228,863,864],{"class":252},"d.env",[228,866,856],{"class":245},[228,868,869],{"class":444},"status: ",[228,871,842],{"class":245},[228,873,874],{"class":252},"d.status",[228,876,856],{"class":245},[228,878,879],{"class":444},"by: ",[228,881,842],{"class":245},[228,883,884],{"class":252},"d.author",[228,886,848],{"class":245},[228,888,889],{"class":444},"\"",[228,891,351],{"class":252},[228,893,895],{"class":230,"line":894},52,[228,896,260],{"emptyLinePlaceholder":259},[228,898,900,902,905,908,910],{"class":230,"line":899},53,[228,901,540],{"class":241},[228,903,904],{"class":334}," on_data_table_row_selected",[228,906,907],{"class":252},"(self, event: DataTable.RowSelected) -> ",[228,909,549],{"class":245},[228,911,363],{"class":252},[228,913,915,917,920,923],{"class":230,"line":914},54,[228,916,572],{"class":245},[228,918,919],{"class":252},".exit(",[228,921,922],{"class":245},"self",[228,924,925],{"class":252},".deploys[event.row_key.value])\n",[228,927,929],{"class":230,"line":928},55,[228,930,260],{"emptyLinePlaceholder":259},[228,932,934,936,939,941,943],{"class":230,"line":933},56,[228,935,540],{"class":241},[228,937,938],{"class":334}," action_refresh",[228,940,698],{"class":252},[228,942,549],{"class":245},[228,944,363],{"class":252},[228,946,948,950,953,956],{"class":230,"line":947},57,[228,949,572],{"class":245},[228,951,952],{"class":252},".notify(",[228,954,955],{"class":444},"\"refreshed\"",[228,957,351],{"class":252},[10,959,960,963,964,372,967,970,971,974,975,979],{},[14,961,962],{},"App[Deploy | None]"," declares the app's ",[26,965,966],{},"return type",[14,968,969],{},"self.exit(value)"," ends the app and ",[14,972,973],{},"app.run()"," returns that value to the caller. That is what lets a TUI act as an interactive picker for a larger command. ",[38,976,978],{"href":977},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app\u002F","Building your first Textual app"," walks through each part in more detail, including screens, workers for loading data without freezing the UI, and styling.",[53,981,983],{"id":982},"screens-styling-and-state","Screens, styling and state",[10,985,986],{},"Three more Textual ideas cover most of what a CLI's TUI needs beyond a single view.",[10,988,989,992,993,996,997,1000,1001,1004,1005,1008,1009,1012],{},[26,990,991],{},"Screens for navigation."," A detail view, a confirmation dialog or a help overlay is a ",[14,994,995],{},"Screen"," pushed on top of the current one with ",[14,998,999],{},"self.push_screen(DetailScreen(deploy))"," and dismissed with ",[14,1002,1003],{},"self.dismiss(result)",". ",[14,1006,1007],{},"ModalScreen"," dims what is underneath and captures focus, which suits confirmations. Pushing a screen with a callback — ",[14,1010,1011],{},"self.push_screen(ConfirmRollback(d), self.on_confirmed)"," — returns the dialog's answer to the app without any global state. Keep screens small and single-purpose; a TUI with five screens is already a sizeable application.",[10,1014,1015,1018,1019,17,1022,17,1025,1028,1029,1032,1033,1036,1037,1039,1040,1043,1044,1047,1048,1051],{},[26,1016,1017],{},"CSS for everything visual."," Textual CSS supports layouts (",[14,1020,1021],{},"horizontal",[14,1023,1024],{},"vertical",[14,1026,1027],{},"grid","), sizing in cells, fractions and percentages, docking (headers and footers stay put), borders, padding and colours drawn from theme variables such as ",[14,1030,1031],{},"$accent"," and ",[14,1034,1035],{},"$panel",". Keeping visual decisions in CSS — ideally a separate ",[14,1038,193],{}," file referenced with ",[14,1041,1042],{},"CSS_PATH"," — keeps Python code about behaviour, and Textual's built-in themes (switchable at runtime) mean dark and light terminals both look right without per-theme code. Textual's developer console (",[14,1045,1046],{},"textual run --dev"," with ",[14,1049,1050],{},"textual console"," in another terminal) shows messages and logs live and supports live-reloading CSS while you adjust layouts.",[10,1053,1054,1057,1058,1061,1062,1065],{},[26,1055,1056],{},"Reactive attributes for state."," Declaring ",[14,1059,1060],{},"filter_text = reactive(\"\")"," on a widget or app makes assignments trigger ",[14,1063,1064],{},"watch_filter_text"," automatically, which is the idiomatic way to re-render a table when a search box changes. Reactives keep the update logic in one place instead of scattered across handlers, and they compose with workers: a watcher can start a worker that fetches filtered data, and the worker's completion updates the table.",[219,1067,1069],{"className":221,"code":1068,"language":223,"meta":224,"style":224},"from textual.reactive import reactive\nfrom textual.widgets import DataTable, Input\n\n\nclass FilterableBrowser(DeployBrowser):\n    filter_text = reactive(\"\")\n\n    def on_input_changed(self, event: Input.Changed) -> None:\n        self.filter_text = event.value\n\n    def watch_filter_text(self, text: str) -> None:\n        table = self.query_one(DataTable)\n        table.clear()\n        for key, d in self.deploys.items():\n            if text.lower() in f\"{d.project} {d.env} {d.status}\".lower():\n                table.add_row(str(d.id), d.project, d.env, d.status, key=key)\n",[14,1070,1071,1083,1094,1098,1102,1117,1132,1136,1150,1162,1166,1185,1195,1200,1212,1250],{"__ignoreMap":224},[228,1072,1073,1075,1078,1080],{"class":230,"line":231},[228,1074,242],{"class":241},[228,1076,1077],{"class":252}," textual.reactive ",[228,1079,271],{"class":241},[228,1081,1082],{"class":252}," reactive\n",[228,1084,1085,1087,1089,1091],{"class":230,"line":238},[228,1086,242],{"class":241},[228,1088,313],{"class":252},[228,1090,271],{"class":241},[228,1092,1093],{"class":252}," DataTable, Input\n",[228,1095,1096],{"class":230,"line":256},[228,1097,260],{"emptyLinePlaceholder":259},[228,1099,1100],{"class":230,"line":263},[228,1101,260],{"emptyLinePlaceholder":259},[228,1103,1104,1106,1109,1111,1114],{"class":230,"line":277},[228,1105,357],{"class":241},[228,1107,1108],{"class":334}," FilterableBrowser",[228,1110,338],{"class":252},[228,1112,1113],{"class":334},"DeployBrowser",[228,1115,1116],{"class":252},"):\n",[228,1118,1119,1122,1124,1127,1130],{"class":230,"line":282},[228,1120,1121],{"class":252},"    filter_text ",[228,1123,345],{"class":241},[228,1125,1126],{"class":252}," reactive(",[228,1128,1129],{"class":444},"\"\"",[228,1131,351],{"class":252},[228,1133,1134],{"class":230,"line":295},[228,1135,260],{"emptyLinePlaceholder":259},[228,1137,1138,1140,1143,1146,1148],{"class":230,"line":308},[228,1139,540],{"class":241},[228,1141,1142],{"class":334}," on_input_changed",[228,1144,1145],{"class":252},"(self, event: Input.Changed) -> ",[228,1147,549],{"class":245},[228,1149,363],{"class":252},[228,1151,1152,1154,1157,1159],{"class":230,"line":321},[228,1153,572],{"class":245},[228,1155,1156],{"class":252},".filter_text ",[228,1158,345],{"class":241},[228,1160,1161],{"class":252}," event.value\n",[228,1163,1164],{"class":230,"line":326},[228,1165,260],{"emptyLinePlaceholder":259},[228,1167,1168,1170,1173,1176,1178,1181,1183],{"class":230,"line":331},[228,1169,540],{"class":241},[228,1171,1172],{"class":334}," watch_filter_text",[228,1174,1175],{"class":252},"(self, text: ",[228,1177,583],{"class":245},[228,1179,1180],{"class":252},") -> ",[228,1182,549],{"class":245},[228,1184,363],{"class":252},[228,1186,1187,1189,1191,1193],{"class":230,"line":354},[228,1188,708],{"class":252},[228,1190,345],{"class":241},[228,1192,713],{"class":245},[228,1194,716],{"class":252},[228,1196,1197],{"class":230,"line":366},[228,1198,1199],{"class":252},"        table.clear()\n",[228,1201,1202,1204,1206,1208,1210],{"class":230,"line":378},[228,1203,748],{"class":241},[228,1205,751],{"class":252},[228,1207,595],{"class":241},[228,1209,713],{"class":245},[228,1211,758],{"class":252},[228,1213,1214,1217,1220,1222,1225,1227,1229,1231,1233,1235,1237,1239,1241,1243,1245,1247],{"class":230,"line":387},[228,1215,1216],{"class":241},"            if",[228,1218,1219],{"class":252}," text.lower() ",[228,1221,595],{"class":241},[228,1223,1224],{"class":241}," f",[228,1226,889],{"class":444},[228,1228,842],{"class":245},[228,1230,853],{"class":252},[228,1232,848],{"class":245},[228,1234,580],{"class":245},[228,1236,864],{"class":252},[228,1238,848],{"class":245},[228,1240,580],{"class":245},[228,1242,874],{"class":252},[228,1244,848],{"class":245},[228,1246,889],{"class":444},[228,1248,1249],{"class":252},".lower():\n",[228,1251,1252,1255,1257,1259,1261,1263],{"class":230,"line":395},[228,1253,1254],{"class":252},"                table.add_row(",[228,1256,583],{"class":245},[228,1258,769],{"class":252},[228,1260,772],{"class":341},[228,1262,345],{"class":241},[228,1264,777],{"class":252},[10,1266,1267,1268,1271],{},"With the filter logic in a watcher, the Pilot test for it is simply \"type into the input, then count table rows\" — and the same filtering rule, extracted into a plain function, can be unit-tested and reused by the non-interactive ",[14,1269,1270],{},"list --filter"," command so the two never disagree.",[53,1273,1275],{"id":1274},"plugging-a-tui-into-a-cli","Plugging a TUI into a CLI",[49,1277],{"name":1278},"tui-cli-entry",[10,1280,1281],{},"The TUI should be one command among many. It parses its arguments like any other command, loads data through the same core functions, runs the app, and acts on the result:",[219,1283,1285],{"className":221,"code":1284,"language":223,"meta":224,"style":224},"# src\u002Fmytool\u002Fcli.py\nimport sys\nfrom typing import Annotated\n\nimport typer\n\napp = typer.Typer()\n\n\ndef load_deploys(project: str | None) -> list:\n    from mytool.tui.browser import Deploy          # core would normally live elsewhere\n    data = [Deploy(4411, \"web\", \"prod\", \"passed\", \"ana\"), Deploy(4410, \"billing\", \"prod\", \"failed\", \"ben\")]\n    return [d for d in data if project in (None, d.project)]\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment tool.\"\"\"\n\n\n@app.command()\ndef browse(project: Annotated[str | None, typer.Option(help=\"Only this project.\")] = None) -> None:\n    \"\"\"Browse deploys interactively; prints the chosen deploy's ID.\"\"\"\n    if not sys.stdout.isatty():\n        typer.echo(\"error: browse needs an interactive terminal; use 'mytool deploys list'\", err=True)\n        raise typer.Exit(2)\n    from mytool.tui.browser import DeployBrowser   # lazy: Textual is imported only here\n    chosen = DeployBrowser(load_deploys(project)).run()\n    if chosen is None:\n        raise typer.Exit(1)\n    typer.echo(chosen.id)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1286,1287,1292,1299,1311,1315,1322,1326,1336,1340,1344,1368,1384,1444,1476,1480,1484,1491,1505,1510,1514,1518,1525,1565,1570,1581,1600,1613,1627,1637,1651,1662,1667,1671,1675,1690],{"__ignoreMap":224},[228,1288,1289],{"class":230,"line":231},[228,1290,1291],{"class":234},"# src\u002Fmytool\u002Fcli.py\n",[228,1293,1294,1296],{"class":230,"line":238},[228,1295,271],{"class":241},[228,1297,1298],{"class":252}," sys\n",[228,1300,1301,1303,1306,1308],{"class":230,"line":256},[228,1302,242],{"class":241},[228,1304,1305],{"class":252}," typing ",[228,1307,271],{"class":241},[228,1309,1310],{"class":252}," Annotated\n",[228,1312,1313],{"class":230,"line":263},[228,1314,260],{"emptyLinePlaceholder":259},[228,1316,1317,1319],{"class":230,"line":277},[228,1318,271],{"class":241},[228,1320,1321],{"class":252}," typer\n",[228,1323,1324],{"class":230,"line":282},[228,1325,260],{"emptyLinePlaceholder":259},[228,1327,1328,1331,1333],{"class":230,"line":295},[228,1329,1330],{"class":252},"app ",[228,1332,345],{"class":241},[228,1334,1335],{"class":252}," typer.Typer()\n",[228,1337,1338],{"class":230,"line":308},[228,1339,260],{"emptyLinePlaceholder":259},[228,1341,1342],{"class":230,"line":321},[228,1343,260],{"emptyLinePlaceholder":259},[228,1345,1346,1349,1352,1355,1357,1360,1362,1364,1366],{"class":230,"line":326},[228,1347,1348],{"class":241},"def",[228,1350,1351],{"class":334}," load_deploys",[228,1353,1354],{"class":252},"(project: ",[228,1356,583],{"class":245},[228,1358,1359],{"class":241}," |",[228,1361,435],{"class":245},[228,1363,1180],{"class":252},[228,1365,16],{"class":245},[228,1367,363],{"class":252},[228,1369,1370,1373,1376,1378,1381],{"class":230,"line":331},[228,1371,1372],{"class":241},"    from",[228,1374,1375],{"class":252}," mytool.tui.browser ",[228,1377,271],{"class":241},[228,1379,1380],{"class":252}," Deploy          ",[228,1382,1383],{"class":234},"# core would normally live elsewhere\n",[228,1385,1386,1389,1391,1394,1397,1399,1402,1404,1407,1409,1412,1414,1417,1420,1423,1425,1428,1430,1432,1434,1437,1439,1442],{"class":230,"line":354},[228,1387,1388],{"class":252},"    data ",[228,1390,345],{"class":241},[228,1392,1393],{"class":252}," [Deploy(",[228,1395,1396],{"class":245},"4411",[228,1398,17],{"class":252},[228,1400,1401],{"class":444},"\"web\"",[228,1403,17],{"class":252},[228,1405,1406],{"class":444},"\"prod\"",[228,1408,17],{"class":252},[228,1410,1411],{"class":444},"\"passed\"",[228,1413,17],{"class":252},[228,1415,1416],{"class":444},"\"ana\"",[228,1418,1419],{"class":252},"), Deploy(",[228,1421,1422],{"class":245},"4410",[228,1424,17],{"class":252},[228,1426,1427],{"class":444},"\"billing\"",[228,1429,17],{"class":252},[228,1431,1406],{"class":444},[228,1433,17],{"class":252},[228,1435,1436],{"class":444},"\"failed\"",[228,1438,17],{"class":252},[228,1440,1441],{"class":444},"\"ben\"",[228,1443,529],{"class":252},[228,1445,1446,1449,1452,1454,1456,1458,1461,1464,1467,1469,1471,1473],{"class":230,"line":366},[228,1447,1448],{"class":241},"    return",[228,1450,1451],{"class":252}," [d ",[228,1453,589],{"class":241},[228,1455,592],{"class":252},[228,1457,595],{"class":241},[228,1459,1460],{"class":252}," data ",[228,1462,1463],{"class":241},"if",[228,1465,1466],{"class":252}," project ",[228,1468,595],{"class":241},[228,1470,72],{"class":252},[228,1472,549],{"class":245},[228,1474,1475],{"class":252},", d.project)]\n",[228,1477,1478],{"class":230,"line":378},[228,1479,260],{"emptyLinePlaceholder":259},[228,1481,1482],{"class":230,"line":387},[228,1483,260],{"emptyLinePlaceholder":259},[228,1485,1486,1489],{"class":230,"line":395},[228,1487,1488],{"class":334},"@app.callback",[228,1490,566],{"class":252},[228,1492,1493,1495,1498,1501,1503],{"class":230,"line":403},[228,1494,1348],{"class":241},[228,1496,1497],{"class":334}," main",[228,1499,1500],{"class":252},"() -> ",[228,1502,549],{"class":245},[228,1504,363],{"class":252},[228,1506,1507],{"class":230,"line":411},[228,1508,1509],{"class":444},"    \"\"\"Deployment tool.\"\"\"\n",[228,1511,1512],{"class":230,"line":416},[228,1513,260],{"emptyLinePlaceholder":259},[228,1515,1516],{"class":230,"line":421},[228,1517,260],{"emptyLinePlaceholder":259},[228,1519,1520,1523],{"class":230,"line":441},[228,1521,1522],{"class":334},"@app.command",[228,1524,566],{"class":252},[228,1526,1527,1529,1532,1535,1537,1539,1541,1544,1547,1549,1552,1555,1557,1559,1561,1563],{"class":230,"line":448},[228,1528,1348],{"class":241},[228,1530,1531],{"class":334}," browse",[228,1533,1534],{"class":252},"(project: Annotated[",[228,1536,583],{"class":245},[228,1538,1359],{"class":241},[228,1540,435],{"class":245},[228,1542,1543],{"class":252},", typer.Option(",[228,1545,1546],{"class":341},"help",[228,1548,345],{"class":241},[228,1550,1551],{"class":444},"\"Only this project.\"",[228,1553,1554],{"class":252},")] ",[228,1556,345],{"class":241},[228,1558,435],{"class":245},[228,1560,1180],{"class":252},[228,1562,549],{"class":245},[228,1564,363],{"class":252},[228,1566,1567],{"class":230,"line":453},[228,1568,1569],{"class":444},"    \"\"\"Browse deploys interactively; prints the chosen deploy's ID.\"\"\"\n",[228,1571,1572,1575,1578],{"class":230,"line":465},[228,1573,1574],{"class":241},"    if",[228,1576,1577],{"class":241}," not",[228,1579,1580],{"class":252}," sys.stdout.isatty():\n",[228,1582,1583,1586,1589,1591,1594,1596,1598],{"class":230,"line":471},[228,1584,1585],{"class":252},"        typer.echo(",[228,1587,1588],{"class":444},"\"error: browse needs an interactive terminal; use 'mytool deploys list'\"",[228,1590,17],{"class":252},[228,1592,1593],{"class":341},"err",[228,1595,345],{"class":241},[228,1597,348],{"class":245},[228,1599,351],{"class":252},[228,1601,1602,1605,1608,1611],{"class":230,"line":477},[228,1603,1604],{"class":241},"        raise",[228,1606,1607],{"class":252}," typer.Exit(",[228,1609,1610],{"class":245},"2",[228,1612,351],{"class":252},[228,1614,1615,1617,1619,1621,1624],{"class":230,"line":483},[228,1616,1372],{"class":241},[228,1618,1375],{"class":252},[228,1620,271],{"class":241},[228,1622,1623],{"class":252}," DeployBrowser   ",[228,1625,1626],{"class":234},"# lazy: Textual is imported only here\n",[228,1628,1629,1632,1634],{"class":230,"line":489},[228,1630,1631],{"class":252},"    chosen ",[228,1633,345],{"class":241},[228,1635,1636],{"class":252}," DeployBrowser(load_deploys(project)).run()\n",[228,1638,1639,1641,1644,1647,1649],{"class":230,"line":532},[228,1640,1574],{"class":241},[228,1642,1643],{"class":252}," chosen ",[228,1645,1646],{"class":241},"is",[228,1648,435],{"class":245},[228,1650,363],{"class":252},[228,1652,1653,1655,1657,1660],{"class":230,"line":537},[228,1654,1604],{"class":241},[228,1656,1607],{"class":252},[228,1658,1659],{"class":245},"1",[228,1661,351],{"class":252},[228,1663,1664],{"class":230,"line":554},[228,1665,1666],{"class":252},"    typer.echo(chosen.id)\n",[228,1668,1669],{"class":230,"line":569},[228,1670,260],{"emptyLinePlaceholder":259},[228,1672,1673],{"class":230,"line":601},[228,1674,260],{"emptyLinePlaceholder":259},[228,1676,1677,1679,1682,1685,1688],{"class":230,"line":606},[228,1678,1463],{"class":241},[228,1680,1681],{"class":245}," __name__",[228,1683,1684],{"class":241}," ==",[228,1686,1687],{"class":444}," \"__main__\"",[228,1689,363],{"class":252},[228,1691,1692],{"class":230,"line":617},[228,1693,1694],{"class":252},"    app()\n",[10,1696,1697,1698,1701,1702,1004,1706,1709,1710,1713,1714,1717],{},"Three details make it a good citizen. ",[26,1699,1700],{},"The TTY check"," refuses to start a full-screen app in a pipe or CI job and points at the scriptable equivalent — see ",[38,1703,1705],{"href":1704},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","detecting CI environments and non-interactive shells",[26,1707,1708],{},"The lazy import"," keeps Textual's import cost out of every other command. And ",[26,1711,1712],{},"printing the result"," makes the TUI composable: ",[14,1715,1716],{},"mytool deploy rollback $(mytool browse --project web)"," uses the TUI as an interactive picker inside a shell command.",[53,1719,1721],{"id":1720},"testing-a-tui","Testing a TUI",[10,1723,1724,1725,1728,1729,1732],{},"Textual apps are testable without a terminal. ",[14,1726,1727],{},"app.run_test()"," starts the app headless at a chosen size and yields a ",[26,1730,1731],{},"Pilot"," that presses keys, clicks and waits for events to settle; assertions then query widgets directly:",[219,1734,1736],{"className":221,"code":1735,"language":223,"meta":224,"style":224},"# tests\u002Ftest_browser.py\nfrom mytool.tui.browser import Deploy, DeployBrowser\n\nDEPLOYS = [Deploy(1, \"web\", \"prod\", \"passed\", \"ana\"), Deploy(2, \"api\", \"dev\", \"failed\", \"ben\")]\n\n\nasync def test_highlight_updates_detail_and_enter_returns_choice():\n    app = DeployBrowser(DEPLOYS)\n    async with app.run_test(size=(100, 30)) as pilot:\n        await pilot.press(\"down\")\n        await pilot.pause()\n        assert \"status: failed\" in str(app.query_one(\"#detail\").render())\n        await pilot.press(\"enter\")\n    assert app.return_value == DEPLOYS[1]\n",[14,1737,1738,1743,1754,1758,1809,1813,1817,1831,1845,1880,1893,1900,1922,1933],{"__ignoreMap":224},[228,1739,1740],{"class":230,"line":231},[228,1741,1742],{"class":234},"# tests\u002Ftest_browser.py\n",[228,1744,1745,1747,1749,1751],{"class":230,"line":238},[228,1746,242],{"class":241},[228,1748,1375],{"class":252},[228,1750,271],{"class":241},[228,1752,1753],{"class":252}," Deploy, DeployBrowser\n",[228,1755,1756],{"class":230,"line":256},[228,1757,260],{"emptyLinePlaceholder":259},[228,1759,1760,1763,1765,1767,1769,1771,1773,1775,1777,1779,1781,1783,1785,1787,1789,1791,1794,1796,1799,1801,1803,1805,1807],{"class":230,"line":263},[228,1761,1762],{"class":245},"DEPLOYS",[228,1764,459],{"class":241},[228,1766,1393],{"class":252},[228,1768,1659],{"class":245},[228,1770,17],{"class":252},[228,1772,1401],{"class":444},[228,1774,17],{"class":252},[228,1776,1406],{"class":444},[228,1778,17],{"class":252},[228,1780,1411],{"class":444},[228,1782,17],{"class":252},[228,1784,1416],{"class":444},[228,1786,1419],{"class":252},[228,1788,1610],{"class":245},[228,1790,17],{"class":252},[228,1792,1793],{"class":444},"\"api\"",[228,1795,17],{"class":252},[228,1797,1798],{"class":444},"\"dev\"",[228,1800,17],{"class":252},[228,1802,1436],{"class":444},[228,1804,17],{"class":252},[228,1806,1441],{"class":444},[228,1808,529],{"class":252},[228,1810,1811],{"class":230,"line":277},[228,1812,260],{"emptyLinePlaceholder":259},[228,1814,1815],{"class":230,"line":282},[228,1816,260],{"emptyLinePlaceholder":259},[228,1818,1819,1822,1825,1828],{"class":230,"line":295},[228,1820,1821],{"class":241},"async",[228,1823,1824],{"class":241}," def",[228,1826,1827],{"class":334}," test_highlight_updates_detail_and_enter_returns_choice",[228,1829,1830],{"class":252},"():\n",[228,1832,1833,1836,1838,1841,1843],{"class":230,"line":308},[228,1834,1835],{"class":252},"    app ",[228,1837,345],{"class":241},[228,1839,1840],{"class":252}," DeployBrowser(",[228,1842,1762],{"class":245},[228,1844,351],{"class":252},[228,1846,1847,1850,1853,1856,1859,1861,1863,1866,1868,1871,1874,1877],{"class":230,"line":321},[228,1848,1849],{"class":241},"    async",[228,1851,1852],{"class":241}," with",[228,1854,1855],{"class":252}," app.run_test(",[228,1857,1858],{"class":341},"size",[228,1860,345],{"class":241},[228,1862,338],{"class":252},[228,1864,1865],{"class":245},"100",[228,1867,17],{"class":252},[228,1869,1870],{"class":245},"30",[228,1872,1873],{"class":252},")) ",[228,1875,1876],{"class":241},"as",[228,1878,1879],{"class":252}," pilot:\n",[228,1881,1882,1885,1888,1891],{"class":230,"line":326},[228,1883,1884],{"class":241},"        await",[228,1886,1887],{"class":252}," pilot.press(",[228,1889,1890],{"class":444},"\"down\"",[228,1892,351],{"class":252},[228,1894,1895,1897],{"class":230,"line":331},[228,1896,1884],{"class":241},[228,1898,1899],{"class":252}," pilot.pause()\n",[228,1901,1902,1905,1908,1911,1914,1917,1919],{"class":230,"line":354},[228,1903,1904],{"class":241},"        assert",[228,1906,1907],{"class":444}," \"status: failed\"",[228,1909,1910],{"class":241}," in",[228,1912,1913],{"class":245}," str",[228,1915,1916],{"class":252},"(app.query_one(",[228,1918,827],{"class":444},[228,1920,1921],{"class":252},").render())\n",[228,1923,1924,1926,1928,1931],{"class":230,"line":366},[228,1925,1884],{"class":241},[228,1927,1887],{"class":252},[228,1929,1930],{"class":444},"\"enter\"",[228,1932,351],{"class":252},[228,1934,1935,1938,1941,1944,1947,1950,1952],{"class":230,"line":378},[228,1936,1937],{"class":241},"    assert",[228,1939,1940],{"class":252}," app.return_value ",[228,1942,1943],{"class":241},"==",[228,1945,1946],{"class":245}," DEPLOYS",[228,1948,1949],{"class":252},"[",[228,1951,1659],{"class":245},[228,1953,1954],{"class":252},"]\n",[10,1956,1957,1958,1961,1962,1965,1966,1970,1971,1974],{},"Async tests need a runner such as ",[14,1959,1960],{},"pytest-asyncio"," (with ",[14,1963,1964],{},"asyncio_mode = \"auto\"",") or anyio's plugin. ",[38,1967,1969],{"href":1968},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot\u002F","Testing Textual apps with Pilot"," covers the full approach, including visual snapshot tests with ",[14,1972,1973],{},"pytest-textual-snapshot",".",[53,1976,1978],{"id":1977},"performance-and-responsiveness","Performance and responsiveness",[10,1980,1981],{},"A TUI is judged by how it feels, and two things make Textual apps feel slow: blocking the event loop, and loading everything up front.",[10,1983,1984,1987,1988,1991,1992,1995,1996,1999,2000,2003],{},[26,1985,1986],{},"Never block the event loop."," Textual runs on asyncio; a synchronous HTTP call inside a handler freezes the whole interface until it returns. Load data in a ",[26,1989,1990],{},"worker"," — ",[14,1993,1994],{},"self.run_worker(self.load(), exclusive=True)"," for async code, or ",[14,1997,1998],{},"@work(thread=True)"," for blocking libraries — and update widgets when the worker finishes. Show a loading indicator in the meantime; Textual widgets have a ",[14,2001,2002],{},"loading"," property for exactly this.",[10,2005,2006,2009,2010,2014],{},[26,2007,2008],{},"Load progressively."," For large datasets, fetch the first page, render it, and fetch more as the user scrolls or searches, rather than making them wait for everything. The generator-based pagination from ",[38,2011,2013],{"href":2012},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli\u002F","paginating API results in a CLI"," fits naturally into a worker.",[10,2016,2017,2020],{},[26,2018,2019],{},"Mind startup."," Importing Textual and building the widget tree takes a noticeable fraction of a second. That is fine for a command the user runs deliberately to browse, which is another reason to keep it lazily imported and out of every other command's path.",[53,2022,2024],{"id":2023},"shipping-a-cli-that-includes-a-tui","Shipping a CLI that includes a TUI",[10,2026,2027,2028,2031,2032,1991,2035,2038,2039,2041,2042,2046],{},"A TUI changes packaging decisions slightly. Textual is a runtime dependency only of the ",[14,2029,2030],{},"browse"," command, so consider making it an ",[26,2033,2034],{},"extra",[14,2036,2037],{},"pipx install \"mytool[tui]\""," — if many users run the tool only in scripts or CI, where the TUI can never be used anyway. The ",[14,2040,2030],{}," command then checks for the import and prints an installation hint when it is missing, the lazy-optional-dependency pattern from ",[38,2043,2045],{"href":2044},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","reducing CLI dependency weight",". For internal tools used mostly interactively, a normal dependency is simpler.",[10,2048,2049,2050,2052,2053,2057,2058,2061,2062,2066],{},"Keep the stylesheet inside the package and load it with ",[14,2051,1042],{}," relative to the app module, so it is included in the wheel like any other package data; see ",[38,2054,2056],{"href":2055},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources\u002F","bundling data files with importlib.resources",". Standalone binaries built with PyInstaller or Nuitka need Textual's own CSS and data files collected as well, which the ",[14,2059,2060],{},"doctor","-style smoke test in ",[38,2063,2065],{"href":2064},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci\u002F","smoke-testing the built wheel in CI"," will catch if they are missing — importing the TUI module there is enough to surface the problem.",[53,2068,2070],{"id":2069},"accessibility-and-terminals","Accessibility and terminals",[10,2072,2073,2074,2077,2078,1974],{},"Full-screen interfaces are harder for some users than line-oriented output: screen readers handle scrolling text better than redrawn regions, and some terminals and multiplexers render box-drawing characters or colours poorly. Textual handles a lot — it adapts to terminal capabilities, supports mouse and keyboard, and has high-contrast themes — but the mitigation that matters most is structural: because every action also exists as a plain command with text output, nobody is ever forced to use the TUI. Respect ",[14,2075,2076],{},"NO_COLOR",", keep key bindings conventional, and show them in the footer, as covered in ",[38,2079,2081],{"href":2080},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color\u002F","respecting NO_COLOR and FORCE_COLOR",[53,2083,2085],{"id":2084},"common-pitfalls","Common pitfalls",[10,2087,2088],{},"A handful of mistakes account for most TUI problems in CLI projects:",[58,2090,2091,2116,2128,2134,2145,2159],{},[61,2092,2093,2096,2097,1032,2100,2103,2104,2107,2108,2111,2112,2115],{},[26,2094,2095],{},"Printing from inside the app."," ",[14,2098,2099],{},"print()",[14,2101,2102],{},"typer.echo()"," write to a terminal that Textual is managing, corrupting the display. Use ",[14,2105,2106],{},"self.log()"," (visible in the dev console), ",[14,2109,2110],{},"self.notify()"," for user-facing messages, or return data from the app and print after ",[14,2113,2114],{},"run()"," returns.",[61,2117,2118,2127],{},[26,2119,2120,2121,2123,2124,1974],{},"Doing I\u002FO in ",[14,2122,162],{}," or ",[14,2125,2126],{},"on_mount"," Both run on the event loop before the first frame; a slow API call there means a blank screen. Mount the widgets immediately and fill them from a worker.",[61,2129,2130,2133],{},[26,2131,2132],{},"Logic inside widgets."," Filtering, sorting and formatting written in handler methods can only be tested through the Pilot. Extract them into plain functions that the handlers call, and the TUI shrinks to wiring.",[61,2135,2136,2139,2140,1032,2142,2144],{},[26,2137,2138],{},"Diverging from the commands."," A TUI with its own data-loading and business rules slowly disagrees with the ",[14,2141,16],{},[14,2143,23],{}," commands. Route both through the same core module.",[61,2146,2147,2150,2151,2154,2155,2158],{},[26,2148,2149],{},"Forgetting the exit path."," Users should always be able to leave with ",[14,2152,2153],{},"q"," or Ctrl+C, and the terminal must be restored afterwards. Textual restores it on normal exit and on exceptions; custom signal handlers or ",[14,2156,2157],{},"os._exit"," calls can bypass that and leave the terminal in a broken state.",[61,2160,2161,2096,2164,2167],{},[26,2162,2163],{},"Unbounded refresh timers.",[14,2165,2166],{},"set_interval"," for live updates is convenient; make the interval sensible (seconds, not milliseconds) and stop timers when the relevant screen is not visible, or the app burns CPU in the background.",[53,2169,2171],{"id":2170},"key-takeaways","Key takeaways",[58,2173,2174,2177,2180,2183,2186,2192],{},[61,2175,2176],{},"Use a TUI for browsing, watching and triage; keep every action available as a scriptable command.",[61,2178,2179],{},"Make the TUI a single command that reuses core functions and returns a value, so it can act as a picker.",[61,2181,2182],{},"Structure apps with composed widgets, CSS, message handlers and key-bound actions.",[61,2184,2185],{},"Keep blocking work in workers, load progressively, and import Textual lazily.",[61,2187,2188,2189,2191],{},"Test logic with unit tests and interactions with ",[14,2190,95],{}," and Pilot.",[61,2193,2194],{},"Refuse to start without a terminal, and point users at the non-interactive equivalent.",[53,2196,2198],{"id":2197},"frequently-asked-questions","Frequently asked questions",[2200,2201,2203],"h3",{"id":2202},"textual-or-cursesurwidprompt_toolkit","Textual or curses\u002Furwid\u002Fprompt_toolkit?",[10,2205,2206,2207,2210,2211,2214,2215,2218],{},"Textual offers the most modern developer experience in Python — components, CSS, async, testing — and renders well across terminals, including on Windows. ",[14,2208,2209],{},"curses"," is lower level and Unix-only in the standard library; ",[14,2212,2213],{},"urwid"," is mature but older in style; ",[14,2216,2217],{},"prompt_toolkit"," excels at rich line editing and prompts rather than full applications. For a new TUI in a Python CLI, Textual is the default choice.",[2200,2220,2222],{"id":2221},"does-textual-work-over-ssh-and-in-tmux","Does Textual work over SSH and in tmux?",[10,2224,2225],{},"Yes. It works in any terminal emulator with reasonable capabilities, including over SSH and inside tmux or screen. Colours and some glyphs degrade gracefully on limited terminals.",[2200,2227,2229],{"id":2228},"can-a-textual-app-run-in-a-browser","Can a Textual app run in a browser?",[10,2231,2232,2235],{},[14,2233,2234],{},"textual-serve"," and Textual's web support can serve an app to a browser, which is occasionally useful for sharing an internal tool. Treat it as a bonus rather than a reason to choose Textual.",[2200,2237,2239],{"id":2238},"how-big-a-dependency-is-textual","How big a dependency is Textual?",[10,2241,2242],{},"It depends on Rich and a few small packages; installing it adds a few megabytes. Import time is the bigger cost, which lazy importing inside the TUI command avoids.",[2200,2244,2246],{"id":2245},"how-long-does-it-take-to-build-a-useful-tui","How long does it take to build a useful TUI?",[10,2248,2249],{},"A single-screen browser like the one above — a table, a detail panel, a few key bindings — is an afternoon's work once the data-loading functions exist. The time goes into what follows: loading states, error handling for failed fetches, filtering, keyboard polish and tests. Start with one screen that solves one real browsing problem, ship it, and grow it only when users ask.",[2200,2251,2253],{"id":2252},"should-the-tui-write-to-stdout-when-it-exits","Should the TUI write to stdout when it exits?",[10,2255,2256],{},"Only its result, and only if the command is designed as a picker. Everything the app draws goes to the terminal on an alternate screen, so once it exits the shell's scrollback is clean and stdout carries just the chosen value.",[53,2258,2260],{"id":2259},"related","Related",[58,2262,2263,2268,2273,2277,2281,2287],{},[61,2264,2265,2266],{},"Up: ",[38,2267,41],{"href":40},[61,2269,2270,2271],{},"Down: ",[38,2272,978],{"href":977},[61,2274,2270,2275],{},[38,2276,1969],{"href":1968},[61,2278,2270,2279],{},[38,2280,116],{"href":115},[61,2282,2283,2284],{},"Sideways: ",[38,2285,2286],{"href":45},"Interactive terminal UI with Rich",[61,2288,2283,2289],{},[38,2290,2292],{"href":2291},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002F","Cross-platform terminal compatibility",[2294,2295,2296],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":224,"searchDepth":238,"depth":238,"links":2298},[2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2318],{"id":55,"depth":238,"text":56},{"id":105,"depth":238,"text":106},{"id":148,"depth":238,"text":149},{"id":982,"depth":238,"text":983},{"id":1274,"depth":238,"text":1275},{"id":1720,"depth":238,"text":1721},{"id":1977,"depth":238,"text":1978},{"id":2023,"depth":238,"text":2024},{"id":2069,"depth":238,"text":2070},{"id":2084,"depth":238,"text":2085},{"id":2170,"depth":238,"text":2171},{"id":2197,"depth":238,"text":2198,"children":2311},[2312,2313,2314,2315,2316,2317],{"id":2202,"depth":256,"text":2203},{"id":2221,"depth":256,"text":2222},{"id":2228,"depth":256,"text":2229},{"id":2238,"depth":256,"text":2239},{"id":2245,"depth":256,"text":2246},{"id":2252,"depth":256,"text":2253},{"id":2259,"depth":238,"text":2260},"2026-09-18","Add a full-screen terminal UI to a Python CLI with Textual: when a TUI helps, app structure, widgets and CSS, integrating with Typer, testing with Pilot and pitfalls.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual",{"title":5,"description":2320},"advanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Findex",[2329,2330,2331,2332],"textual","tui","rich","terminal-ux","YM35o8uXQCcb0HeVF8jJol2YnWtHgIAEaprPjNsldcU",[2335,2338,2341,2344,2347,2350,2353,2356,2359,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2795,2798,2801,2804,2807,2810,2813,2816,2819,2822,2825,2828,2831,2834,2837,2840,2843,2846,2849,2852,2855,2858,2861,2864,2867,2870,2873,2876,2879],{"path":2336,"title":2337},"\u002Fabout","About Python CLI Toolcraft",{"path":2339,"title":2340},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2342,"title":2343},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2345,"title":2346},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2348,"title":2349},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2351,"title":2352},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2354,"title":2355},"\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":2357,"title":2358},"\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":2325,"title":5},{"path":2361,"title":2362},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2364,"title":2365},"\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":2367,"title":2368},"\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":2370,"title":2371},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2373,"title":2374},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2376,"title":2377},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2379,"title":2380},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2382,"title":2383},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2385,"title":2386},"\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":2388,"title":2389},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2391,"title":2392},"\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":2394,"title":2395},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2397,"title":2398},"\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":2400,"title":2401},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2403,"title":2404},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2406,"title":2407},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2409,"title":2410},"\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":2412,"title":2413},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2415,"title":2416},"\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":2418,"title":2419},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2421,"title":2422},"\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":2424,"title":2425},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2427,"title":2428},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2430,"title":2431},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2433,"title":2434},"\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":2436,"title":2437},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2439,"title":2440},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2442,"title":2443},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2448,"title":2449},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2451,"title":2452},"\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":2454,"title":2455},"\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":2457,"title":2458},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2460,"title":2461},"\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":2463,"title":2464},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2466,"title":2467},"\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":2469,"title":2470},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2472,"title":2473},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2475,"title":2476},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2478,"title":2479},"\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":2481,"title":2482},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2484,"title":2485},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2487,"title":2488},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2490,"title":2491},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2493,"title":2494},"\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":2496,"title":2497},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2499,"title":2500},"\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":2502,"title":2503},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2505,"title":2506},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2508,"title":2509},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2511,"title":2512},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2514,"title":2515},"\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":2517,"title":2518},"\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":2520,"title":2521},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2523,"title":2524},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2526,"title":2527},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2529,"title":2530},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2532,"title":2533},"\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":2535,"title":2536},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2538,"title":2539},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2541,"title":2542},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2544,"title":2545},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2547,"title":2548},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2550,"title":2551},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2553,"title":2554},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2556,"title":2557},"\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":2559,"title":2560},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2562,"title":2563},"\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":2565,"title":2566},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2568,"title":2569},"\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":2571,"title":2572},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2574,"title":2575},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2577,"title":2578},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2580,"title":2581},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2583,"title":2584},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2586,"title":2587},"\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":2589,"title":2590},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2592,"title":2593},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2595,"title":2596},"\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":2598,"title":2599},"\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":2601,"title":2602},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2604,"title":2605},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2607,"title":2608},"\u002F","Python CLI Toolcraft",{"path":2610,"title":2611},"\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":2613,"title":2614},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2616,"title":2617},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2619,"title":2620},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2622,"title":2623},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2625,"title":2626},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2628,"title":2629},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2631,"title":2632},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2634,"title":2635},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2637,"title":2638},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2640,"title":2641},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2643,"title":2644},"\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":2646,"title":2647},"\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":2649,"title":2650},"\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":2652,"title":2653},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2655,"title":2656},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2658,"title":2659},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2661,"title":2662},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2664,"title":2665},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2667,"title":2668},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2670,"title":2671},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2673,"title":2674},"\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":2676,"title":2677},"\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":2679,"title":2680},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2682,"title":2683},"\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":2685,"title":2686},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2688,"title":2689},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2691,"title":2692},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2694,"title":2695},"\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":2697,"title":2698},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2700,"title":2701},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2703,"title":2704},"\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":2706,"title":2707},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2709,"title":2710},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2712,"title":2713},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2715,"title":2716},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2718,"title":2719},"\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":2721,"title":2722},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2724,"title":2725},"\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":2727,"title":2728},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2730,"title":2731},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2733,"title":2734},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2736,"title":2737},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2739,"title":2740},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2742,"title":2743},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2745,"title":2746},"\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":2748,"title":2749},"\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":2751,"title":2752},"\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":2754,"title":2755},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2757,"title":2758},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2760,"title":2761},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2763,"title":2764},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2766,"title":2767},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2769,"title":2770},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2772,"title":2773},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2775,"title":2776},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2778,"title":2779},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2781,"title":2782},"\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":2784,"title":2785},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2787,"title":2788},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2790,"title":2791},"\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":2793,"title":2794},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2796,"title":2797},"\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":2799,"title":2800},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2802,"title":2803},"\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":2805,"title":2806},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2808,"title":2809},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2811,"title":2812},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2814,"title":2815},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2817,"title":2818},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2820,"title":2821},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2823,"title":2824},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2826,"title":2827},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2829,"title":2830},"\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":2832,"title":2833},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2835,"title":2836},"\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":2838,"title":2839},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2841,"title":2842},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2844,"title":2845},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2847,"title":2848},"\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":2850,"title":2851},"\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":2853,"title":2854},"\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":2856,"title":2857},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2859,"title":2860},"\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":2862,"title":2863},"\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":2865,"title":2866},"\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":2868,"title":2869},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2871,"title":2872},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2874,"title":2875},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2877,"title":2878},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2880,"title":2881},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905042]