[{"data":1,"prerenderedAt":3691},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv\u002F":3,"content-directory":2846},{"id":4,"title":5,"body":6,"date":2831,"description":2832,"difficulty":2833,"draft":2834,"extension":2835,"meta":2836,"navigation":123,"path":2837,"seo":2838,"stem":2839,"tags":2840,"updated":2831,"__hash__":2845},"content\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv\u002Findex.md","Adding a Format Flag for Table, JSON and CSV Output",{"type":7,"value":8,"toc":2814},"minimark",[9,40,45,67,71,79,83,393,396,400,407,1393,1416,1420,1423,1825,1837,1991,1994,1997,2001,2079,2082,2086,2089,2206,2224,2228,2238,2659,2667,2671,2691,2695,2700,2713,2717,2731,2735,2757,2761,2776,2780,2810],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"Your ",[14,15,16],"code",{},"list"," command prints a nice table, and now a colleague wants to pipe it into ",[14,19,20],{},"jq",". The quick fix — an ",[14,23,24],{},"if json:"," branch inside the command — works once and then spreads: the next command copies it, the third command adds ",[14,27,28],{},"--csv",", and within a release the tool has five commands with three different ideas about how output works. This guide builds the alternative from scratch: one ",[14,31,32],{},"--output\u002F-o"," option shared by every listing command, a renderer per format, and a default that does the right thing in a terminal and in a pipe. It is the hands-on recipe behind the ",[35,36,38],"a",{"href":37},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002F","output formats topic",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,48,56,59],"ul",{},[49,50,51,52,55],"li",{},"Python 3.10+ with Typer (",[14,53,54],{},"uv add typer",") or Click 8; Rich comes with Typer and draws the table.",[49,57,58],{},"A command that already produces data as a list of dictionaries, or one you are willing to refactor so it does.",[49,60,61,62,66],{},"Basic familiarity with ",[35,63,65],{"href":64},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting\u002F","emitting JSON output for scripting",", which covers what makes JSON output script-friendly.",[41,68,70],{"id":69},"step-1-separate-the-data-from-the-printing","Step 1: separate the data from the printing",[10,72,73,74,78],{},"The refactor that makes everything else possible is small. A command should ",[75,76,77],"em",{},"return"," records — plain dictionaries with raw values — and something else should decide how to print them. Values stay raw: an ISO timestamp rather than \"2 hours ago\", an integer byte count rather than \"1.2 GB\". Humanising happens only in the table.",[80,81],"inline-diagram",{"name":82},"ofmt-flag-before-after",[84,85,90],"pre",{"className":86,"code":87,"language":88,"meta":89,"style":89},"language-python shiki shiki-themes github-light github-dark","# src\u002Ffleet\u002Fdata.py\nfrom datetime import datetime, timezone\n\ndef fetch_servers() -> list[dict]:\n    \"\"\"Stand-in for an API call. Returns raw, JSON-serialisable values.\"\"\"\n    now = datetime(2026, 10, 2, 12, 0, tzinfo=timezone.utc).isoformat()\n    return [\n        {\"name\": \"web-1\", \"region\": \"eu-west\", \"cpu\": 0.42, \"status\": \"ok\", \"seen\": now},\n        {\"name\": \"web-2\", \"region\": \"eu-west\", \"cpu\": 0.91, \"status\": \"hot\", \"seen\": now},\n        {\"name\": \"db-1\", \"region\": \"us-east\", \"cpu\": 0.18, \"status\": \"ok\", \"seen\": now},\n    ]\n\nSERVER_COLUMNS = [\"name\", \"region\", \"cpu\", \"status\", \"seen\"]\n","python","",[14,91,92,101,118,125,145,152,199,208,261,305,349,355,360],{"__ignoreMap":89},[93,94,97],"span",{"class":95,"line":96},"line",1,[93,98,100],{"class":99},"sJ8bj","# src\u002Ffleet\u002Fdata.py\n",[93,102,104,108,112,115],{"class":95,"line":103},2,[93,105,107],{"class":106},"szBVR","from",[93,109,111],{"class":110},"sVt8B"," datetime ",[93,113,114],{"class":106},"import",[93,116,117],{"class":110}," datetime, timezone\n",[93,119,121],{"class":95,"line":120},3,[93,122,124],{"emptyLinePlaceholder":123},true,"\n",[93,126,128,131,135,138,142],{"class":95,"line":127},4,[93,129,130],{"class":106},"def",[93,132,134],{"class":133},"sScJk"," fetch_servers",[93,136,137],{"class":110},"() -> list[",[93,139,141],{"class":140},"sj4cs","dict",[93,143,144],{"class":110},"]:\n",[93,146,148],{"class":95,"line":147},5,[93,149,151],{"class":150},"sZZnC","    \"\"\"Stand-in for an API call. Returns raw, JSON-serialisable values.\"\"\"\n",[93,153,155,158,161,164,167,170,173,175,178,180,183,185,188,190,194,196],{"class":95,"line":154},6,[93,156,157],{"class":110},"    now ",[93,159,160],{"class":106},"=",[93,162,163],{"class":110}," datetime(",[93,165,166],{"class":140},"2026",[93,168,169],{"class":110},", ",[93,171,172],{"class":140},"10",[93,174,169],{"class":110},[93,176,177],{"class":140},"2",[93,179,169],{"class":110},[93,181,182],{"class":140},"12",[93,184,169],{"class":110},[93,186,187],{"class":140},"0",[93,189,169],{"class":110},[93,191,193],{"class":192},"s4XuR","tzinfo",[93,195,160],{"class":106},[93,197,198],{"class":110},"timezone.utc).isoformat()\n",[93,200,202,205],{"class":95,"line":201},7,[93,203,204],{"class":106},"    return",[93,206,207],{"class":110}," [\n",[93,209,211,214,217,220,223,225,228,230,233,235,238,240,243,245,248,250,253,255,258],{"class":95,"line":210},8,[93,212,213],{"class":110},"        {",[93,215,216],{"class":150},"\"name\"",[93,218,219],{"class":110},": ",[93,221,222],{"class":150},"\"web-1\"",[93,224,169],{"class":110},[93,226,227],{"class":150},"\"region\"",[93,229,219],{"class":110},[93,231,232],{"class":150},"\"eu-west\"",[93,234,169],{"class":110},[93,236,237],{"class":150},"\"cpu\"",[93,239,219],{"class":110},[93,241,242],{"class":140},"0.42",[93,244,169],{"class":110},[93,246,247],{"class":150},"\"status\"",[93,249,219],{"class":110},[93,251,252],{"class":150},"\"ok\"",[93,254,169],{"class":110},[93,256,257],{"class":150},"\"seen\"",[93,259,260],{"class":110},": now},\n",[93,262,264,266,268,270,273,275,277,279,281,283,285,287,290,292,294,296,299,301,303],{"class":95,"line":263},9,[93,265,213],{"class":110},[93,267,216],{"class":150},[93,269,219],{"class":110},[93,271,272],{"class":150},"\"web-2\"",[93,274,169],{"class":110},[93,276,227],{"class":150},[93,278,219],{"class":110},[93,280,232],{"class":150},[93,282,169],{"class":110},[93,284,237],{"class":150},[93,286,219],{"class":110},[93,288,289],{"class":140},"0.91",[93,291,169],{"class":110},[93,293,247],{"class":150},[93,295,219],{"class":110},[93,297,298],{"class":150},"\"hot\"",[93,300,169],{"class":110},[93,302,257],{"class":150},[93,304,260],{"class":110},[93,306,308,310,312,314,317,319,321,323,326,328,330,332,335,337,339,341,343,345,347],{"class":95,"line":307},10,[93,309,213],{"class":110},[93,311,216],{"class":150},[93,313,219],{"class":110},[93,315,316],{"class":150},"\"db-1\"",[93,318,169],{"class":110},[93,320,227],{"class":150},[93,322,219],{"class":110},[93,324,325],{"class":150},"\"us-east\"",[93,327,169],{"class":110},[93,329,237],{"class":150},[93,331,219],{"class":110},[93,333,334],{"class":140},"0.18",[93,336,169],{"class":110},[93,338,247],{"class":150},[93,340,219],{"class":110},[93,342,252],{"class":150},[93,344,169],{"class":110},[93,346,257],{"class":150},[93,348,260],{"class":110},[93,350,352],{"class":95,"line":351},11,[93,353,354],{"class":110},"    ]\n",[93,356,358],{"class":95,"line":357},12,[93,359,124],{"emptyLinePlaceholder":123},[93,361,363,366,369,372,374,376,378,380,382,384,386,388,390],{"class":95,"line":362},13,[93,364,365],{"class":140},"SERVER_COLUMNS",[93,367,368],{"class":106}," =",[93,370,371],{"class":110}," [",[93,373,216],{"class":150},[93,375,169],{"class":110},[93,377,227],{"class":150},[93,379,169],{"class":110},[93,381,237],{"class":150},[93,383,169],{"class":110},[93,385,247],{"class":150},[93,387,169],{"class":110},[93,389,257],{"class":150},[93,391,392],{"class":110},"]\n",[10,394,395],{},"The column list matters as much as the rows. It fixes the order of CSV columns and table headers, and it defines which keys appear in JSON, so a stray internal key in a record never leaks into the output.",[41,397,399],{"id":398},"step-2-one-renderer-per-format","Step 2: one renderer per format",[10,401,402,403,406],{},"Each renderer takes the same two arguments and returns a string. A closed ",[14,404,405],{},"Enum"," names the formats, and a dictionary maps each member to its function:",[84,408,410],{"className":86,"code":409,"language":88,"meta":89,"style":89},"# src\u002Ffleet\u002Foutput.py\nfrom __future__ import annotations\n\nimport csv\nimport io\nimport json\nimport os\nimport shutil\nimport sys\nfrom collections.abc import Callable, Sequence\nfrom enum import Enum\nfrom typing import Any\n\nRow = dict[str, Any]\n\n\nclass Format(str, Enum):\n    table = \"table\"\n    json = \"json\"\n    jsonl = \"jsonl\"\n    csv = \"csv\"\n\n\ndef _table(rows: Sequence[Row], columns: Sequence[str]) -> str:\n    from rich.console import Console\n    from rich.table import Table\n\n    table = Table(*columns, box=None, pad_edge=False, header_style=\"bold\")\n    for row in rows:\n        table.add_row(*(\"\" if row.get(c) is None else str(row[c]) for c in columns))\n    buf = io.StringIO()\n    Console(file=buf, width=shutil.get_terminal_size((120, 24)).columns).print(table)\n    return buf.getvalue()\n\n\ndef _json(rows: Sequence[Row], columns: Sequence[str]) -> str:\n    return json.dumps([{c: r.get(c) for c in columns} for r in rows], indent=2) + \"\\n\"\n\n\ndef _jsonl(rows: Sequence[Row], columns: Sequence[str]) -> str:\n    return \"\".join(\n        json.dumps({c: r.get(c) for c in columns}, separators=(\",\", \":\")) + \"\\n\" for r in rows\n    )\n\n\ndef _csv(rows: Sequence[Row], columns: Sequence[str]) -> str:\n    buf = io.StringIO(newline=\"\")\n    writer = csv.DictWriter(buf, fieldnames=list(columns), extrasaction=\"ignore\",\n                            lineterminator=\"\\n\")\n    writer.writeheader()\n    writer.writerows(rows)\n    return buf.getvalue()\n\n\nRENDERERS: dict[Format, Callable[[Sequence[Row], Sequence[str]], str]] = {\n    Format.table: _table,\n    Format.json: _json,\n    Format.jsonl: _jsonl,\n    Format.csv: _csv,\n}\n\n\ndef resolve_format(explicit: Format | None) -> Format:\n    \"\"\"Flag wins, then FLEET_OUTPUT, then table-on-a-terminal \u002F jsonl-in-a-pipe.\"\"\"\n    if explicit is not None:\n        return explicit\n    env = os.environ.get(\"FLEET_OUTPUT\")\n    if env:\n        try:\n            return Format(env)\n        except ValueError:\n            print(f\"warning: ignoring FLEET_OUTPUT={env!r}\", file=sys.stderr)\n    return Format.table if sys.stdout.isatty() else Format.jsonl\n\n\ndef emit(rows: Sequence[Row], columns: Sequence[str], fmt: Format) -> None:\n    sys.stdout.write(RENDERERS[fmt](rows, columns))\n",[14,411,412,417,430,434,441,448,455,462,469,476,488,500,512,516,533,538,543,564,575,586,597,608,613,618,639,653,666,671,718,733,778,789,822,830,835,840,858,907,912,917,935,946,998,1004,1009,1014,1032,1051,1083,1099,1105,1111,1118,1123,1128,1152,1158,1164,1170,1176,1182,1187,1192,1211,1217,1235,1244,1260,1268,1276,1285,1296,1333,1353,1358,1363,1382],{"__ignoreMap":89},[93,413,414],{"class":95,"line":96},[93,415,416],{"class":99},"# src\u002Ffleet\u002Foutput.py\n",[93,418,419,421,424,427],{"class":95,"line":103},[93,420,107],{"class":106},[93,422,423],{"class":140}," __future__",[93,425,426],{"class":106}," import",[93,428,429],{"class":110}," annotations\n",[93,431,432],{"class":95,"line":120},[93,433,124],{"emptyLinePlaceholder":123},[93,435,436,438],{"class":95,"line":127},[93,437,114],{"class":106},[93,439,440],{"class":110}," csv\n",[93,442,443,445],{"class":95,"line":147},[93,444,114],{"class":106},[93,446,447],{"class":110}," io\n",[93,449,450,452],{"class":95,"line":154},[93,451,114],{"class":106},[93,453,454],{"class":110}," json\n",[93,456,457,459],{"class":95,"line":201},[93,458,114],{"class":106},[93,460,461],{"class":110}," os\n",[93,463,464,466],{"class":95,"line":210},[93,465,114],{"class":106},[93,467,468],{"class":110}," shutil\n",[93,470,471,473],{"class":95,"line":263},[93,472,114],{"class":106},[93,474,475],{"class":110}," sys\n",[93,477,478,480,483,485],{"class":95,"line":307},[93,479,107],{"class":106},[93,481,482],{"class":110}," collections.abc ",[93,484,114],{"class":106},[93,486,487],{"class":110}," Callable, Sequence\n",[93,489,490,492,495,497],{"class":95,"line":351},[93,491,107],{"class":106},[93,493,494],{"class":110}," enum ",[93,496,114],{"class":106},[93,498,499],{"class":110}," Enum\n",[93,501,502,504,507,509],{"class":95,"line":357},[93,503,107],{"class":106},[93,505,506],{"class":110}," typing ",[93,508,114],{"class":106},[93,510,511],{"class":110}," Any\n",[93,513,514],{"class":95,"line":362},[93,515,124],{"emptyLinePlaceholder":123},[93,517,519,522,524,527,530],{"class":95,"line":518},14,[93,520,521],{"class":110},"Row ",[93,523,160],{"class":106},[93,525,526],{"class":110}," dict[",[93,528,529],{"class":140},"str",[93,531,532],{"class":110},", Any]\n",[93,534,536],{"class":95,"line":535},15,[93,537,124],{"emptyLinePlaceholder":123},[93,539,541],{"class":95,"line":540},16,[93,542,124],{"emptyLinePlaceholder":123},[93,544,546,549,552,555,557,559,561],{"class":95,"line":545},17,[93,547,548],{"class":106},"class",[93,550,551],{"class":133}," Format",[93,553,554],{"class":110},"(",[93,556,529],{"class":140},[93,558,169],{"class":110},[93,560,405],{"class":133},[93,562,563],{"class":110},"):\n",[93,565,567,570,572],{"class":95,"line":566},18,[93,568,569],{"class":110},"    table ",[93,571,160],{"class":106},[93,573,574],{"class":150}," \"table\"\n",[93,576,578,581,583],{"class":95,"line":577},19,[93,579,580],{"class":110},"    json ",[93,582,160],{"class":106},[93,584,585],{"class":150}," \"json\"\n",[93,587,589,592,594],{"class":95,"line":588},20,[93,590,591],{"class":110},"    jsonl ",[93,593,160],{"class":106},[93,595,596],{"class":150}," \"jsonl\"\n",[93,598,600,603,605],{"class":95,"line":599},21,[93,601,602],{"class":110},"    csv ",[93,604,160],{"class":106},[93,606,607],{"class":150}," \"csv\"\n",[93,609,611],{"class":95,"line":610},22,[93,612,124],{"emptyLinePlaceholder":123},[93,614,616],{"class":95,"line":615},23,[93,617,124],{"emptyLinePlaceholder":123},[93,619,621,623,626,629,631,634,636],{"class":95,"line":620},24,[93,622,130],{"class":106},[93,624,625],{"class":133}," _table",[93,627,628],{"class":110},"(rows: Sequence[Row], columns: Sequence[",[93,630,529],{"class":140},[93,632,633],{"class":110},"]) -> ",[93,635,529],{"class":140},[93,637,638],{"class":110},":\n",[93,640,642,645,648,650],{"class":95,"line":641},25,[93,643,644],{"class":106},"    from",[93,646,647],{"class":110}," rich.console ",[93,649,114],{"class":106},[93,651,652],{"class":110}," Console\n",[93,654,656,658,661,663],{"class":95,"line":655},26,[93,657,644],{"class":106},[93,659,660],{"class":110}," rich.table ",[93,662,114],{"class":106},[93,664,665],{"class":110}," Table\n",[93,667,669],{"class":95,"line":668},27,[93,670,124],{"emptyLinePlaceholder":123},[93,672,674,676,678,681,684,687,690,692,695,697,700,702,705,707,710,712,715],{"class":95,"line":673},28,[93,675,569],{"class":110},[93,677,160],{"class":106},[93,679,680],{"class":110}," Table(",[93,682,683],{"class":106},"*",[93,685,686],{"class":110},"columns, ",[93,688,689],{"class":192},"box",[93,691,160],{"class":106},[93,693,694],{"class":140},"None",[93,696,169],{"class":110},[93,698,699],{"class":192},"pad_edge",[93,701,160],{"class":106},[93,703,704],{"class":140},"False",[93,706,169],{"class":110},[93,708,709],{"class":192},"header_style",[93,711,160],{"class":106},[93,713,714],{"class":150},"\"bold\"",[93,716,717],{"class":110},")\n",[93,719,721,724,727,730],{"class":95,"line":720},29,[93,722,723],{"class":106},"    for",[93,725,726],{"class":110}," row ",[93,728,729],{"class":106},"in",[93,731,732],{"class":110}," rows:\n",[93,734,736,739,741,743,746,749,752,755,758,761,764,767,770,773,775],{"class":95,"line":735},30,[93,737,738],{"class":110},"        table.add_row(",[93,740,683],{"class":106},[93,742,554],{"class":110},[93,744,745],{"class":150},"\"\"",[93,747,748],{"class":106}," if",[93,750,751],{"class":110}," row.get(c) ",[93,753,754],{"class":106},"is",[93,756,757],{"class":140}," None",[93,759,760],{"class":106}," else",[93,762,763],{"class":140}," str",[93,765,766],{"class":110},"(row[c]) ",[93,768,769],{"class":106},"for",[93,771,772],{"class":110}," c ",[93,774,729],{"class":106},[93,776,777],{"class":110}," columns))\n",[93,779,781,784,786],{"class":95,"line":780},31,[93,782,783],{"class":110},"    buf ",[93,785,160],{"class":106},[93,787,788],{"class":110}," io.StringIO()\n",[93,790,792,795,798,800,803,806,808,811,814,816,819],{"class":95,"line":791},32,[93,793,794],{"class":110},"    Console(",[93,796,797],{"class":192},"file",[93,799,160],{"class":106},[93,801,802],{"class":110},"buf, ",[93,804,805],{"class":192},"width",[93,807,160],{"class":106},[93,809,810],{"class":110},"shutil.get_terminal_size((",[93,812,813],{"class":140},"120",[93,815,169],{"class":110},[93,817,818],{"class":140},"24",[93,820,821],{"class":110},")).columns).print(table)\n",[93,823,825,827],{"class":95,"line":824},33,[93,826,204],{"class":106},[93,828,829],{"class":110}," buf.getvalue()\n",[93,831,833],{"class":95,"line":832},34,[93,834,124],{"emptyLinePlaceholder":123},[93,836,838],{"class":95,"line":837},35,[93,839,124],{"emptyLinePlaceholder":123},[93,841,843,845,848,850,852,854,856],{"class":95,"line":842},36,[93,844,130],{"class":106},[93,846,847],{"class":133}," _json",[93,849,628],{"class":110},[93,851,529],{"class":140},[93,853,633],{"class":110},[93,855,529],{"class":140},[93,857,638],{"class":110},[93,859,861,863,866,868,870,872,875,877,880,882,885,888,890,892,895,898,901,904],{"class":95,"line":860},37,[93,862,204],{"class":106},[93,864,865],{"class":110}," json.dumps([{c: r.get(c) ",[93,867,769],{"class":106},[93,869,772],{"class":110},[93,871,729],{"class":106},[93,873,874],{"class":110}," columns} ",[93,876,769],{"class":106},[93,878,879],{"class":110}," r ",[93,881,729],{"class":106},[93,883,884],{"class":110}," rows], ",[93,886,887],{"class":192},"indent",[93,889,160],{"class":106},[93,891,177],{"class":140},[93,893,894],{"class":110},") ",[93,896,897],{"class":106},"+",[93,899,900],{"class":150}," \"",[93,902,903],{"class":140},"\\n",[93,905,906],{"class":150},"\"\n",[93,908,910],{"class":95,"line":909},38,[93,911,124],{"emptyLinePlaceholder":123},[93,913,915],{"class":95,"line":914},39,[93,916,124],{"emptyLinePlaceholder":123},[93,918,920,922,925,927,929,931,933],{"class":95,"line":919},40,[93,921,130],{"class":106},[93,923,924],{"class":133}," _jsonl",[93,926,628],{"class":110},[93,928,529],{"class":140},[93,930,633],{"class":110},[93,932,529],{"class":140},[93,934,638],{"class":110},[93,936,938,940,943],{"class":95,"line":937},41,[93,939,204],{"class":106},[93,941,942],{"class":150}," \"\"",[93,944,945],{"class":110},".join(\n",[93,947,949,952,954,956,958,961,964,966,968,971,973,976,979,981,983,985,988,991,993,995],{"class":95,"line":948},42,[93,950,951],{"class":110},"        json.dumps({c: r.get(c) ",[93,953,769],{"class":106},[93,955,772],{"class":110},[93,957,729],{"class":106},[93,959,960],{"class":110}," columns}, ",[93,962,963],{"class":192},"separators",[93,965,160],{"class":106},[93,967,554],{"class":110},[93,969,970],{"class":150},"\",\"",[93,972,169],{"class":110},[93,974,975],{"class":150},"\":\"",[93,977,978],{"class":110},")) ",[93,980,897],{"class":106},[93,982,900],{"class":150},[93,984,903],{"class":140},[93,986,987],{"class":150},"\"",[93,989,990],{"class":106}," for",[93,992,879],{"class":110},[93,994,729],{"class":106},[93,996,997],{"class":110}," rows\n",[93,999,1001],{"class":95,"line":1000},43,[93,1002,1003],{"class":110},"    )\n",[93,1005,1007],{"class":95,"line":1006},44,[93,1008,124],{"emptyLinePlaceholder":123},[93,1010,1012],{"class":95,"line":1011},45,[93,1013,124],{"emptyLinePlaceholder":123},[93,1015,1017,1019,1022,1024,1026,1028,1030],{"class":95,"line":1016},46,[93,1018,130],{"class":106},[93,1020,1021],{"class":133}," _csv",[93,1023,628],{"class":110},[93,1025,529],{"class":140},[93,1027,633],{"class":110},[93,1029,529],{"class":140},[93,1031,638],{"class":110},[93,1033,1035,1037,1039,1042,1045,1047,1049],{"class":95,"line":1034},47,[93,1036,783],{"class":110},[93,1038,160],{"class":106},[93,1040,1041],{"class":110}," io.StringIO(",[93,1043,1044],{"class":192},"newline",[93,1046,160],{"class":106},[93,1048,745],{"class":150},[93,1050,717],{"class":110},[93,1052,1054,1057,1059,1062,1065,1067,1069,1072,1075,1077,1080],{"class":95,"line":1053},48,[93,1055,1056],{"class":110},"    writer ",[93,1058,160],{"class":106},[93,1060,1061],{"class":110}," csv.DictWriter(buf, ",[93,1063,1064],{"class":192},"fieldnames",[93,1066,160],{"class":106},[93,1068,16],{"class":140},[93,1070,1071],{"class":110},"(columns), ",[93,1073,1074],{"class":192},"extrasaction",[93,1076,160],{"class":106},[93,1078,1079],{"class":150},"\"ignore\"",[93,1081,1082],{"class":110},",\n",[93,1084,1086,1089,1091,1093,1095,1097],{"class":95,"line":1085},49,[93,1087,1088],{"class":192},"                            lineterminator",[93,1090,160],{"class":106},[93,1092,987],{"class":150},[93,1094,903],{"class":140},[93,1096,987],{"class":150},[93,1098,717],{"class":110},[93,1100,1102],{"class":95,"line":1101},50,[93,1103,1104],{"class":110},"    writer.writeheader()\n",[93,1106,1108],{"class":95,"line":1107},51,[93,1109,1110],{"class":110},"    writer.writerows(rows)\n",[93,1112,1114,1116],{"class":95,"line":1113},52,[93,1115,204],{"class":106},[93,1117,829],{"class":110},[93,1119,1121],{"class":95,"line":1120},53,[93,1122,124],{"emptyLinePlaceholder":123},[93,1124,1126],{"class":95,"line":1125},54,[93,1127,124],{"emptyLinePlaceholder":123},[93,1129,1131,1134,1137,1139,1142,1144,1147,1149],{"class":95,"line":1130},55,[93,1132,1133],{"class":140},"RENDERERS",[93,1135,1136],{"class":110},": dict[Format, Callable[[Sequence[Row], Sequence[",[93,1138,529],{"class":140},[93,1140,1141],{"class":110},"]], ",[93,1143,529],{"class":140},[93,1145,1146],{"class":110},"]] ",[93,1148,160],{"class":106},[93,1150,1151],{"class":110}," {\n",[93,1153,1155],{"class":95,"line":1154},56,[93,1156,1157],{"class":110},"    Format.table: _table,\n",[93,1159,1161],{"class":95,"line":1160},57,[93,1162,1163],{"class":110},"    Format.json: _json,\n",[93,1165,1167],{"class":95,"line":1166},58,[93,1168,1169],{"class":110},"    Format.jsonl: _jsonl,\n",[93,1171,1173],{"class":95,"line":1172},59,[93,1174,1175],{"class":110},"    Format.csv: _csv,\n",[93,1177,1179],{"class":95,"line":1178},60,[93,1180,1181],{"class":110},"}\n",[93,1183,1185],{"class":95,"line":1184},61,[93,1186,124],{"emptyLinePlaceholder":123},[93,1188,1190],{"class":95,"line":1189},62,[93,1191,124],{"emptyLinePlaceholder":123},[93,1193,1195,1197,1200,1203,1206,1208],{"class":95,"line":1194},63,[93,1196,130],{"class":106},[93,1198,1199],{"class":133}," resolve_format",[93,1201,1202],{"class":110},"(explicit: Format ",[93,1204,1205],{"class":106},"|",[93,1207,757],{"class":140},[93,1209,1210],{"class":110},") -> Format:\n",[93,1212,1214],{"class":95,"line":1213},64,[93,1215,1216],{"class":150},"    \"\"\"Flag wins, then FLEET_OUTPUT, then table-on-a-terminal \u002F jsonl-in-a-pipe.\"\"\"\n",[93,1218,1220,1223,1226,1228,1231,1233],{"class":95,"line":1219},65,[93,1221,1222],{"class":106},"    if",[93,1224,1225],{"class":110}," explicit ",[93,1227,754],{"class":106},[93,1229,1230],{"class":106}," not",[93,1232,757],{"class":140},[93,1234,638],{"class":110},[93,1236,1238,1241],{"class":95,"line":1237},66,[93,1239,1240],{"class":106},"        return",[93,1242,1243],{"class":110}," explicit\n",[93,1245,1247,1250,1252,1255,1258],{"class":95,"line":1246},67,[93,1248,1249],{"class":110},"    env ",[93,1251,160],{"class":106},[93,1253,1254],{"class":110}," os.environ.get(",[93,1256,1257],{"class":150},"\"FLEET_OUTPUT\"",[93,1259,717],{"class":110},[93,1261,1263,1265],{"class":95,"line":1262},68,[93,1264,1222],{"class":106},[93,1266,1267],{"class":110}," env:\n",[93,1269,1271,1274],{"class":95,"line":1270},69,[93,1272,1273],{"class":106},"        try",[93,1275,638],{"class":110},[93,1277,1279,1282],{"class":95,"line":1278},70,[93,1280,1281],{"class":106},"            return",[93,1283,1284],{"class":110}," Format(env)\n",[93,1286,1288,1291,1294],{"class":95,"line":1287},71,[93,1289,1290],{"class":106},"        except",[93,1292,1293],{"class":140}," ValueError",[93,1295,638],{"class":110},[93,1297,1299,1302,1304,1307,1310,1313,1316,1319,1322,1324,1326,1328,1330],{"class":95,"line":1298},72,[93,1300,1301],{"class":140},"            print",[93,1303,554],{"class":110},[93,1305,1306],{"class":106},"f",[93,1308,1309],{"class":150},"\"warning: ignoring FLEET_OUTPUT=",[93,1311,1312],{"class":140},"{",[93,1314,1315],{"class":110},"env",[93,1317,1318],{"class":106},"!r",[93,1320,1321],{"class":140},"}",[93,1323,987],{"class":150},[93,1325,169],{"class":110},[93,1327,797],{"class":192},[93,1329,160],{"class":106},[93,1331,1332],{"class":110},"sys.stderr)\n",[93,1334,1336,1338,1341,1344,1347,1350],{"class":95,"line":1335},73,[93,1337,204],{"class":106},[93,1339,1340],{"class":110}," Format.table ",[93,1342,1343],{"class":106},"if",[93,1345,1346],{"class":110}," sys.stdout.isatty() ",[93,1348,1349],{"class":106},"else",[93,1351,1352],{"class":110}," Format.jsonl\n",[93,1354,1356],{"class":95,"line":1355},74,[93,1357,124],{"emptyLinePlaceholder":123},[93,1359,1361],{"class":95,"line":1360},75,[93,1362,124],{"emptyLinePlaceholder":123},[93,1364,1366,1368,1371,1373,1375,1378,1380],{"class":95,"line":1365},76,[93,1367,130],{"class":106},[93,1369,1370],{"class":133}," emit",[93,1372,628],{"class":110},[93,1374,529],{"class":140},[93,1376,1377],{"class":110},"], fmt: Format) -> ",[93,1379,694],{"class":140},[93,1381,638],{"class":110},[93,1383,1385,1388,1390],{"class":95,"line":1384},77,[93,1386,1387],{"class":110},"    sys.stdout.write(",[93,1389,1133],{"class":140},[93,1391,1392],{"class":110},"[fmt](rows, columns))\n",[10,1394,1395,1396,1399,1400,1402,1403,1407,1408,1411,1412,1415],{},"A few details are deliberate. Rich is imported inside ",[14,1397,1398],{},"_table",", so commands piped into ",[14,1401,20],{}," never pay its import cost — see ",[35,1404,1406],{"href":1405},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight\u002F","reducing CLI dependency weight",". JSON Lines uses compact separators because each line is meant for a machine. And the CSV writer sets ",[14,1409,1410],{},"lineterminator=\"\\n\""," explicitly; the module default is ",[14,1413,1414],{},"\\r\\n",", which is correct for RFC 4180 but surprises Unix tools.",[41,1417,1419],{"id":1418},"step-3-wire-the-option-into-commands","Step 3: wire the option into commands",[10,1421,1422],{},"Define the option once and reuse it, so every command spells it the same way and shows the same help:",[84,1424,1426],{"className":86,"code":1425,"language":88,"meta":89,"style":89},"# src\u002Ffleet\u002Fcli.py\nfrom typing import Annotated, Optional\n\nimport typer\n\nfrom fleet.data import SERVER_COLUMNS, fetch_servers\nfrom fleet.output import Format, emit, resolve_format\n\napp = typer.Typer()\n\nOutputOpt = Annotated[\n    Optional[Format],\n    typer.Option(\"--output\", \"-o\", case_sensitive=False,\n                 help=\"Output format. Default: table on a terminal, jsonl when piped.\"),\n]\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Manage the fleet.\"\"\"\n\n\n@app.command(\"list\")\ndef list_servers(output: OutputOpt = None) -> None:\n    \"\"\"List servers.\"\"\"\n    emit(fetch_servers(), SERVER_COLUMNS, resolve_format(output))\n\n\n@app.command()\ndef regions(output: OutputOpt = None) -> None:\n    \"\"\"List regions with server counts.\"\"\"\n    counts: dict[str, int] = {}\n    for s in fetch_servers():\n        counts[s[\"region\"]] = counts.get(s[\"region\"], 0) + 1\n    rows = [{\"region\": r, \"servers\": n} for r, n in sorted(counts.items())]\n    emit(rows, [\"region\", \"servers\"], resolve_format(output))\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1427,1428,1433,1444,1448,1455,1459,1474,1486,1490,1500,1504,1514,1519,1543,1556,1560,1564,1568,1576,1590,1595,1599,1603,1615,1636,1641,1651,1655,1659,1665,1684,1689,1709,1721,1749,1783,1797,1801,1805,1820],{"__ignoreMap":89},[93,1429,1430],{"class":95,"line":96},[93,1431,1432],{"class":99},"# src\u002Ffleet\u002Fcli.py\n",[93,1434,1435,1437,1439,1441],{"class":95,"line":103},[93,1436,107],{"class":106},[93,1438,506],{"class":110},[93,1440,114],{"class":106},[93,1442,1443],{"class":110}," Annotated, Optional\n",[93,1445,1446],{"class":95,"line":120},[93,1447,124],{"emptyLinePlaceholder":123},[93,1449,1450,1452],{"class":95,"line":127},[93,1451,114],{"class":106},[93,1453,1454],{"class":110}," typer\n",[93,1456,1457],{"class":95,"line":147},[93,1458,124],{"emptyLinePlaceholder":123},[93,1460,1461,1463,1466,1468,1471],{"class":95,"line":154},[93,1462,107],{"class":106},[93,1464,1465],{"class":110}," fleet.data ",[93,1467,114],{"class":106},[93,1469,1470],{"class":140}," SERVER_COLUMNS",[93,1472,1473],{"class":110},", fetch_servers\n",[93,1475,1476,1478,1481,1483],{"class":95,"line":201},[93,1477,107],{"class":106},[93,1479,1480],{"class":110}," fleet.output ",[93,1482,114],{"class":106},[93,1484,1485],{"class":110}," Format, emit, resolve_format\n",[93,1487,1488],{"class":95,"line":210},[93,1489,124],{"emptyLinePlaceholder":123},[93,1491,1492,1495,1497],{"class":95,"line":263},[93,1493,1494],{"class":110},"app ",[93,1496,160],{"class":106},[93,1498,1499],{"class":110}," typer.Typer()\n",[93,1501,1502],{"class":95,"line":307},[93,1503,124],{"emptyLinePlaceholder":123},[93,1505,1506,1509,1511],{"class":95,"line":351},[93,1507,1508],{"class":110},"OutputOpt ",[93,1510,160],{"class":106},[93,1512,1513],{"class":110}," Annotated[\n",[93,1515,1516],{"class":95,"line":357},[93,1517,1518],{"class":110},"    Optional[Format],\n",[93,1520,1521,1524,1527,1529,1532,1534,1537,1539,1541],{"class":95,"line":362},[93,1522,1523],{"class":110},"    typer.Option(",[93,1525,1526],{"class":150},"\"--output\"",[93,1528,169],{"class":110},[93,1530,1531],{"class":150},"\"-o\"",[93,1533,169],{"class":110},[93,1535,1536],{"class":192},"case_sensitive",[93,1538,160],{"class":106},[93,1540,704],{"class":140},[93,1542,1082],{"class":110},[93,1544,1545,1548,1550,1553],{"class":95,"line":518},[93,1546,1547],{"class":192},"                 help",[93,1549,160],{"class":106},[93,1551,1552],{"class":150},"\"Output format. Default: table on a terminal, jsonl when piped.\"",[93,1554,1555],{"class":110},"),\n",[93,1557,1558],{"class":95,"line":535},[93,1559,392],{"class":110},[93,1561,1562],{"class":95,"line":540},[93,1563,124],{"emptyLinePlaceholder":123},[93,1565,1566],{"class":95,"line":545},[93,1567,124],{"emptyLinePlaceholder":123},[93,1569,1570,1573],{"class":95,"line":566},[93,1571,1572],{"class":133},"@app.callback",[93,1574,1575],{"class":110},"()\n",[93,1577,1578,1580,1583,1586,1588],{"class":95,"line":577},[93,1579,130],{"class":106},[93,1581,1582],{"class":133}," main",[93,1584,1585],{"class":110},"() -> ",[93,1587,694],{"class":140},[93,1589,638],{"class":110},[93,1591,1592],{"class":95,"line":588},[93,1593,1594],{"class":150},"    \"\"\"Manage the fleet.\"\"\"\n",[93,1596,1597],{"class":95,"line":599},[93,1598,124],{"emptyLinePlaceholder":123},[93,1600,1601],{"class":95,"line":610},[93,1602,124],{"emptyLinePlaceholder":123},[93,1604,1605,1608,1610,1613],{"class":95,"line":615},[93,1606,1607],{"class":133},"@app.command",[93,1609,554],{"class":110},[93,1611,1612],{"class":150},"\"list\"",[93,1614,717],{"class":110},[93,1616,1617,1619,1622,1625,1627,1629,1632,1634],{"class":95,"line":620},[93,1618,130],{"class":106},[93,1620,1621],{"class":133}," list_servers",[93,1623,1624],{"class":110},"(output: OutputOpt ",[93,1626,160],{"class":106},[93,1628,757],{"class":140},[93,1630,1631],{"class":110},") -> ",[93,1633,694],{"class":140},[93,1635,638],{"class":110},[93,1637,1638],{"class":95,"line":641},[93,1639,1640],{"class":150},"    \"\"\"List servers.\"\"\"\n",[93,1642,1643,1646,1648],{"class":95,"line":655},[93,1644,1645],{"class":110},"    emit(fetch_servers(), ",[93,1647,365],{"class":140},[93,1649,1650],{"class":110},", resolve_format(output))\n",[93,1652,1653],{"class":95,"line":668},[93,1654,124],{"emptyLinePlaceholder":123},[93,1656,1657],{"class":95,"line":673},[93,1658,124],{"emptyLinePlaceholder":123},[93,1660,1661,1663],{"class":95,"line":720},[93,1662,1607],{"class":133},[93,1664,1575],{"class":110},[93,1666,1667,1669,1672,1674,1676,1678,1680,1682],{"class":95,"line":735},[93,1668,130],{"class":106},[93,1670,1671],{"class":133}," regions",[93,1673,1624],{"class":110},[93,1675,160],{"class":106},[93,1677,757],{"class":140},[93,1679,1631],{"class":110},[93,1681,694],{"class":140},[93,1683,638],{"class":110},[93,1685,1686],{"class":95,"line":780},[93,1687,1688],{"class":150},"    \"\"\"List regions with server counts.\"\"\"\n",[93,1690,1691,1694,1696,1698,1701,1704,1706],{"class":95,"line":791},[93,1692,1693],{"class":110},"    counts: dict[",[93,1695,529],{"class":140},[93,1697,169],{"class":110},[93,1699,1700],{"class":140},"int",[93,1702,1703],{"class":110},"] ",[93,1705,160],{"class":106},[93,1707,1708],{"class":110}," {}\n",[93,1710,1711,1713,1716,1718],{"class":95,"line":824},[93,1712,723],{"class":106},[93,1714,1715],{"class":110}," s ",[93,1717,729],{"class":106},[93,1719,1720],{"class":110}," fetch_servers():\n",[93,1722,1723,1726,1728,1730,1732,1735,1737,1740,1742,1744,1746],{"class":95,"line":832},[93,1724,1725],{"class":110},"        counts[s[",[93,1727,227],{"class":150},[93,1729,1146],{"class":110},[93,1731,160],{"class":106},[93,1733,1734],{"class":110}," counts.get(s[",[93,1736,227],{"class":150},[93,1738,1739],{"class":110},"], ",[93,1741,187],{"class":140},[93,1743,894],{"class":110},[93,1745,897],{"class":106},[93,1747,1748],{"class":140}," 1\n",[93,1750,1751,1754,1756,1759,1761,1764,1767,1770,1772,1775,1777,1780],{"class":95,"line":837},[93,1752,1753],{"class":110},"    rows ",[93,1755,160],{"class":106},[93,1757,1758],{"class":110}," [{",[93,1760,227],{"class":150},[93,1762,1763],{"class":110},": r, ",[93,1765,1766],{"class":150},"\"servers\"",[93,1768,1769],{"class":110},": n} ",[93,1771,769],{"class":106},[93,1773,1774],{"class":110}," r, n ",[93,1776,729],{"class":106},[93,1778,1779],{"class":140}," sorted",[93,1781,1782],{"class":110},"(counts.items())]\n",[93,1784,1785,1788,1790,1792,1794],{"class":95,"line":842},[93,1786,1787],{"class":110},"    emit(rows, [",[93,1789,227],{"class":150},[93,1791,169],{"class":110},[93,1793,1766],{"class":150},[93,1795,1796],{"class":110},"], resolve_format(output))\n",[93,1798,1799],{"class":95,"line":860},[93,1800,124],{"emptyLinePlaceholder":123},[93,1802,1803],{"class":95,"line":909},[93,1804,124],{"emptyLinePlaceholder":123},[93,1806,1807,1809,1812,1815,1818],{"class":95,"line":914},[93,1808,1343],{"class":106},[93,1810,1811],{"class":140}," __name__",[93,1813,1814],{"class":106}," ==",[93,1816,1817],{"class":150}," \"__main__\"",[93,1819,638],{"class":110},[93,1821,1822],{"class":95,"line":919},[93,1823,1824],{"class":110},"    app()\n",[10,1826,1827,1828,1831,1832,1836],{},"The ",[14,1829,1830],{},"Annotated"," alias is the Typer equivalent of a shared decorator; ",[35,1833,1835],{"href":1834},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands\u002F","sharing common options across commands"," covers the pattern in depth. In Click, the same thing is a reusable decorator:",[84,1838,1840],{"className":86,"code":1839,"language":88,"meta":89,"style":89},"import click\n\noutput_option = click.option(\n    \"--output\", \"-o\",\n    type=click.Choice([f.value for f in Format], case_sensitive=False),\n    default=None,\n    help=\"Output format. Default: table on a terminal, jsonl when piped.\",\n)\n\n@click.command(\"list\")\n@output_option\ndef list_servers(output: str | None) -> None:\n    emit(fetch_servers(), SERVER_COLUMNS, resolve_format(Format(output) if output else None))\n",[14,1841,1842,1849,1853,1863,1874,1902,1913,1924,1928,1932,1943,1948,1970],{"__ignoreMap":89},[93,1843,1844,1846],{"class":95,"line":96},[93,1845,114],{"class":106},[93,1847,1848],{"class":110}," click\n",[93,1850,1851],{"class":95,"line":103},[93,1852,124],{"emptyLinePlaceholder":123},[93,1854,1855,1858,1860],{"class":95,"line":120},[93,1856,1857],{"class":110},"output_option ",[93,1859,160],{"class":106},[93,1861,1862],{"class":110}," click.option(\n",[93,1864,1865,1868,1870,1872],{"class":95,"line":127},[93,1866,1867],{"class":150},"    \"--output\"",[93,1869,169],{"class":110},[93,1871,1531],{"class":150},[93,1873,1082],{"class":110},[93,1875,1876,1879,1881,1884,1886,1889,1891,1894,1896,1898,1900],{"class":95,"line":147},[93,1877,1878],{"class":192},"    type",[93,1880,160],{"class":106},[93,1882,1883],{"class":110},"click.Choice([f.value ",[93,1885,769],{"class":106},[93,1887,1888],{"class":110}," f ",[93,1890,729],{"class":106},[93,1892,1893],{"class":110}," Format], ",[93,1895,1536],{"class":192},[93,1897,160],{"class":106},[93,1899,704],{"class":140},[93,1901,1555],{"class":110},[93,1903,1904,1907,1909,1911],{"class":95,"line":154},[93,1905,1906],{"class":192},"    default",[93,1908,160],{"class":106},[93,1910,694],{"class":140},[93,1912,1082],{"class":110},[93,1914,1915,1918,1920,1922],{"class":95,"line":201},[93,1916,1917],{"class":192},"    help",[93,1919,160],{"class":106},[93,1921,1552],{"class":150},[93,1923,1082],{"class":110},[93,1925,1926],{"class":95,"line":210},[93,1927,717],{"class":110},[93,1929,1930],{"class":95,"line":263},[93,1931,124],{"emptyLinePlaceholder":123},[93,1933,1934,1937,1939,1941],{"class":95,"line":307},[93,1935,1936],{"class":133},"@click.command",[93,1938,554],{"class":110},[93,1940,1612],{"class":150},[93,1942,717],{"class":110},[93,1944,1945],{"class":95,"line":351},[93,1946,1947],{"class":133},"@output_option\n",[93,1949,1950,1952,1954,1957,1959,1962,1964,1966,1968],{"class":95,"line":357},[93,1951,130],{"class":106},[93,1953,1621],{"class":133},[93,1955,1956],{"class":110},"(output: ",[93,1958,529],{"class":140},[93,1960,1961],{"class":106}," |",[93,1963,757],{"class":140},[93,1965,1631],{"class":110},[93,1967,694],{"class":140},[93,1969,638],{"class":110},[93,1971,1972,1974,1976,1979,1981,1984,1986,1988],{"class":95,"line":362},[93,1973,1645],{"class":110},[93,1975,365],{"class":140},[93,1977,1978],{"class":110},", resolve_format(Format(output) ",[93,1980,1343],{"class":106},[93,1982,1983],{"class":110}," output ",[93,1985,1349],{"class":106},[93,1987,757],{"class":140},[93,1989,1990],{"class":110},"))\n",[10,1992,1993],{},"Running it shows the three behaviours side by side:",[80,1995],{"name":1996},"ofmt-flag-terminal",[41,1998,2000],{"id":1999},"ux-considerations","UX considerations",[46,2002,2003,2024,2045,2059,2069],{},[49,2004,2005,2016,2017,2019,2020,2023],{},[2006,2007,2008,2009,2012,2013,39],"strong",{},"Name it ",[14,2010,2011],{},"--output",", short ",[14,2014,2015],{},"-o"," That matches kubectl, the AWS and Azure CLIs and many more, so users guess it. If ",[14,2018,2011],{}," already means \"output file\" in your tool, use ",[14,2021,2022],{},"--format"," and keep it consistent everywhere.",[49,2025,2026,2033,2034,2036,2037,2040,2041,39],{},[2006,2027,2028,2029,2032],{},"Keep ",[14,2030,2031],{},"--json"," as an alias if you already shipped it."," Removing a flag breaks scripts; map ",[14,2035,2031],{}," to ",[14,2038,2039],{},"-o json"," and deprecate it gently, as in ",[35,2042,2044],{"href":2043},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F","versioning and deprecating CLI flags",[49,2046,2047,2050,2051,2054,2055,2058],{},[2006,2048,2049],{},"Say what the default is."," The help text should state the TTY rule. A user who sees JSON Lines in ",[14,2052,2053],{},"| less"," will check ",[14,2056,2057],{},"--help"," first.",[49,2060,2061,2064,2065,39],{},[2006,2062,2063],{},"Errors stay on stderr in every format."," An error in JSON mode can itself be JSON, but it still goes to stderr with a non-zero exit code; see ",[35,2066,2068],{"href":2067},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode\u002F","reporting machine-readable errors in JSON mode",[49,2070,2071,2074,2075,2078],{},[2006,2072,2073],{},"Empty results are still valid output."," ",[14,2076,2077],{},"[]"," for JSON, a header row for CSV, nothing for JSON Lines, and a one-line \"no servers found\" on stderr for the table.",[80,2080],{"name":2081},"ofmt-flag-empty-results",[41,2083,2085],{"id":2084},"adding-a-format-later","Adding a format later",[10,2087,2088],{},"The payoff of the registry design shows the first time someone asks for a new format. Adding YAML is one function and one enum member:",[84,2090,2092],{"className":86,"code":2091,"language":88,"meta":89,"style":89},"class Format(str, Enum):\n    ...\n    yaml = \"yaml\"\n\n\ndef _yaml(rows, columns):\n    import yaml                                  # optional dependency, imported lazily\n    return yaml.safe_dump([{c: r.get(c) for c in columns} for r in rows], sort_keys=False)\n\n\nRENDERERS[Format.yaml] = _yaml\n",[14,2093,2094,2110,2115,2125,2129,2133,2143,2154,2186,2190,2194],{"__ignoreMap":89},[93,2095,2096,2098,2100,2102,2104,2106,2108],{"class":95,"line":96},[93,2097,548],{"class":106},[93,2099,551],{"class":133},[93,2101,554],{"class":110},[93,2103,529],{"class":140},[93,2105,169],{"class":110},[93,2107,405],{"class":133},[93,2109,563],{"class":110},[93,2111,2112],{"class":95,"line":103},[93,2113,2114],{"class":140},"    ...\n",[93,2116,2117,2120,2122],{"class":95,"line":120},[93,2118,2119],{"class":110},"    yaml ",[93,2121,160],{"class":106},[93,2123,2124],{"class":150}," \"yaml\"\n",[93,2126,2127],{"class":95,"line":127},[93,2128,124],{"emptyLinePlaceholder":123},[93,2130,2131],{"class":95,"line":147},[93,2132,124],{"emptyLinePlaceholder":123},[93,2134,2135,2137,2140],{"class":95,"line":154},[93,2136,130],{"class":106},[93,2138,2139],{"class":133}," _yaml",[93,2141,2142],{"class":110},"(rows, columns):\n",[93,2144,2145,2148,2151],{"class":95,"line":201},[93,2146,2147],{"class":106},"    import",[93,2149,2150],{"class":110}," yaml                                  ",[93,2152,2153],{"class":99},"# optional dependency, imported lazily\n",[93,2155,2156,2158,2161,2163,2165,2167,2169,2171,2173,2175,2177,2180,2182,2184],{"class":95,"line":210},[93,2157,204],{"class":106},[93,2159,2160],{"class":110}," yaml.safe_dump([{c: r.get(c) ",[93,2162,769],{"class":106},[93,2164,772],{"class":110},[93,2166,729],{"class":106},[93,2168,874],{"class":110},[93,2170,769],{"class":106},[93,2172,879],{"class":110},[93,2174,729],{"class":106},[93,2176,884],{"class":110},[93,2178,2179],{"class":192},"sort_keys",[93,2181,160],{"class":106},[93,2183,704],{"class":140},[93,2185,717],{"class":110},[93,2187,2188],{"class":95,"line":263},[93,2189,124],{"emptyLinePlaceholder":123},[93,2191,2192],{"class":95,"line":307},[93,2193,124],{"emptyLinePlaceholder":123},[93,2195,2196,2198,2201,2203],{"class":95,"line":351},[93,2197,1133],{"class":140},[93,2199,2200],{"class":110},"[Format.yaml] ",[93,2202,160],{"class":106},[93,2204,2205],{"class":110}," _yaml\n",[10,2207,2208,2209,2212,2213,2215,2216,2219,2220,39],{},"Every listing command gains ",[14,2210,2211],{},"-o yaml"," without being touched, ",[14,2214,2057],{}," lists it automatically, and the existing tests keep passing. If the dependency is optional, catch the ",[14,2217,2218],{},"ImportError"," inside the renderer and tell the user which extra to install, as described in ",[35,2221,2223],{"href":2222},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis\u002F","optional dependencies and extras for CLIs",[41,2225,2227],{"id":2226},"testing-the-behaviour","Testing the behaviour",[10,2229,2230,2233,2234,2237],{},[14,2231,2232],{},"CliRunner"," captures stdout into a buffer, which is not a TTY, so the default format under test is ",[14,2235,2236],{},"jsonl",". That is convenient — assert the default explicitly, then test each format by name:",[84,2239,2241],{"className":86,"code":2240,"language":88,"meta":89,"style":89},"# tests\u002Ftest_output_flag.py\nimport csv\nimport io\nimport json\n\nfrom typer.testing import CliRunner\n\nfrom fleet.cli import app\n\nrunner = CliRunner()\n\n\ndef test_default_in_a_pipe_is_jsonl(monkeypatch):\n    monkeypatch.delenv(\"FLEET_OUTPUT\", raising=False)\n    result = runner.invoke(app, [\"list\"])\n    assert result.exit_code == 0\n    first = json.loads(result.stdout.splitlines()[0])\n    assert first[\"name\"] == \"web-1\"\n\n\ndef test_json_is_one_array():\n    data = json.loads(runner.invoke(app, [\"list\", \"-o\", \"json\"]).stdout)\n    assert [s[\"name\"] for s in data] == [\"web-1\", \"web-2\", \"db-1\"]\n\n\ndef test_csv_has_header_and_rows():\n    text = runner.invoke(app, [\"list\", \"-o\", \"csv\"]).stdout\n    rows = list(csv.DictReader(io.StringIO(text)))\n    assert rows[1][\"status\"] == \"hot\"\n\n\ndef test_env_var_sets_default(monkeypatch):\n    monkeypatch.setenv(\"FLEET_OUTPUT\", \"csv\")\n    assert runner.invoke(app, [\"regions\"]).stdout.startswith(\"region,servers\\n\")\n\n\ndef test_unknown_format_is_a_usage_error():\n    result = runner.invoke(app, [\"list\", \"-o\", \"xml\"])\n    assert result.exit_code == 2\n",[14,2242,2243,2248,2254,2260,2266,2270,2282,2286,2298,2302,2312,2316,2320,2330,2348,2363,2377,2391,2407,2411,2415,2425,2449,2485,2489,2493,2502,2525,2537,2559,2563,2567,2576,2589,2610,2614,2618,2627,2648],{"__ignoreMap":89},[93,2244,2245],{"class":95,"line":96},[93,2246,2247],{"class":99},"# tests\u002Ftest_output_flag.py\n",[93,2249,2250,2252],{"class":95,"line":103},[93,2251,114],{"class":106},[93,2253,440],{"class":110},[93,2255,2256,2258],{"class":95,"line":120},[93,2257,114],{"class":106},[93,2259,447],{"class":110},[93,2261,2262,2264],{"class":95,"line":127},[93,2263,114],{"class":106},[93,2265,454],{"class":110},[93,2267,2268],{"class":95,"line":147},[93,2269,124],{"emptyLinePlaceholder":123},[93,2271,2272,2274,2277,2279],{"class":95,"line":154},[93,2273,107],{"class":106},[93,2275,2276],{"class":110}," typer.testing ",[93,2278,114],{"class":106},[93,2280,2281],{"class":110}," CliRunner\n",[93,2283,2284],{"class":95,"line":201},[93,2285,124],{"emptyLinePlaceholder":123},[93,2287,2288,2290,2293,2295],{"class":95,"line":210},[93,2289,107],{"class":106},[93,2291,2292],{"class":110}," fleet.cli ",[93,2294,114],{"class":106},[93,2296,2297],{"class":110}," app\n",[93,2299,2300],{"class":95,"line":263},[93,2301,124],{"emptyLinePlaceholder":123},[93,2303,2304,2307,2309],{"class":95,"line":307},[93,2305,2306],{"class":110},"runner ",[93,2308,160],{"class":106},[93,2310,2311],{"class":110}," CliRunner()\n",[93,2313,2314],{"class":95,"line":351},[93,2315,124],{"emptyLinePlaceholder":123},[93,2317,2318],{"class":95,"line":357},[93,2319,124],{"emptyLinePlaceholder":123},[93,2321,2322,2324,2327],{"class":95,"line":362},[93,2323,130],{"class":106},[93,2325,2326],{"class":133}," test_default_in_a_pipe_is_jsonl",[93,2328,2329],{"class":110},"(monkeypatch):\n",[93,2331,2332,2335,2337,2339,2342,2344,2346],{"class":95,"line":518},[93,2333,2334],{"class":110},"    monkeypatch.delenv(",[93,2336,1257],{"class":150},[93,2338,169],{"class":110},[93,2340,2341],{"class":192},"raising",[93,2343,160],{"class":106},[93,2345,704],{"class":140},[93,2347,717],{"class":110},[93,2349,2350,2353,2355,2358,2360],{"class":95,"line":535},[93,2351,2352],{"class":110},"    result ",[93,2354,160],{"class":106},[93,2356,2357],{"class":110}," runner.invoke(app, [",[93,2359,1612],{"class":150},[93,2361,2362],{"class":110},"])\n",[93,2364,2365,2368,2371,2374],{"class":95,"line":540},[93,2366,2367],{"class":106},"    assert",[93,2369,2370],{"class":110}," result.exit_code ",[93,2372,2373],{"class":106},"==",[93,2375,2376],{"class":140}," 0\n",[93,2378,2379,2382,2384,2387,2389],{"class":95,"line":545},[93,2380,2381],{"class":110},"    first ",[93,2383,160],{"class":106},[93,2385,2386],{"class":110}," json.loads(result.stdout.splitlines()[",[93,2388,187],{"class":140},[93,2390,2362],{"class":110},[93,2392,2393,2395,2398,2400,2402,2404],{"class":95,"line":566},[93,2394,2367],{"class":106},[93,2396,2397],{"class":110}," first[",[93,2399,216],{"class":150},[93,2401,1703],{"class":110},[93,2403,2373],{"class":106},[93,2405,2406],{"class":150}," \"web-1\"\n",[93,2408,2409],{"class":95,"line":577},[93,2410,124],{"emptyLinePlaceholder":123},[93,2412,2413],{"class":95,"line":588},[93,2414,124],{"emptyLinePlaceholder":123},[93,2416,2417,2419,2422],{"class":95,"line":599},[93,2418,130],{"class":106},[93,2420,2421],{"class":133}," test_json_is_one_array",[93,2423,2424],{"class":110},"():\n",[93,2426,2427,2430,2432,2435,2437,2439,2441,2443,2446],{"class":95,"line":610},[93,2428,2429],{"class":110},"    data ",[93,2431,160],{"class":106},[93,2433,2434],{"class":110}," json.loads(runner.invoke(app, [",[93,2436,1612],{"class":150},[93,2438,169],{"class":110},[93,2440,1531],{"class":150},[93,2442,169],{"class":110},[93,2444,2445],{"class":150},"\"json\"",[93,2447,2448],{"class":110},"]).stdout)\n",[93,2450,2451,2453,2456,2458,2460,2462,2464,2466,2469,2471,2473,2475,2477,2479,2481,2483],{"class":95,"line":615},[93,2452,2367],{"class":106},[93,2454,2455],{"class":110}," [s[",[93,2457,216],{"class":150},[93,2459,1703],{"class":110},[93,2461,769],{"class":106},[93,2463,1715],{"class":110},[93,2465,729],{"class":106},[93,2467,2468],{"class":110}," data] ",[93,2470,2373],{"class":106},[93,2472,371],{"class":110},[93,2474,222],{"class":150},[93,2476,169],{"class":110},[93,2478,272],{"class":150},[93,2480,169],{"class":110},[93,2482,316],{"class":150},[93,2484,392],{"class":110},[93,2486,2487],{"class":95,"line":620},[93,2488,124],{"emptyLinePlaceholder":123},[93,2490,2491],{"class":95,"line":641},[93,2492,124],{"emptyLinePlaceholder":123},[93,2494,2495,2497,2500],{"class":95,"line":655},[93,2496,130],{"class":106},[93,2498,2499],{"class":133}," test_csv_has_header_and_rows",[93,2501,2424],{"class":110},[93,2503,2504,2507,2509,2511,2513,2515,2517,2519,2522],{"class":95,"line":668},[93,2505,2506],{"class":110},"    text ",[93,2508,160],{"class":106},[93,2510,2357],{"class":110},[93,2512,1612],{"class":150},[93,2514,169],{"class":110},[93,2516,1531],{"class":150},[93,2518,169],{"class":110},[93,2520,2521],{"class":150},"\"csv\"",[93,2523,2524],{"class":110},"]).stdout\n",[93,2526,2527,2529,2531,2534],{"class":95,"line":673},[93,2528,1753],{"class":110},[93,2530,160],{"class":106},[93,2532,2533],{"class":140}," list",[93,2535,2536],{"class":110},"(csv.DictReader(io.StringIO(text)))\n",[93,2538,2539,2541,2544,2547,2550,2552,2554,2556],{"class":95,"line":720},[93,2540,2367],{"class":106},[93,2542,2543],{"class":110}," rows[",[93,2545,2546],{"class":140},"1",[93,2548,2549],{"class":110},"][",[93,2551,247],{"class":150},[93,2553,1703],{"class":110},[93,2555,2373],{"class":106},[93,2557,2558],{"class":150}," \"hot\"\n",[93,2560,2561],{"class":95,"line":735},[93,2562,124],{"emptyLinePlaceholder":123},[93,2564,2565],{"class":95,"line":780},[93,2566,124],{"emptyLinePlaceholder":123},[93,2568,2569,2571,2574],{"class":95,"line":791},[93,2570,130],{"class":106},[93,2572,2573],{"class":133}," test_env_var_sets_default",[93,2575,2329],{"class":110},[93,2577,2578,2581,2583,2585,2587],{"class":95,"line":824},[93,2579,2580],{"class":110},"    monkeypatch.setenv(",[93,2582,1257],{"class":150},[93,2584,169],{"class":110},[93,2586,2521],{"class":150},[93,2588,717],{"class":110},[93,2590,2591,2593,2595,2598,2601,2604,2606,2608],{"class":95,"line":832},[93,2592,2367],{"class":106},[93,2594,2357],{"class":110},[93,2596,2597],{"class":150},"\"regions\"",[93,2599,2600],{"class":110},"]).stdout.startswith(",[93,2602,2603],{"class":150},"\"region,servers",[93,2605,903],{"class":140},[93,2607,987],{"class":150},[93,2609,717],{"class":110},[93,2611,2612],{"class":95,"line":837},[93,2613,124],{"emptyLinePlaceholder":123},[93,2615,2616],{"class":95,"line":842},[93,2617,124],{"emptyLinePlaceholder":123},[93,2619,2620,2622,2625],{"class":95,"line":860},[93,2621,130],{"class":106},[93,2623,2624],{"class":133}," test_unknown_format_is_a_usage_error",[93,2626,2424],{"class":110},[93,2628,2629,2631,2633,2635,2637,2639,2641,2643,2646],{"class":95,"line":909},[93,2630,2352],{"class":110},[93,2632,160],{"class":106},[93,2634,2357],{"class":110},[93,2636,1612],{"class":150},[93,2638,169],{"class":110},[93,2640,1531],{"class":150},[93,2642,169],{"class":110},[93,2644,2645],{"class":150},"\"xml\"",[93,2647,2362],{"class":110},[93,2649,2650,2652,2654,2656],{"class":95,"line":914},[93,2651,2367],{"class":106},[93,2653,2370],{"class":110},[93,2655,2373],{"class":106},[93,2657,2658],{"class":140}," 2\n",[10,2660,2661,2662,2666],{},"Parse the output in tests instead of comparing strings, except for one snapshot per format at the command level — that is where you want a reviewer to see a changed key. ",[35,2663,2665],{"href":2664},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner\u002F","Testing Click commands with CliRunner"," covers the runner's quirks, including separating stdout from stderr.",[41,2668,2670],{"id":2669},"conclusion","Conclusion",[10,2672,2673,2674,2676,2677,169,2681,2685,2686,2690],{},"A format flag is a small feature with a large effect on how scriptable a tool feels. Get the boundary right — commands return records, renderers print them — and the flag itself is a few lines: an ",[14,2675,405],{},", a dictionary of functions and a shared option. From there, ",[35,2678,2680],{"href":2679},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly\u002F","CSV details",[35,2682,2684],{"href":2683},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output\u002F","field selection"," and ",[35,2687,2689],{"href":2688},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files\u002F","file export"," all plug into the same records.",[41,2692,2694],{"id":2693},"frequently-asked-questions","Frequently asked questions",[2696,2697,2699],"h3",{"id":2698},"why-default-to-json-lines-rather-than-json-in-a-pipe","Why default to JSON Lines rather than JSON in a pipe?",[10,2701,2702,2703,169,2706,2685,2709,2712],{},"JSON Lines streams: ",[14,2704,2705],{},"head",[14,2707,2708],{},"grep",[14,2710,2711],{},"jq -c"," can work on the first record before the last one exists, and a consumer never has to hold the whole array in memory. A single JSON array is better when the consumer wants the complete result, so offer both and pick JSON Lines for the implicit pipe default.",[2696,2714,2716],{"id":2715},"should-the-format-option-be-global-or-per-command","Should the format option be global or per command?",[10,2718,2719,2720,2722,2723,2726,2727,39],{},"Per command, usually, because only listing commands support every format. A global ",[14,2721,2011],{}," on the root group forces commands that print a single confirmation to handle ",[14,2724,2725],{},"csv",". If most of your commands list things, a global option stored on the context works too; see ",[35,2728,2730],{"href":2729},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options\u002F","global options vs per-command options",[2696,2732,2734],{"id":2733},"how-do-i-print-dates-and-decimals-in-json","How do I print dates and decimals in JSON?",[10,2736,2737,2738,2741,2742,2745,2746,2749,2750,2036,2753,2756],{},"Convert them before rendering: ",[14,2739,2740],{},"datetime.isoformat()"," for timestamps and either ",[14,2743,2744],{},"float"," or a string for ",[14,2747,2748],{},"Decimal",", depending on whether precision matters. Alternatively pass ",[14,2751,2752],{},"default=str",[14,2754,2755],{},"json.dumps",", which handles both — but be explicit for the fields scripts care about.",[2696,2758,2760],{"id":2759},"can-i-let-users-pick-a-table-style","Can I let users pick a table style?",[10,2762,2763,2764,2767,2768,2771,2772,2775],{},"You can, but keep it inside the table renderer — for example ",[14,2765,2766],{},"-o table"," plus a ",[14,2769,2770],{},"--no-headers"," flag, or a ",[14,2773,2774],{},"wide"," format that shows extra columns as kubectl does. Do not let table styling options leak into the machine formats.",[41,2777,2779],{"id":2778},"related","Related",[46,2781,2782,2788,2793,2798,2804],{},[49,2783,2784,2785],{},"Up: ",[35,2786,2787],{"href":37},"Output formats for data-heavy Python CLIs",[49,2789,2790],{},[35,2791,2792],{"href":2679},"Writing CSV and TSV output correctly",[49,2794,2795],{},[35,2796,2797],{"href":2683},"Selecting fields and columns from CLI output",[49,2799,2800],{},[35,2801,2803],{"href":2802},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich\u002F","Rendering tables and JSON with Rich",[49,2805,2806],{},[35,2807,2809],{"href":2808},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","Detecting a TTY and adapting output",[2811,2812,2813],"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 .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":89,"searchDepth":103,"depth":103,"links":2815},[2816,2817,2818,2819,2820,2821,2822,2823,2824,2830],{"id":43,"depth":103,"text":44},{"id":69,"depth":103,"text":70},{"id":398,"depth":103,"text":399},{"id":1418,"depth":103,"text":1419},{"id":1999,"depth":103,"text":2000},{"id":2084,"depth":103,"text":2085},{"id":2226,"depth":103,"text":2227},{"id":2669,"depth":103,"text":2670},{"id":2693,"depth":103,"text":2694,"children":2825},[2826,2827,2828,2829],{"id":2698,"depth":120,"text":2699},{"id":2715,"depth":120,"text":2716},{"id":2733,"depth":120,"text":2734},{"id":2759,"depth":120,"text":2760},{"id":2778,"depth":103,"text":2779},"2026-10-02","Add one --output option to a Typer or Click CLI that renders the same records as a table, JSON, JSON Lines or CSV, with a TTY-aware default and tests.","beginner",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv",{"title":5,"description":2832},"advanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv\u002Findex",[2841,2842,2725,2843,2844],"output","json","typer","click","I1J5jCoqQwO-sHys-8daPVYAaf60mJZCuesk8IxEDwc",[2847,2850,2853,2856,2859,2862,2865,2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2973,2976,2979,2982,2985,2988,2991,2994,2997,3000,3003,3006,3009,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3313,3316,3319,3322,3325,3328,3331,3334,3337,3340,3343,3346,3349,3352,3355,3358,3361,3364,3367,3370,3373,3376,3379,3382,3385,3388,3391,3394,3397,3400,3403,3406,3409,3412,3415,3418,3421,3424,3427,3430,3433,3436,3439,3442,3445,3448,3451,3454,3457,3460,3463,3466,3469,3472,3475,3478,3481,3484,3487,3490,3493,3496,3499,3502,3505,3508,3511,3514,3517,3520,3523,3526,3529,3532,3535,3538,3541,3544,3547,3550,3553,3556,3559,3562,3565,3568,3571,3574,3577,3580,3583,3586,3589,3592,3595,3598,3601,3604,3607,3610,3613,3616,3619,3622,3625,3628,3631,3634,3637,3640,3643,3646,3649,3652,3655,3658,3661,3664,3667,3670,3673,3676,3679,3682,3685,3688],{"path":2848,"title":2849},"\u002Fabout","About Python CLI Toolcraft",{"path":2851,"title":2852},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2854,"title":2855},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2857,"title":2858},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":2860,"title":2861},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2863,"title":2864},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2866,"title":2867},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2869,"title":2870},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2872,"title":2873},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2875,"title":2876},"\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":2878,"title":2879},"\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":2881,"title":2882},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2884,"title":2885},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":2887,"title":2888},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2890,"title":2891},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2893,"title":2894},"\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":2896,"title":2897},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2899,"title":2900},"\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":2902,"title":2903},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2905,"title":2906},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":2908,"title":2909},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2911,"title":2912},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2914,"title":2915},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2917,"title":2918},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2920,"title":2921},"\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":2923,"title":2924},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2926,"title":2927},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2929,"title":2930},"\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":2932,"title":2933},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":2935,"title":2936},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2938,"title":2939},"\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":2941,"title":2942},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":2944,"title":2945},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2947,"title":2948},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2950,"title":2951},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2953,"title":2954},"\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":2956,"title":2957},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2959,"title":2960},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2962,"title":2963},"\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":2965,"title":2966},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2968,"title":2969},"\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":2971,"title":2972},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2974,"title":2975},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2977,"title":2978},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2980,"title":2981},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2983,"title":2984},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2986,"title":2987},"\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":2989,"title":2990},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2992,"title":2993},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2995,"title":2996},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2998,"title":2999},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3001,"title":3002},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":3004,"title":3005},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3007,"title":3008},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2837,"title":5},{"path":3011,"title":3012},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":3014,"title":3015},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":3017,"title":3018},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3020,"title":3021},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":3023,"title":3024},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":3026,"title":3027},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3029,"title":3030},"\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":3032,"title":3033},"\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":3035,"title":3036},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3038,"title":3039},"\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":3041,"title":3042},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3044,"title":3045},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3047,"title":3048},"\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":3050,"title":3051},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3053,"title":3054},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3056,"title":3057},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3059,"title":3060},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":3062,"title":3063},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3065,"title":3066},"\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":3068,"title":3069},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3071,"title":3072},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3074,"title":3075},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3077,"title":3078},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3080,"title":3081},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":3083,"title":3084},"\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":3086,"title":3087},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3089,"title":3090},"\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":3092,"title":3093},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3095,"title":3096},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3098,"title":3099},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":3101,"title":3102},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3104,"title":3105},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3107,"title":3108},"\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":3110,"title":3111},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":3113,"title":3114},"\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":3116,"title":3117},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3119,"title":3120},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3122,"title":3123},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3125,"title":3126},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3128,"title":3129},"\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":3131,"title":3132},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":3134,"title":3135},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":3137,"title":3138},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3140,"title":3141},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3143,"title":3144},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":3146,"title":3147},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3149,"title":3150},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3152,"title":3153},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3155,"title":3156},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":3158,"title":3159},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3161,"title":3162},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3164,"title":3165},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":3167,"title":3168},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3170,"title":3171},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":3173,"title":3174},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":3176,"title":3177},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":3179,"title":3180},"\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":3182,"title":3183},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3185,"title":3186},"\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":3188,"title":3189},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3191,"title":3192},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3194,"title":3195},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":3197,"title":3198},"\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":3200,"title":3201},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":3203,"title":3204},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3206,"title":3207},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3209,"title":3210},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3212,"title":3213},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3215,"title":3216},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":3218,"title":3219},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3221,"title":3222},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3224,"title":3225},"\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":3227,"title":3228},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3230,"title":3231},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3233,"title":3234},"\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":3236,"title":3237},"\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":3239,"title":3240},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3242,"title":3243},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3245,"title":3246},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3248,"title":3249},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":3251,"title":3252},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3254,"title":3255},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":3257,"title":3258},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":3260,"title":3261},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":3263,"title":3264},"\u002F","Python CLI Toolcraft",{"path":3266,"title":3267},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3269,"title":3270},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3272,"title":3273},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3275,"title":3276},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3278,"title":3279},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":3281,"title":3282},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":3284,"title":3285},"\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":3287,"title":3288},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3290,"title":3291},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3293,"title":3294},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3296,"title":3297},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3299,"title":3300},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3302,"title":3303},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3305,"title":3306},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3308,"title":3309},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":3311,"title":3312},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3314,"title":3315},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3317,"title":3318},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3320,"title":3321},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":3323,"title":3324},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3326,"title":3327},"\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":3329,"title":3330},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3332,"title":3333},"\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":3335,"title":3336},"\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":3338,"title":3339},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3341,"title":3342},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3344,"title":3345},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3347,"title":3348},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3350,"title":3351},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3353,"title":3354},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3356,"title":3357},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3359,"title":3360},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3362,"title":3363},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3365,"title":3366},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3368,"title":3369},"\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":3371,"title":3372},"\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":3374,"title":3375},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3377,"title":3378},"\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":3380,"title":3381},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3383,"title":3384},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":3386,"title":3387},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3389,"title":3390},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3392,"title":3393},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3395,"title":3396},"\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":3398,"title":3399},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3401,"title":3402},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3404,"title":3405},"\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":3407,"title":3408},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3410,"title":3411},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3413,"title":3414},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3416,"title":3417},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3419,"title":3420},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3422,"title":3423},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3425,"title":3426},"\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":3428,"title":3429},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3431,"title":3432},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":3434,"title":3435},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":3437,"title":3438},"\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":3440,"title":3441},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3443,"title":3444},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":3446,"title":3447},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3449,"title":3450},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3452,"title":3453},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3455,"title":3456},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3458,"title":3459},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3461,"title":3462},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":3464,"title":3465},"\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":3467,"title":3468},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":3470,"title":3471},"\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":3473,"title":3474},"\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":3476,"title":3477},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3479,"title":3480},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3482,"title":3483},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3485,"title":3486},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3488,"title":3489},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3491,"title":3492},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3494,"title":3495},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3497,"title":3498},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3500,"title":3501},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3503,"title":3504},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3506,"title":3507},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3509,"title":3510},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3512,"title":3513},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3515,"title":3516},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3518,"title":3519},"\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":3521,"title":3522},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3524,"title":3525},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3527,"title":3528},"\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":3530,"title":3531},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3533,"title":3534},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3536,"title":3537},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3539,"title":3540},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3542,"title":3543},"\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":3545,"title":3546},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3548,"title":3549},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3551,"title":3552},"\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":3554,"title":3555},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3557,"title":3558},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3560,"title":3561},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3563,"title":3564},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3566,"title":3567},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3569,"title":3570},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3572,"title":3573},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3575,"title":3576},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3578,"title":3579},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3581,"title":3582},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3584,"title":3585},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3587,"title":3588},"\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":3590,"title":3591},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3593,"title":3594},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3596,"title":3597},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3599,"title":3600},"\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":3602,"title":3603},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3605,"title":3606},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3608,"title":3609},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3611,"title":3612},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3614,"title":3615},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3617,"title":3618},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3620,"title":3621},"\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":3623,"title":3624},"\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":3626,"title":3627},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3629,"title":3630},"\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":3632,"title":3633},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3635,"title":3636},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3638,"title":3639},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3641,"title":3642},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3644,"title":3645},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3647,"title":3648},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3650,"title":3651},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3653,"title":3654},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3656,"title":3657},"\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":3659,"title":3660},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3662,"title":3663},"\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":3665,"title":3666},"\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":3668,"title":3669},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3671,"title":3672},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3674,"title":3675},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3677,"title":3678},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3680,"title":3681},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3683,"title":3684},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3686,"title":3687},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3689,"title":3690},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537213]