[{"data":1,"prerenderedAt":1775},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich\u002F":3,"content-directory":1527},{"id":4,"title":5,"body":6,"date":1511,"description":1512,"difficulty":1513,"draft":1514,"extension":1515,"meta":1516,"navigation":111,"path":1517,"seo":1518,"stem":1519,"tags":1520,"updated":1511,"__hash__":1526},"content\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich\u002Findex.md","Rendering Tables and JSON with Rich",{"type":7,"value":8,"toc":1491},"minimark",[9,13,18,57,61,70,74,78,385,388,397,406,415,425,429,440,443,450,484,488,491,494,497,680,691,695,698,701,860,872,875,895,899,905,911,921,924,928,931,940,954,963,983,1083,1086,1090,1093,1368,1375,1379,1382,1386,1391,1398,1402,1412,1416,1427,1431,1438,1442,1449,1453,1488],[10,11,12],"p",{},"A table is the most common structured output a CLI produces, and the difference between a readable\none and a grid of text is entirely in the column configuration. The second half of the job is\nmaking sure the same data can leave as JSON, so nobody has to parse the table.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,40,43,46,54],"ul",{},[22,23,24,25,29,30,29,33,29,36,39],"li",{},"Configure columns: ",[26,27,28],"code",{},"justify",", ",[26,31,32],{},"style",[26,34,35],{},"no_wrap",[26,37,38],{},"overflow"," — that is where readability lives.",[22,41,42],{},"Right-align numbers, left-align text, never wrap identifiers people copy.",[22,44,45],{},"Pair every colour with a word; colour alone disappears in a log file.",[22,47,48,49,53],{},"Render the table and the JSON from the ",[50,51,52],"strong",{},"same data structure"," so they cannot disagree.",[22,55,56],{},"Decide what to drop when the terminal is narrow, before the terminal decides for you.",[14,58,60],{"id":59},"prerequisites","Prerequisites",[10,62,63,64,69],{},"Rich installed, and the two-console arrangement from\n",[65,66,68],"a",{"href":67},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002F","working with stdin, stdout and pipes",":\nresults on stdout, narration on stderr.",[14,71,73],{"id":72},"a-table-worth-reading","A table worth reading",[75,76],"inline-diagram",{"name":77},"table-column-anatomy",[79,80,85],"pre",{"className":81,"code":82,"language":83,"meta":84,"style":84},"language-python shiki shiki-themes github-light github-dark","from rich.table import Table\n\nfrom mytool.console import out\n\ndef render_environments(rows: list[Environment]) -> None:\n    table = Table(title=\"Environments\", title_justify=\"left\", caption=f\"{len(rows)} total\")\n\n    table.add_column(\"Name\", style=\"bold\", no_wrap=True)\n    table.add_column(\"Region\")\n    table.add_column(\"Replicas\", justify=\"right\")\n    table.add_column(\"Updated\", justify=\"right\", style=\"dim\")\n    table.add_column(\"Status\", justify=\"right\")\n\n    for env in rows:\n        status = \"[green]healthy[\u002F]\" if env.healthy else \"[red]degraded[\u002F]\"\n        table.add_row(env.name, env.region, str(env.replicas), env.updated_ago, status)\n\n    out.print(table)\n","python","",[26,86,87,106,113,126,131,151,211,216,245,255,274,301,319,324,339,362,374,379],{"__ignoreMap":84},[88,89,92,96,100,103],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"szBVR","from",[88,97,99],{"class":98},"sVt8B"," rich.table ",[88,101,102],{"class":94},"import",[88,104,105],{"class":98}," Table\n",[88,107,109],{"class":90,"line":108},2,[88,110,112],{"emptyLinePlaceholder":111},true,"\n",[88,114,116,118,121,123],{"class":90,"line":115},3,[88,117,95],{"class":94},[88,119,120],{"class":98}," mytool.console ",[88,122,102],{"class":94},[88,124,125],{"class":98}," out\n",[88,127,129],{"class":90,"line":128},4,[88,130,112],{"emptyLinePlaceholder":111},[88,132,134,137,141,144,148],{"class":90,"line":133},5,[88,135,136],{"class":94},"def",[88,138,140],{"class":139},"sScJk"," render_environments",[88,142,143],{"class":98},"(rows: list[Environment]) -> ",[88,145,147],{"class":146},"sj4cs","None",[88,149,150],{"class":98},":\n",[88,152,154,157,160,163,167,169,173,175,178,180,183,185,188,190,193,196,199,202,205,208],{"class":90,"line":153},6,[88,155,156],{"class":98},"    table ",[88,158,159],{"class":94},"=",[88,161,162],{"class":98}," Table(",[88,164,166],{"class":165},"s4XuR","title",[88,168,159],{"class":94},[88,170,172],{"class":171},"sZZnC","\"Environments\"",[88,174,29],{"class":98},[88,176,177],{"class":165},"title_justify",[88,179,159],{"class":94},[88,181,182],{"class":171},"\"left\"",[88,184,29],{"class":98},[88,186,187],{"class":165},"caption",[88,189,159],{"class":94},[88,191,192],{"class":94},"f",[88,194,195],{"class":171},"\"",[88,197,198],{"class":146},"{len",[88,200,201],{"class":98},"(rows)",[88,203,204],{"class":146},"}",[88,206,207],{"class":171}," total\"",[88,209,210],{"class":98},")\n",[88,212,214],{"class":90,"line":213},7,[88,215,112],{"emptyLinePlaceholder":111},[88,217,219,222,225,227,229,231,234,236,238,240,243],{"class":90,"line":218},8,[88,220,221],{"class":98},"    table.add_column(",[88,223,224],{"class":171},"\"Name\"",[88,226,29],{"class":98},[88,228,32],{"class":165},[88,230,159],{"class":94},[88,232,233],{"class":171},"\"bold\"",[88,235,29],{"class":98},[88,237,35],{"class":165},[88,239,159],{"class":94},[88,241,242],{"class":146},"True",[88,244,210],{"class":98},[88,246,248,250,253],{"class":90,"line":247},9,[88,249,221],{"class":98},[88,251,252],{"class":171},"\"Region\"",[88,254,210],{"class":98},[88,256,258,260,263,265,267,269,272],{"class":90,"line":257},10,[88,259,221],{"class":98},[88,261,262],{"class":171},"\"Replicas\"",[88,264,29],{"class":98},[88,266,28],{"class":165},[88,268,159],{"class":94},[88,270,271],{"class":171},"\"right\"",[88,273,210],{"class":98},[88,275,277,279,282,284,286,288,290,292,294,296,299],{"class":90,"line":276},11,[88,278,221],{"class":98},[88,280,281],{"class":171},"\"Updated\"",[88,283,29],{"class":98},[88,285,28],{"class":165},[88,287,159],{"class":94},[88,289,271],{"class":171},[88,291,29],{"class":98},[88,293,32],{"class":165},[88,295,159],{"class":94},[88,297,298],{"class":171},"\"dim\"",[88,300,210],{"class":98},[88,302,304,306,309,311,313,315,317],{"class":90,"line":303},12,[88,305,221],{"class":98},[88,307,308],{"class":171},"\"Status\"",[88,310,29],{"class":98},[88,312,28],{"class":165},[88,314,159],{"class":94},[88,316,271],{"class":171},[88,318,210],{"class":98},[88,320,322],{"class":90,"line":321},13,[88,323,112],{"emptyLinePlaceholder":111},[88,325,327,330,333,336],{"class":90,"line":326},14,[88,328,329],{"class":94},"    for",[88,331,332],{"class":98}," env ",[88,334,335],{"class":94},"in",[88,337,338],{"class":98}," rows:\n",[88,340,342,345,347,350,353,356,359],{"class":90,"line":341},15,[88,343,344],{"class":98},"        status ",[88,346,159],{"class":94},[88,348,349],{"class":171}," \"[green]healthy[\u002F]\"",[88,351,352],{"class":94}," if",[88,354,355],{"class":98}," env.healthy ",[88,357,358],{"class":94},"else",[88,360,361],{"class":171}," \"[red]degraded[\u002F]\"\n",[88,363,365,368,371],{"class":90,"line":364},16,[88,366,367],{"class":98},"        table.add_row(env.name, env.region, ",[88,369,370],{"class":146},"str",[88,372,373],{"class":98},"(env.replicas), env.updated_ago, status)\n",[88,375,377],{"class":90,"line":376},17,[88,378,112],{"emptyLinePlaceholder":111},[88,380,382],{"class":90,"line":381},18,[88,383,384],{"class":98},"    out.print(table)\n",[10,386,387],{},"Four decisions in the column definitions do most of the work.",[10,389,390,396],{},[50,391,392,395],{},[26,393,394],{},"justify=\"right\""," on numbers"," lines up the digits, so a column of replica counts can be scanned\nrather than read.",[10,398,399,405],{},[50,400,401,404],{},[26,402,403],{},"no_wrap=True"," on identifiers"," stops a name people need to copy from being split across two\nlines. It is the single most useful column option, and the one most often missing.",[10,407,408,414],{},[50,409,410,413],{},[26,411,412],{},"style=\"dim\""," on secondary columns"," lets the eye skip them. Timestamps and metadata are usually\ncontext rather than content.",[10,416,417,420,421,424],{},[50,418,419],{},"A caption with the count"," answers \"is this everything\" without another command. Add the filter\ntoo when one is applied: ",[26,422,423],{},"12 of 48 (filtered by --region eu-west-1)",".",[14,426,428],{"id":427},"colour-that-survives-being-redirected","Colour that survives being redirected",[10,430,431,432,435,436,439],{},"The status column above uses ",[26,433,434],{},"[green]"," and ",[26,437,438],{},"[red]",", and it pairs each with a word. That pairing is\nthe important part: a colour-blind reader and a log file both lose the colour, and a row that means\n\"degraded\" only by being red means nothing to either.",[10,441,442],{},"The rule that follows: colour is emphasis, never information. Anything a user must know has to be\nreadable in plain text — which is also what makes the output snapshot-testable.",[10,444,445,446,449],{},"Rich turns colour off automatically when stdout is not a terminal, and honours ",[26,447,448],{},"NO_COLOR",". What it\ncannot decide is your width policy, which is worth setting explicitly so redirected output is\nstable:",[79,451,453],{"className":81,"code":452,"language":83,"meta":84,"style":84},"out = Console(width=None if sys.stdout.isatty() else 100)\n",[26,454,455],{"__ignoreMap":84},[88,456,457,460,462,465,468,470,472,474,477,479,482],{"class":90,"line":91},[88,458,459],{"class":98},"out ",[88,461,159],{"class":94},[88,463,464],{"class":98}," Console(",[88,466,467],{"class":165},"width",[88,469,159],{"class":94},[88,471,147],{"class":146},[88,473,352],{"class":94},[88,475,476],{"class":98}," sys.stdout.isatty() ",[88,478,358],{"class":94},[88,480,481],{"class":146}," 100",[88,483,210],{"class":98},[14,485,487],{"id":486},"when-the-table-does-not-fit","When the table does not fit",[75,489],{"name":490},"wide-table-strategies",[10,492,493],{},"Six columns of real data will not fit an 80-column terminal, and Rich's default response — wrapping\neverything — produces something worse than a list.",[10,495,496],{},"Decide the policy yourself:",[79,498,500],{"className":81,"code":499,"language":83,"meta":84,"style":84},"def render_environments(rows: list[Environment], *, wide: bool = False) -> None:\n    width = out.size.width\n    table = Table()\n    table.add_column(\"Name\", no_wrap=True)\n    table.add_column(\"Status\", justify=\"right\")\n\n    if wide or width >= 100:\n        table.add_column(\"Region\")\n        table.add_column(\"Replicas\", justify=\"right\")\n    if wide or width >= 140:\n        table.add_column(\"Updated\", justify=\"right\", style=\"dim\")\n    ...\n",[26,501,502,533,543,552,568,584,588,609,618,634,651,675],{"__ignoreMap":84},[88,503,504,506,508,511,514,517,520,523,526,529,531],{"class":90,"line":91},[88,505,136],{"class":94},[88,507,140],{"class":139},[88,509,510],{"class":98},"(rows: list[Environment], ",[88,512,513],{"class":94},"*",[88,515,516],{"class":98},", wide: ",[88,518,519],{"class":146},"bool",[88,521,522],{"class":94}," =",[88,524,525],{"class":146}," False",[88,527,528],{"class":98},") -> ",[88,530,147],{"class":146},[88,532,150],{"class":98},[88,534,535,538,540],{"class":90,"line":108},[88,536,537],{"class":98},"    width ",[88,539,159],{"class":94},[88,541,542],{"class":98}," out.size.width\n",[88,544,545,547,549],{"class":90,"line":115},[88,546,156],{"class":98},[88,548,159],{"class":94},[88,550,551],{"class":98}," Table()\n",[88,553,554,556,558,560,562,564,566],{"class":90,"line":128},[88,555,221],{"class":98},[88,557,224],{"class":171},[88,559,29],{"class":98},[88,561,35],{"class":165},[88,563,159],{"class":94},[88,565,242],{"class":146},[88,567,210],{"class":98},[88,569,570,572,574,576,578,580,582],{"class":90,"line":133},[88,571,221],{"class":98},[88,573,308],{"class":171},[88,575,29],{"class":98},[88,577,28],{"class":165},[88,579,159],{"class":94},[88,581,271],{"class":171},[88,583,210],{"class":98},[88,585,586],{"class":90,"line":153},[88,587,112],{"emptyLinePlaceholder":111},[88,589,590,593,596,599,602,605,607],{"class":90,"line":213},[88,591,592],{"class":94},"    if",[88,594,595],{"class":98}," wide ",[88,597,598],{"class":94},"or",[88,600,601],{"class":98}," width ",[88,603,604],{"class":94},">=",[88,606,481],{"class":146},[88,608,150],{"class":98},[88,610,611,614,616],{"class":90,"line":218},[88,612,613],{"class":98},"        table.add_column(",[88,615,252],{"class":171},[88,617,210],{"class":98},[88,619,620,622,624,626,628,630,632],{"class":90,"line":247},[88,621,613],{"class":98},[88,623,262],{"class":171},[88,625,29],{"class":98},[88,627,28],{"class":165},[88,629,159],{"class":94},[88,631,271],{"class":171},[88,633,210],{"class":98},[88,635,636,638,640,642,644,646,649],{"class":90,"line":257},[88,637,592],{"class":94},[88,639,595],{"class":98},[88,641,598],{"class":94},[88,643,601],{"class":98},[88,645,604],{"class":94},[88,647,648],{"class":146}," 140",[88,650,150],{"class":98},[88,652,653,655,657,659,661,663,665,667,669,671,673],{"class":90,"line":276},[88,654,613],{"class":98},[88,656,281],{"class":171},[88,658,29],{"class":98},[88,660,28],{"class":165},[88,662,159],{"class":94},[88,664,271],{"class":171},[88,666,29],{"class":98},[88,668,32],{"class":165},[88,670,159],{"class":94},[88,672,298],{"class":171},[88,674,210],{"class":98},[88,676,677],{"class":90,"line":303},[88,678,679],{"class":146},"    ...\n",[10,681,682,683,686,687,690],{},"Three columns always, five on a normal terminal, everything under ",[26,684,685],{},"--wide",". And when someone needs\nevery field regardless of width, ",[26,688,689],{},"--json"," is the honest answer — say so in the caption or the help\ntext.",[14,692,694],{"id":693},"the-same-data-two-renderings","The same data, two renderings",[75,696],{"name":697},"table-vs-json-decision",[10,699,700],{},"The mistake that produces contradictory output is querying twice. Build the data once and render it\ntwo ways:",[79,702,704],{"className":81,"code":703,"language":83,"meta":84,"style":84},"@app.command()\ndef status(\n    json_output: Annotated[bool, typer.Option(\"--json\", help=\"Emit JSON on stdout.\")] = False,\n    wide: Annotated[bool, typer.Option(help=\"Show every column.\")] = False,\n) -> None:\n    rows = core.collect_status()                     # one query, one list of dataclasses\n\n    if json_output:\n        Envelope(data=[row.as_dict() for row in rows]).emit()\n        return\n\n    render_environments(rows, wide=wide)\n",[26,705,706,714,724,757,781,789,803,807,814,838,843,847],{"__ignoreMap":84},[88,707,708,711],{"class":90,"line":91},[88,709,710],{"class":139},"@app.command",[88,712,713],{"class":98},"()\n",[88,715,716,718,721],{"class":90,"line":108},[88,717,136],{"class":94},[88,719,720],{"class":139}," status",[88,722,723],{"class":98},"(\n",[88,725,726,729,731,734,737,739,742,744,747,750,752,754],{"class":90,"line":115},[88,727,728],{"class":98},"    json_output: Annotated[",[88,730,519],{"class":146},[88,732,733],{"class":98},", typer.Option(",[88,735,736],{"class":171},"\"--json\"",[88,738,29],{"class":98},[88,740,741],{"class":165},"help",[88,743,159],{"class":94},[88,745,746],{"class":171},"\"Emit JSON on stdout.\"",[88,748,749],{"class":98},")] ",[88,751,159],{"class":94},[88,753,525],{"class":146},[88,755,756],{"class":98},",\n",[88,758,759,762,764,766,768,770,773,775,777,779],{"class":90,"line":128},[88,760,761],{"class":98},"    wide: Annotated[",[88,763,519],{"class":146},[88,765,733],{"class":98},[88,767,741],{"class":165},[88,769,159],{"class":94},[88,771,772],{"class":171},"\"Show every column.\"",[88,774,749],{"class":98},[88,776,159],{"class":94},[88,778,525],{"class":146},[88,780,756],{"class":98},[88,782,783,785,787],{"class":90,"line":133},[88,784,528],{"class":98},[88,786,147],{"class":146},[88,788,150],{"class":98},[88,790,791,794,796,799],{"class":90,"line":153},[88,792,793],{"class":98},"    rows ",[88,795,159],{"class":94},[88,797,798],{"class":98}," core.collect_status()                     ",[88,800,802],{"class":801},"sJ8bj","# one query, one list of dataclasses\n",[88,804,805],{"class":90,"line":213},[88,806,112],{"emptyLinePlaceholder":111},[88,808,809,811],{"class":90,"line":218},[88,810,592],{"class":94},[88,812,813],{"class":98}," json_output:\n",[88,815,816,819,822,824,827,830,833,835],{"class":90,"line":247},[88,817,818],{"class":98},"        Envelope(",[88,820,821],{"class":165},"data",[88,823,159],{"class":94},[88,825,826],{"class":98},"[row.as_dict() ",[88,828,829],{"class":94},"for",[88,831,832],{"class":98}," row ",[88,834,335],{"class":94},[88,836,837],{"class":98}," rows]).emit()\n",[88,839,840],{"class":90,"line":257},[88,841,842],{"class":94},"        return\n",[88,844,845],{"class":90,"line":276},[88,846,112],{"emptyLinePlaceholder":111},[88,848,849,852,855,857],{"class":90,"line":303},[88,850,851],{"class":98},"    render_environments(rows, ",[88,853,854],{"class":165},"wide",[88,856,159],{"class":94},[88,858,859],{"class":98},"wide)\n",[10,861,862,863,866,867,871],{},"Because both branches start from ",[26,864,865],{},"rows",", the table and the JSON cannot report different facts. The\nJSON should also carry the fields the table only ",[868,869,870],"em",{},"implies"," — the raw timestamp behind \"3 hours ago\",\nthe boolean behind the coloured word — because a script cannot see that a row was red.",[10,873,874],{},"Rich can pretty-print JSON for a human while keeping it valid:",[79,876,878],{"className":81,"code":877,"language":83,"meta":84,"style":84},"out.print_json(data=payload)      # coloured and indented on a terminal, plain when piped\n",[26,879,880],{"__ignoreMap":84},[88,881,882,885,887,889,892],{"class":90,"line":91},[88,883,884],{"class":98},"out.print_json(",[88,886,821],{"class":165},[88,888,159],{"class":94},[88,890,891],{"class":98},"payload)      ",[88,893,894],{"class":801},"# coloured and indented on a terminal, plain when piped\n",[14,896,898],{"id":897},"other-renderables-worth-knowing","Other renderables worth knowing",[10,900,901,904],{},[50,902,903],{},"Panels"," for a single important thing — a summary at the end of a run, a warning that deserves to\nstand apart. Not for lists; a panel per item is noise.",[10,906,907,910],{},[50,908,909],{},"Columns"," for short items that would waste space as a table: a list of thirty environment names\nlaid out in columns is far easier to scan than thirty lines.",[10,912,913,916,917,920],{},[50,914,915],{},"Trees"," for hierarchy — a command tree, a dependency graph, a directory listing. ",[26,918,919],{},"rich.tree.Tree","\nhandles the drawing characters and degrades to ASCII when the encoding cannot represent them.",[10,922,923],{},"All three take the same styling and all three degrade the same way when output is redirected, which\nis the property that makes them safe to use in a tool that gets piped.",[14,925,927],{"id":926},"formatting-the-values-inside-cells","Formatting the values inside cells",[10,929,930],{},"A table's readability depends as much on the values as on the columns. Four small formatting\ndecisions come up in almost every CLI table.",[10,932,933,936,937,939],{},[50,934,935],{},"Relative times for recency, absolute times for records."," \"3 hours ago\" is what a human wants when\nscanning; an ISO timestamp is what they want when correlating with a log. Show the relative form in\nthe table and the absolute one in the JSON, and put the absolute value in a ",[26,938,685],{}," column when the\naudience needs both.",[10,941,942,945,946,949,950,953],{},[50,943,944],{},"Human-readable sizes, machine-readable in JSON."," ",[26,947,948],{},"1.4 GB"," in the table, ",[26,951,952],{},"1503238553"," in the\npayload. A table of raw byte counts is arithmetic homework.",[10,955,956,959,960,962],{},[50,957,958],{},"Truncate identifiers deliberately."," A commit hash or a UUID rendered in full eats a third of the\nwidth. Show the first eight characters with ",[26,961,403],{},", and keep the full value in the JSON —\nbut never truncate something the user is expected to copy and paste back into your tool.",[10,964,965,968,969,972,973,976,977,979,980,982],{},[50,966,967],{},"Empty means empty."," A missing value should render as ",[26,970,971],{},"—"," or a dim ",[26,974,975],{},"none",", not as ",[26,978,147],{}," or an\nempty cell. ",[26,981,147],{}," leaking into output is a small thing that makes a tool look unfinished, and it is\nusually a formatting function that forgot the case.",[79,984,986],{"className":81,"code":985,"language":83,"meta":84,"style":84},"def cell(value: object) -> str:\n    if value is None or value == \"\":\n        return \"[dim]—[\u002F]\"\n    if isinstance(value, bool):\n        return \"yes\" if value else \"no\"\n    return str(value)\n",[26,987,988,1007,1033,1041,1056,1072],{"__ignoreMap":84},[88,989,990,992,995,998,1001,1003,1005],{"class":90,"line":91},[88,991,136],{"class":94},[88,993,994],{"class":139}," cell",[88,996,997],{"class":98},"(value: ",[88,999,1000],{"class":146},"object",[88,1002,528],{"class":98},[88,1004,370],{"class":146},[88,1006,150],{"class":98},[88,1008,1009,1011,1014,1017,1020,1023,1025,1028,1031],{"class":90,"line":108},[88,1010,592],{"class":94},[88,1012,1013],{"class":98}," value ",[88,1015,1016],{"class":94},"is",[88,1018,1019],{"class":146}," None",[88,1021,1022],{"class":94}," or",[88,1024,1013],{"class":98},[88,1026,1027],{"class":94},"==",[88,1029,1030],{"class":171}," \"\"",[88,1032,150],{"class":98},[88,1034,1035,1038],{"class":90,"line":115},[88,1036,1037],{"class":94},"        return",[88,1039,1040],{"class":171}," \"[dim]—[\u002F]\"\n",[88,1042,1043,1045,1048,1051,1053],{"class":90,"line":128},[88,1044,592],{"class":94},[88,1046,1047],{"class":146}," isinstance",[88,1049,1050],{"class":98},"(value, ",[88,1052,519],{"class":146},[88,1054,1055],{"class":98},"):\n",[88,1057,1058,1060,1063,1065,1067,1069],{"class":90,"line":133},[88,1059,1037],{"class":94},[88,1061,1062],{"class":171}," \"yes\"",[88,1064,352],{"class":94},[88,1066,1013],{"class":98},[88,1068,358],{"class":94},[88,1070,1071],{"class":171}," \"no\"\n",[88,1073,1074,1077,1080],{"class":90,"line":153},[88,1075,1076],{"class":94},"    return",[88,1078,1079],{"class":146}," str",[88,1081,1082],{"class":98},"(value)\n",[10,1084,1085],{},"Ten lines, applied at the point rows are built, and every table in the tool gains consistent\ntreatment of the awkward cases.",[14,1087,1089],{"id":1088},"testing-the-behaviour","Testing the behaviour",[10,1091,1092],{},"Table output is testable if you fix the width and disable colour:",[79,1094,1096],{"className":81,"code":1095,"language":83,"meta":84,"style":84},"from rich.console import Console\n\ndef render_to_text(rows, width=100) -> str:\n    console = Console(width=width, no_color=True, file=io.StringIO())\n    render_environments(rows, console=console)\n    return console.file.getvalue()\n\ndef test_table_shows_the_important_columns():\n    text = render_to_text([Environment(name=\"prod\", healthy=False, replicas=3)])\n\n    assert \"prod\" in text\n    assert \"degraded\" in text          # the word, not just the colour\n    assert \"\\x1b[\" not in text         # no escape sequences at this width\n\ndef test_narrow_terminals_drop_secondary_columns():\n    text = render_to_text(SAMPLE_ROWS, width=70)\n\n    assert \"Name\" in text\n    assert \"Updated\" not in text       # dropped, not wrapped\n",[26,1097,1098,1110,1114,1135,1168,1180,1187,1191,1201,1242,1246,1260,1275,1299,1303,1312,1335,1339,1350],{"__ignoreMap":84},[88,1099,1100,1102,1105,1107],{"class":90,"line":91},[88,1101,95],{"class":94},[88,1103,1104],{"class":98}," rich.console ",[88,1106,102],{"class":94},[88,1108,1109],{"class":98}," Console\n",[88,1111,1112],{"class":90,"line":108},[88,1113,112],{"emptyLinePlaceholder":111},[88,1115,1116,1118,1121,1124,1126,1129,1131,1133],{"class":90,"line":115},[88,1117,136],{"class":94},[88,1119,1120],{"class":139}," render_to_text",[88,1122,1123],{"class":98},"(rows, width",[88,1125,159],{"class":94},[88,1127,1128],{"class":146},"100",[88,1130,528],{"class":98},[88,1132,370],{"class":146},[88,1134,150],{"class":98},[88,1136,1137,1140,1142,1144,1146,1148,1151,1154,1156,1158,1160,1163,1165],{"class":90,"line":128},[88,1138,1139],{"class":98},"    console ",[88,1141,159],{"class":94},[88,1143,464],{"class":98},[88,1145,467],{"class":165},[88,1147,159],{"class":94},[88,1149,1150],{"class":98},"width, ",[88,1152,1153],{"class":165},"no_color",[88,1155,159],{"class":94},[88,1157,242],{"class":146},[88,1159,29],{"class":98},[88,1161,1162],{"class":165},"file",[88,1164,159],{"class":94},[88,1166,1167],{"class":98},"io.StringIO())\n",[88,1169,1170,1172,1175,1177],{"class":90,"line":133},[88,1171,851],{"class":98},[88,1173,1174],{"class":165},"console",[88,1176,159],{"class":94},[88,1178,1179],{"class":98},"console)\n",[88,1181,1182,1184],{"class":90,"line":153},[88,1183,1076],{"class":94},[88,1185,1186],{"class":98}," console.file.getvalue()\n",[88,1188,1189],{"class":90,"line":213},[88,1190,112],{"emptyLinePlaceholder":111},[88,1192,1193,1195,1198],{"class":90,"line":218},[88,1194,136],{"class":94},[88,1196,1197],{"class":139}," test_table_shows_the_important_columns",[88,1199,1200],{"class":98},"():\n",[88,1202,1203,1206,1208,1211,1214,1216,1219,1221,1224,1226,1229,1231,1234,1236,1239],{"class":90,"line":247},[88,1204,1205],{"class":98},"    text ",[88,1207,159],{"class":94},[88,1209,1210],{"class":98}," render_to_text([Environment(",[88,1212,1213],{"class":165},"name",[88,1215,159],{"class":94},[88,1217,1218],{"class":171},"\"prod\"",[88,1220,29],{"class":98},[88,1222,1223],{"class":165},"healthy",[88,1225,159],{"class":94},[88,1227,1228],{"class":146},"False",[88,1230,29],{"class":98},[88,1232,1233],{"class":165},"replicas",[88,1235,159],{"class":94},[88,1237,1238],{"class":146},"3",[88,1240,1241],{"class":98},")])\n",[88,1243,1244],{"class":90,"line":257},[88,1245,112],{"emptyLinePlaceholder":111},[88,1247,1248,1251,1254,1257],{"class":90,"line":276},[88,1249,1250],{"class":94},"    assert",[88,1252,1253],{"class":171}," \"prod\"",[88,1255,1256],{"class":94}," in",[88,1258,1259],{"class":98}," text\n",[88,1261,1262,1264,1267,1269,1272],{"class":90,"line":303},[88,1263,1250],{"class":94},[88,1265,1266],{"class":171}," \"degraded\"",[88,1268,1256],{"class":94},[88,1270,1271],{"class":98}," text          ",[88,1273,1274],{"class":801},"# the word, not just the colour\n",[88,1276,1277,1279,1282,1285,1288,1291,1293,1296],{"class":90,"line":321},[88,1278,1250],{"class":94},[88,1280,1281],{"class":171}," \"",[88,1283,1284],{"class":146},"\\x1b",[88,1286,1287],{"class":171},"[\"",[88,1289,1290],{"class":94}," not",[88,1292,1256],{"class":94},[88,1294,1295],{"class":98}," text         ",[88,1297,1298],{"class":801},"# no escape sequences at this width\n",[88,1300,1301],{"class":90,"line":326},[88,1302,112],{"emptyLinePlaceholder":111},[88,1304,1305,1307,1310],{"class":90,"line":341},[88,1306,136],{"class":94},[88,1308,1309],{"class":139}," test_narrow_terminals_drop_secondary_columns",[88,1311,1200],{"class":98},[88,1313,1314,1316,1318,1321,1324,1326,1328,1330,1333],{"class":90,"line":364},[88,1315,1205],{"class":98},[88,1317,159],{"class":94},[88,1319,1320],{"class":98}," render_to_text(",[88,1322,1323],{"class":146},"SAMPLE_ROWS",[88,1325,29],{"class":98},[88,1327,467],{"class":165},[88,1329,159],{"class":94},[88,1331,1332],{"class":146},"70",[88,1334,210],{"class":98},[88,1336,1337],{"class":90,"line":376},[88,1338,112],{"emptyLinePlaceholder":111},[88,1340,1341,1343,1346,1348],{"class":90,"line":381},[88,1342,1250],{"class":94},[88,1344,1345],{"class":171}," \"Name\"",[88,1347,1256],{"class":94},[88,1349,1259],{"class":98},[88,1351,1353,1355,1358,1360,1362,1365],{"class":90,"line":1352},19,[88,1354,1250],{"class":94},[88,1356,1357],{"class":171}," \"Updated\"",[88,1359,1290],{"class":94},[88,1361,1256],{"class":94},[88,1363,1364],{"class":98}," text       ",[88,1366,1367],{"class":801},"# dropped, not wrapped\n",[10,1369,1370,1371,1374],{},"The ",[26,1372,1373],{},"\"degraded\" in text"," assertion is the one worth keeping permanently: it fails if someone\nreplaces the word with a coloured symbol, which is exactly the change that makes output unreadable\nin a log.",[14,1376,1378],{"id":1377},"conclusion","Conclusion",[10,1380,1381],{},"Configure the columns, pair colour with words, decide what to drop before the terminal does, and\nrender both the table and the JSON from one data structure. That last point is the structural one:\ntwo renderings of the same list cannot disagree, and it is what lets you improve the table freely\nwhile scripts depend on the JSON.",[14,1383,1385],{"id":1384},"frequently-asked-questions","Frequently asked questions",[1387,1388,1390],"h3",{"id":1389},"should-tables-have-borders","Should tables have borders?",[10,1392,1393,1394,1397],{},"Rich's default box is fine, and ",[26,1395,1396],{},"box=None"," produces a lighter list-like layout that suits small\ntables. What to avoid is a heavy box around three rows — the borders then carry more visual weight\nthan the data.",[1387,1399,1401],{"id":1400},"how-do-i-sort-or-filter-a-table","How do I sort or filter a table?",[10,1403,1404,1405,29,1408,1411],{},"In the data, before rendering, and expose it as flags (",[26,1406,1407],{},"--sort",[26,1409,1410],{},"--filter","). Sorting inside the\nrenderer means the JSON and the table can disagree about order, which is exactly the class of bug\nthe shared-data-structure rule exists to prevent.",[1387,1413,1415],{"id":1414},"can-i-stream-rows-into-a-table-as-they-arrive","Can I stream rows into a table as they arrive?",[10,1417,1418,1419,1422,1423,1426],{},"Not into a ",[26,1420,1421],{},"Table",", which renders once it is complete. For streaming output, print rows as they\narrive in a simple aligned format, or use ",[26,1424,1425],{},"Live"," with a table you rebuild — which is heavier and\nonly worth it for a genuinely live display.",[1387,1428,1430],{"id":1429},"what-about-very-long-cell-values","What about very long cell values?",[10,1432,1433,1434,1437],{},"Set ",[26,1435,1436],{},"overflow=\"ellipsis\""," on that column so it truncates visibly rather than wrapping, and make sure\nthe full value is available in the JSON. A truncated value with no way to get the whole one is a\ndead end for the reader.",[1387,1439,1441],{"id":1440},"do-tables-work-in-a-windows-terminal","Do tables work in a Windows terminal?",[10,1443,1444,1445,1448],{},"Yes — Windows Terminal and PowerShell handle the box-drawing characters and colour. The legacy\nconsole may not, and Rich falls back to ASCII boxes; ",[26,1446,1447],{},"Console(safe_box=True)"," forces that behaviour\nif you need it everywhere.",[14,1450,1452],{"id":1451},"related","Related",[19,1454,1455,1462,1469,1475,1482],{},[22,1456,1457,1458],{},"Up: ",[65,1459,1461],{"href":1460},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002F","Interactive terminal UI with Rich",[22,1463,1464,1465],{},"Sideways: ",[65,1466,1468],{"href":1467},"\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,1470,1464,1471],{},[65,1472,1474],{"href":1473},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","Emitting JSON output for scripting",[22,1476,1477,1478],{},"Related: ",[65,1479,1481],{"href":1480},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","Detecting a TTY and adapting output",[22,1483,1477,1484],{},[65,1485,1487],{"href":1486},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F","Snapshot testing CLI output",[32,1489,1490],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .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);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":84,"searchDepth":108,"depth":108,"links":1492},[1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1510],{"id":16,"depth":108,"text":17},{"id":59,"depth":108,"text":60},{"id":72,"depth":108,"text":73},{"id":427,"depth":108,"text":428},{"id":486,"depth":108,"text":487},{"id":693,"depth":108,"text":694},{"id":897,"depth":108,"text":898},{"id":926,"depth":108,"text":927},{"id":1088,"depth":108,"text":1089},{"id":1377,"depth":108,"text":1378},{"id":1384,"depth":108,"text":1385,"children":1504},[1505,1506,1507,1508,1509],{"id":1389,"depth":115,"text":1390},{"id":1400,"depth":115,"text":1401},{"id":1414,"depth":115,"text":1415},{"id":1429,"depth":115,"text":1430},{"id":1440,"depth":115,"text":1441},{"id":1451,"depth":108,"text":1452},"2026-08-01","Build readable CLI tables with Rich - column configuration, semantic styling, narrow terminals, and rendering the same data as JSON for scripts.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich",{"title":5,"description":1512},"advanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich\u002Findex",[1521,1522,1523,1524,1525],"rich","tables","json","output","terminal","W6ODoEwycdJFGOdtR0TFi3d27gVfJbb_b02OCAXzsV8",[1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1586,1589,1592,1595,1598,1601,1604,1607,1610,1613,1616,1619,1622,1625,1628,1631,1634,1637,1640,1643,1646,1649,1652,1655,1658,1661,1664,1667,1670,1673,1676,1679,1682,1685,1688,1691,1694,1697,1700,1703,1706,1709,1712,1715,1718,1721,1724,1727,1730,1733,1736,1739,1742,1745,1748,1751,1754,1757,1760,1763,1766,1769,1772],{"path":1529,"title":1530},"\u002Fabout","About Python CLI Toolcraft",{"path":1532,"title":1533},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1535,"title":1536},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1538,"title":1539},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1541,"title":1542},"\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":1544,"title":1545},"\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":1547,"title":1548},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1550,"title":1551},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1553,"title":1554},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1556,"title":1557},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1559,"title":1560},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1562,"title":1563},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1565,"title":1566},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1568,"title":1569},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1571,"title":1572},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1574,"title":1575},"\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":1577,"title":1578},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1580,"title":1581},"\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":1583,"title":1584},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1517,"title":5},{"path":1587,"title":1588},"\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":1590,"title":1591},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1593,"title":1594},"\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":1596,"title":1597},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1599,"title":1600},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1602,"title":1603},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1605,"title":1606},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1608,"title":1609},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1611,"title":1612},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1614,"title":1615},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1617,"title":1618},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1620,"title":1621},"\u002F","Python CLI Toolcraft",{"path":1623,"title":1624},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1626,"title":1627},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1629,"title":1630},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1632,"title":1633},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1635,"title":1636},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1638,"title":1639},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1641,"title":1642},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1644,"title":1645},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1647,"title":1648},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1650,"title":1651},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1653,"title":1654},"\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":1656,"title":1657},"\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":1659,"title":1660},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1662,"title":1663},"\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":1665,"title":1666},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1668,"title":1669},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1671,"title":1672},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1674,"title":1675},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1677,"title":1678},"\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":1680,"title":1681},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1683,"title":1684},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1686,"title":1687},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1689,"title":1690},"\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":1692,"title":1693},"\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":1695,"title":1696},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1698,"title":1699},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1701,"title":1702},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1704,"title":1705},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1707,"title":1708},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1710,"title":1711},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1713,"title":1714},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1716,"title":1717},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1719,"title":1720},"\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":1722,"title":1723},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1725,"title":1726},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1728,"title":1729},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1731,"title":1732},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1734,"title":1735},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1737,"title":1738},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1740,"title":1741},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1743,"title":1744},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1746,"title":1747},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1749,"title":1750},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1752,"title":1753},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1755,"title":1756},"\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":1758,"title":1759},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1761,"title":1762},"\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":1764,"title":1765},"\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":1767,"title":1768},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1770,"title":1771},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1773,"title":1774},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690029]