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