[{"data":1,"prerenderedAt":1418},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002F":3,"content-directory":1170},{"id":4,"title":5,"body":6,"date":1155,"description":1156,"difficulty":1157,"draft":1158,"extension":1159,"meta":1160,"navigation":147,"path":1161,"seo":1162,"stem":1163,"tags":1164,"updated":1155,"__hash__":1169},"content\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002Findex.md","Writing Help Text Users Actually Read",{"type":7,"value":8,"toc":1133},"minimark",[9,13,18,70,74,89,93,167,174,177,183,189,195,199,203,211,281,288,384,390,394,397,414,528,539,616,620,623,630,772,775,779,782,788,798,804,810,814,817,984,987,995,999,1002,1006,1011,1014,1018,1021,1028,1038,1042,1045,1049,1060,1064,1075,1079,1090,1094,1129],[10,11,12],"p",{},"Nobody reads a help screen. They scan it, looking for the one line that answers their question,\nand they give up quickly. Writing help text well is mostly about respecting that: put the answer\nwhere the eye lands, and cut everything that does not carry information.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,30,37,56,67],"ul",{},[22,23,24,25,29],"li",{},"The ",[26,27,28],"strong",{},"first line of the docstring"," is the summary shown next to the command name — write it for\nusers.",[22,31,32,33,36],{},"One line per option, starting with a ",[26,34,35],{},"verb",", naming what it accepts and what the default is.",[22,38,39,40,43,44,48,49,48,52,55],{},"Use a ",[26,41,42],{},"metavar"," to say what a value is: ",[45,46,47],"code",{},"SECONDS",", ",[45,50,51],{},"PATH",[45,53,54],{},"NAME",".",[22,57,58,59,62,63,66],{},"Enable ",[45,60,61],{},"-h"," as well as ",[45,64,65],{},"--help","; it is what people type first.",[22,68,69],{},"Clamp the width to around 100 columns so long lines stay readable.",[14,71,73],{"id":72},"prerequisites","Prerequisites",[10,75,76,77,80,81,84,85,88],{},"A Click or Typer application with a few commands. Everything here applies to ",[45,78,79],{},"argparse"," too, with\n",[45,82,83],{},"help="," and ",[45,86,87],{},"metavar="," in place of the decorators.",[14,90,92],{"id":91},"the-summary-is-the-most-read-sentence","The summary is the most-read sentence",[94,95,100],"pre",{"className":96,"code":97,"language":98,"meta":99,"style":99},"language-python shiki shiki-themes github-light github-dark","@app.command()\ndef sync(source: Path) -> None:\n    \"\"\"Upload changed files from SOURCE to the configured bucket.\n\n    Files already present with the same checksum are skipped. Use --dry-run to\n    see what would happen without uploading anything.\n    \"\"\"\n","python","",[45,101,102,115,135,142,149,155,161],{"__ignoreMap":99},[103,104,107,111],"span",{"class":105,"line":106},"line",1,[103,108,110],{"class":109},"sScJk","@app.command",[103,112,114],{"class":113},"sVt8B","()\n",[103,116,118,122,125,128,132],{"class":105,"line":117},2,[103,119,121],{"class":120},"szBVR","def",[103,123,124],{"class":109}," sync",[103,126,127],{"class":113},"(source: Path) -> ",[103,129,131],{"class":130},"sj4cs","None",[103,133,134],{"class":113},":\n",[103,136,138],{"class":105,"line":137},3,[103,139,141],{"class":140},"sZZnC","    \"\"\"Upload changed files from SOURCE to the configured bucket.\n",[103,143,145],{"class":105,"line":144},4,[103,146,148],{"emptyLinePlaceholder":147},true,"\n",[103,150,152],{"class":105,"line":151},5,[103,153,154],{"class":140},"    Files already present with the same checksum are skipped. Use --dry-run to\n",[103,156,158],{"class":105,"line":157},6,[103,159,160],{"class":140},"    see what would happen without uploading anything.\n",[103,162,164],{"class":105,"line":163},7,[103,165,166],{"class":140},"    \"\"\"\n",[10,168,169,170,173],{},"The first line appears twice: on this command's help screen and, crucially, beside the command name\non the parent screen. That second appearance is how someone scanning ",[45,171,172],{},"mytool --help"," decides which\ncommand they want, which makes it worth more attention than the rest of the docstring combined.",[10,175,176],{},"Three rules make summaries useful:",[10,178,179,182],{},[26,180,181],{},"Start with a verb, in the imperative."," \"Upload changed files…\" not \"This command uploads…\" —\nshorter, and consistent with every other tool the reader uses.",[10,184,185,188],{},[26,186,187],{},"Name the domain object."," \"Upload changed files to the bucket\" beats \"Perform a synchronisation\noperation\", which says nothing a reader did not already infer from the command name.",[10,190,191,194],{},[26,192,193],{},"Fit on one line."," Around 60 characters is what fits beside a command name in most layouts;\nlonger and it wraps or gets truncated, which looks careless.",[14,196,198],{"id":197},"option-lines-that-say-something","Option lines that say something",[200,201],"inline-diagram",{"name":202},"help-text-checklist",[10,204,205,206,210],{},"An option's help text has one job: let the reader decide whether they need it. That means saying\nwhat it ",[207,208,209],"em",{},"does",", not what it is called.",[94,212,214],{"className":96,"code":213,"language":98,"meta":99,"style":99},"# restates the flag name\nforce: Annotated[bool, typer.Option(help=\"Force mode.\")] = False\n\n# tells the reader when to reach for it\nforce: Annotated[bool, typer.Option(help=\"Upload even when the remote copy looks newer.\")] = False\n",[45,215,216,222,251,255,260],{"__ignoreMap":99},[103,217,218],{"class":105,"line":106},[103,219,221],{"class":220},"sJ8bj","# restates the flag name\n",[103,223,224,227,230,233,237,240,243,246,248],{"class":105,"line":117},[103,225,226],{"class":113},"force: Annotated[",[103,228,229],{"class":130},"bool",[103,231,232],{"class":113},", typer.Option(",[103,234,236],{"class":235},"s4XuR","help",[103,238,239],{"class":120},"=",[103,241,242],{"class":140},"\"Force mode.\"",[103,244,245],{"class":113},")] ",[103,247,239],{"class":120},[103,249,250],{"class":130}," False\n",[103,252,253],{"class":105,"line":137},[103,254,148],{"emptyLinePlaceholder":147},[103,256,257],{"class":105,"line":144},[103,258,259],{"class":220},"# tells the reader when to reach for it\n",[103,261,262,264,266,268,270,272,275,277,279],{"class":105,"line":151},[103,263,226],{"class":113},[103,265,229],{"class":130},[103,267,232],{"class":113},[103,269,236],{"class":235},[103,271,239],{"class":120},[103,273,274],{"class":140},"\"Upload even when the remote copy looks newer.\"",[103,276,245],{"class":113},[103,278,239],{"class":120},[103,280,250],{"class":130},[10,282,283,284,287],{},"Defaults belong in the option line rather than in prose. Typer prints them automatically; Click\nneeds ",[45,285,286],{},"show_default=True",", which is worth setting globally:",[94,289,291],{"className":96,"code":290,"language":98,"meta":99,"style":99},"CONTEXT_SETTINGS = {\"show_default\": True, \"help_option_names\": [\"-h\", \"--help\"], \"max_content_width\": 100}\n\n@click.group(context_settings=CONTEXT_SETTINGS)\ndef cli() -> None:\n    ...\n",[45,292,293,343,347,365,379],{"__ignoreMap":99},[103,294,295,298,301,304,307,310,313,315,318,321,324,326,329,332,335,337,340],{"class":105,"line":106},[103,296,297],{"class":130},"CONTEXT_SETTINGS",[103,299,300],{"class":120}," =",[103,302,303],{"class":113}," {",[103,305,306],{"class":140},"\"show_default\"",[103,308,309],{"class":113},": ",[103,311,312],{"class":130},"True",[103,314,48],{"class":113},[103,316,317],{"class":140},"\"help_option_names\"",[103,319,320],{"class":113},": [",[103,322,323],{"class":140},"\"-h\"",[103,325,48],{"class":113},[103,327,328],{"class":140},"\"--help\"",[103,330,331],{"class":113},"], ",[103,333,334],{"class":140},"\"max_content_width\"",[103,336,309],{"class":113},[103,338,339],{"class":130},"100",[103,341,342],{"class":113},"}\n",[103,344,345],{"class":105,"line":117},[103,346,148],{"emptyLinePlaceholder":147},[103,348,349,352,355,358,360,362],{"class":105,"line":137},[103,350,351],{"class":109},"@click.group",[103,353,354],{"class":113},"(",[103,356,357],{"class":235},"context_settings",[103,359,239],{"class":120},[103,361,297],{"class":130},[103,363,364],{"class":113},")\n",[103,366,367,369,372,375,377],{"class":105,"line":144},[103,368,121],{"class":120},[103,370,371],{"class":109}," cli",[103,373,374],{"class":113},"() -> ",[103,376,131],{"class":130},[103,378,134],{"class":113},[103,380,381],{"class":105,"line":151},[103,382,383],{"class":130},"    ...\n",[10,385,386,387,389],{},"Those three settings together fix most of what is wrong with a default help screen: defaults become\nvisible, ",[45,388,61],{}," works, and lines stop stretching across a wide terminal.",[14,391,393],{"id":392},"metavars-naming-what-a-value-is","Metavars: naming what a value is",[200,395],{"name":396},"usage-string-anatomy",[10,398,399,400,48,403,84,406,409,410,413],{},"By default the placeholder is derived from the type, which gives you ",[45,401,402],{},"INTEGER",[45,404,405],{},"TEXT",[45,407,408],{},"FLOAT","\n— accurate and unhelpful. A metavar says what the value ",[207,411,412],{},"means",":",[94,415,417],{"className":96,"code":416,"language":98,"meta":99,"style":99},"timeout: Annotated[float, typer.Option(metavar=\"SECONDS\", help=\"Time to wait per attempt.\")] = 30.0\noutput: Annotated[Path, typer.Option(\"--output\", \"-o\", metavar=\"PATH\", help=\"Write results here.\")] = Path(\"-\")\nregion: Annotated[str, typer.Option(metavar=\"REGION\", help=\"Deployment region, e.g. eu-west-1.\")] = \"eu-west-1\"\n",[45,418,419,452,495],{"__ignoreMap":99},[103,420,421,424,427,429,431,433,436,438,440,442,445,447,449],{"class":105,"line":106},[103,422,423],{"class":113},"timeout: Annotated[",[103,425,426],{"class":130},"float",[103,428,232],{"class":113},[103,430,42],{"class":235},[103,432,239],{"class":120},[103,434,435],{"class":140},"\"SECONDS\"",[103,437,48],{"class":113},[103,439,236],{"class":235},[103,441,239],{"class":120},[103,443,444],{"class":140},"\"Time to wait per attempt.\"",[103,446,245],{"class":113},[103,448,239],{"class":120},[103,450,451],{"class":130}," 30.0\n",[103,453,454,457,460,462,465,467,469,471,474,476,478,480,483,485,487,490,493],{"class":105,"line":117},[103,455,456],{"class":113},"output: Annotated[Path, typer.Option(",[103,458,459],{"class":140},"\"--output\"",[103,461,48],{"class":113},[103,463,464],{"class":140},"\"-o\"",[103,466,48],{"class":113},[103,468,42],{"class":235},[103,470,239],{"class":120},[103,472,473],{"class":140},"\"PATH\"",[103,475,48],{"class":113},[103,477,236],{"class":235},[103,479,239],{"class":120},[103,481,482],{"class":140},"\"Write results here.\"",[103,484,245],{"class":113},[103,486,239],{"class":120},[103,488,489],{"class":113}," Path(",[103,491,492],{"class":140},"\"-\"",[103,494,364],{"class":113},[103,496,497,500,503,505,507,509,512,514,516,518,521,523,525],{"class":105,"line":137},[103,498,499],{"class":113},"region: Annotated[",[103,501,502],{"class":130},"str",[103,504,232],{"class":113},[103,506,42],{"class":235},[103,508,239],{"class":120},[103,510,511],{"class":140},"\"REGION\"",[103,513,48],{"class":113},[103,515,236],{"class":235},[103,517,239],{"class":120},[103,519,520],{"class":140},"\"Deployment region, e.g. eu-west-1.\"",[103,522,245],{"class":113},[103,524,239],{"class":120},[103,526,527],{"class":140}," \"eu-west-1\"\n",[10,529,530,531,534,535,538],{},"For a value from a fixed set, an ",[45,532,533],{},"Enum"," does better than any metavar — it produces\n",[45,536,537],{},"[keep|delete]"," in the help, restricts the input, and feeds shell completion:",[94,540,542],{"className":96,"code":541,"language":98,"meta":99,"style":99},"class SyncMode(str, Enum):\n    keep = \"keep\"\n    delete = \"delete\"\n\nmode: Annotated[SyncMode, typer.Option(case_sensitive=False, help=\"What to do about files missing locally.\")] = SyncMode.keep\n",[45,543,544,563,573,583,587],{"__ignoreMap":99},[103,545,546,549,552,554,556,558,560],{"class":105,"line":106},[103,547,548],{"class":120},"class",[103,550,551],{"class":109}," SyncMode",[103,553,354],{"class":113},[103,555,502],{"class":130},[103,557,48],{"class":113},[103,559,533],{"class":109},[103,561,562],{"class":113},"):\n",[103,564,565,568,570],{"class":105,"line":117},[103,566,567],{"class":113},"    keep ",[103,569,239],{"class":120},[103,571,572],{"class":140}," \"keep\"\n",[103,574,575,578,580],{"class":105,"line":137},[103,576,577],{"class":113},"    delete ",[103,579,239],{"class":120},[103,581,582],{"class":140}," \"delete\"\n",[103,584,585],{"class":105,"line":144},[103,586,148],{"emptyLinePlaceholder":147},[103,588,589,592,595,597,600,602,604,606,609,611,613],{"class":105,"line":151},[103,590,591],{"class":113},"mode: Annotated[SyncMode, typer.Option(",[103,593,594],{"class":235},"case_sensitive",[103,596,239],{"class":120},[103,598,599],{"class":130},"False",[103,601,48],{"class":113},[103,603,236],{"class":235},[103,605,239],{"class":120},[103,607,608],{"class":140},"\"What to do about files missing locally.\"",[103,610,245],{"class":113},[103,612,239],{"class":120},[103,614,615],{"class":113}," SyncMode.keep\n",[14,617,619],{"id":618},"ordering-and-grouping","Ordering and grouping",[10,621,622],{},"Readers scan top to bottom, so order matters more than it looks. Put the options someone is most\nlikely to need first, and keep the rarely used ones together at the bottom.",[10,624,625,626,629],{},"Click supports explicit option groups through third-party extensions, and Typer offers\n",[45,627,628],{},"rich_help_panel",", which is the simplest way to break a long list into sections:",[94,631,633],{"className":96,"code":632,"language":98,"meta":99,"style":99},"@app.command()\ndef sync(\n    source: Annotated[Path, typer.Argument(help=\"Directory to upload.\")],\n    dry_run: Annotated[bool, typer.Option(help=\"Report without uploading.\")] = False,\n    retries: Annotated[int, typer.Option(rich_help_panel=\"Tuning\", help=\"Attempts per file.\")] = 3,\n    timeout: Annotated[float, typer.Option(rich_help_panel=\"Tuning\", metavar=\"SECONDS\")] = 30.0,\n) -> None:\n    ...\n",[45,634,635,641,650,665,691,726,758,767],{"__ignoreMap":99},[103,636,637,639],{"class":105,"line":106},[103,638,110],{"class":109},[103,640,114],{"class":113},[103,642,643,645,647],{"class":105,"line":117},[103,644,121],{"class":120},[103,646,124],{"class":109},[103,648,649],{"class":113},"(\n",[103,651,652,655,657,659,662],{"class":105,"line":137},[103,653,654],{"class":113},"    source: Annotated[Path, typer.Argument(",[103,656,236],{"class":235},[103,658,239],{"class":120},[103,660,661],{"class":140},"\"Directory to upload.\"",[103,663,664],{"class":113},")],\n",[103,666,667,670,672,674,676,678,681,683,685,688],{"class":105,"line":144},[103,668,669],{"class":113},"    dry_run: Annotated[",[103,671,229],{"class":130},[103,673,232],{"class":113},[103,675,236],{"class":235},[103,677,239],{"class":120},[103,679,680],{"class":140},"\"Report without uploading.\"",[103,682,245],{"class":113},[103,684,239],{"class":120},[103,686,687],{"class":130}," False",[103,689,690],{"class":113},",\n",[103,692,693,696,699,701,703,705,708,710,712,714,717,719,721,724],{"class":105,"line":151},[103,694,695],{"class":113},"    retries: Annotated[",[103,697,698],{"class":130},"int",[103,700,232],{"class":113},[103,702,628],{"class":235},[103,704,239],{"class":120},[103,706,707],{"class":140},"\"Tuning\"",[103,709,48],{"class":113},[103,711,236],{"class":235},[103,713,239],{"class":120},[103,715,716],{"class":140},"\"Attempts per file.\"",[103,718,245],{"class":113},[103,720,239],{"class":120},[103,722,723],{"class":130}," 3",[103,725,690],{"class":113},[103,727,728,731,733,735,737,739,741,743,745,747,749,751,753,756],{"class":105,"line":157},[103,729,730],{"class":113},"    timeout: Annotated[",[103,732,426],{"class":130},[103,734,232],{"class":113},[103,736,628],{"class":235},[103,738,239],{"class":120},[103,740,707],{"class":140},[103,742,48],{"class":113},[103,744,42],{"class":235},[103,746,239],{"class":120},[103,748,435],{"class":140},[103,750,245],{"class":113},[103,752,239],{"class":120},[103,754,755],{"class":130}," 30.0",[103,757,690],{"class":113},[103,759,760,763,765],{"class":105,"line":163},[103,761,762],{"class":113},") -> ",[103,764,131],{"class":130},[103,766,134],{"class":113},[103,768,770],{"class":105,"line":769},8,[103,771,383],{"class":130},[10,773,774],{},"Two panels — the common options and \"Tuning\" — turn a wall of twelve flags into something a reader\ncan triage. If you find yourself wanting four panels, the command may be doing too much.",[14,776,778],{"id":777},"ux-considerations","UX considerations",[200,780],{"name":781},"help-width-decision",[10,783,784,787],{},[26,785,786],{},"Width."," Clamp to about 100 columns. A help screen stretched across a 200-column window forces\nthe reader's eye to travel much further than it needs to, and the effect is that people stop\nreading half way along each line.",[10,789,790,793,794,797],{},[26,791,792],{},"Colour."," Typer's Rich-formatted help is pleasant interactively and should disappear when output\nis redirected — which it does automatically, along with honouring ",[45,795,796],{},"NO_COLOR",". What is worth checking\nis that your own epilog does not contain hand-written escape codes that ignore those rules.",[10,799,800,803],{},[26,801,802],{},"Terminology."," Use the words your users use, not your internals'. If your code calls it a\n\"reconciliation pass\" and users call it a sync, the help text says sync. The internal vocabulary\nbelongs in the code, where it helps, rather than in the interface, where it is a translation task\nimposed on the reader.",[10,805,806,809],{},[26,807,808],{},"Length discipline."," A good test: read the option list aloud. Anything that takes more than a\nbreath is too long, and anything you find yourself skipping is not carrying information.",[14,811,813],{"id":812},"testing-the-behaviour","Testing the behaviour",[10,815,816],{},"Two checks keep help output from developing gaps, and both are short:",[94,818,820],{"className":96,"code":819,"language":98,"meta":99,"style":99},"def test_every_parameter_has_help_text():\n    for command in app.registered_commands:\n        for param in command.params:\n            assert param.help, f\"{command.name}: --{param.name} is undocumented\"\n\ndef test_root_help_lists_every_command(cli):\n    result = cli.invoke(app, [\"--help\"])\n    assert result.exit_code == 0\n    for name in (\"sync\", \"db\", \"remote\", \"doctor\"):\n        assert name in result.stdout\n",[45,821,822,832,846,859,895,899,909,924,938,971],{"__ignoreMap":99},[103,823,824,826,829],{"class":105,"line":106},[103,825,121],{"class":120},[103,827,828],{"class":109}," test_every_parameter_has_help_text",[103,830,831],{"class":113},"():\n",[103,833,834,837,840,843],{"class":105,"line":117},[103,835,836],{"class":120},"    for",[103,838,839],{"class":113}," command ",[103,841,842],{"class":120},"in",[103,844,845],{"class":113}," app.registered_commands:\n",[103,847,848,851,854,856],{"class":105,"line":137},[103,849,850],{"class":120},"        for",[103,852,853],{"class":113}," param ",[103,855,842],{"class":120},[103,857,858],{"class":113}," command.params:\n",[103,860,861,864,867,870,873,876,879,882,885,887,890,892],{"class":105,"line":144},[103,862,863],{"class":120},"            assert",[103,865,866],{"class":113}," param.help, ",[103,868,869],{"class":120},"f",[103,871,872],{"class":140},"\"",[103,874,875],{"class":130},"{",[103,877,878],{"class":113},"command.name",[103,880,881],{"class":130},"}",[103,883,884],{"class":140},": --",[103,886,875],{"class":130},[103,888,889],{"class":113},"param.name",[103,891,881],{"class":130},[103,893,894],{"class":140}," is undocumented\"\n",[103,896,897],{"class":105,"line":151},[103,898,148],{"emptyLinePlaceholder":147},[103,900,901,903,906],{"class":105,"line":157},[103,902,121],{"class":120},[103,904,905],{"class":109}," test_root_help_lists_every_command",[103,907,908],{"class":113},"(cli):\n",[103,910,911,914,916,919,921],{"class":105,"line":163},[103,912,913],{"class":113},"    result ",[103,915,239],{"class":120},[103,917,918],{"class":113}," cli.invoke(app, [",[103,920,328],{"class":140},[103,922,923],{"class":113},"])\n",[103,925,926,929,932,935],{"class":105,"line":769},[103,927,928],{"class":120},"    assert",[103,930,931],{"class":113}," result.exit_code ",[103,933,934],{"class":120},"==",[103,936,937],{"class":130}," 0\n",[103,939,941,943,946,948,951,954,956,959,961,964,966,969],{"class":105,"line":940},9,[103,942,836],{"class":120},[103,944,945],{"class":113}," name ",[103,947,842],{"class":120},[103,949,950],{"class":113}," (",[103,952,953],{"class":140},"\"sync\"",[103,955,48],{"class":113},[103,957,958],{"class":140},"\"db\"",[103,960,48],{"class":113},[103,962,963],{"class":140},"\"remote\"",[103,965,48],{"class":113},[103,967,968],{"class":140},"\"doctor\"",[103,970,562],{"class":113},[103,972,974,977,979,981],{"class":105,"line":973},10,[103,975,976],{"class":120},"        assert",[103,978,945],{"class":113},[103,980,842],{"class":120},[103,982,983],{"class":113}," result.stdout\n",[10,985,986],{},"The first is the one that pays off. It catches the option added in a hurry, it runs in\nmilliseconds, and it makes \"document your flag\" a build requirement rather than a review comment.",[10,988,989,990,55],{},"For the wording itself, a snapshot test over the root help screen turns any change into a\nreviewable diff — see\n",[991,992,994],"a",{"href":993},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F","snapshot testing CLI output",[14,996,998],{"id":997},"conclusion","Conclusion",[10,1000,1001],{},"Help text is written once and read thousands of times. A verb-first summary, one informative line\nper option, a metavar that names the value, visible defaults and a sensible width cost perhaps\ntwenty minutes across a whole tool — and they are the difference between a CLI that feels\nconsidered and one that feels like a wrapper somebody left running.",[14,1003,1005],{"id":1004},"frequently-asked-questions","Frequently asked questions",[1007,1008,1010],"h3",{"id":1009},"how-much-detail-belongs-in-help-versus-documentation","How much detail belongs in help versus documentation?",[10,1012,1013],{},"Help answers \"what can I type here\"; documentation answers \"why would I\". Keep each option to one\nline and put conceptual material — how the sync algorithm decides what changed, how configuration\nlayers — on a page you link to from the epilog.",[1007,1015,1017],{"id":1016},"should-the-docstring-be-written-for-users-or-developers","Should the docstring be written for users or developers?",[10,1019,1020],{},"Users, because Click and Typer publish it. If a command genuinely needs implementation notes, put\nthem in a comment rather than a docstring; the comment is for the next maintainer, and the docstring\nis part of the interface.",[1007,1022,1024,1025,1027],{"id":1023},"does-h-conflict-with-anything","Does ",[45,1026,61],{}," conflict with anything?",[10,1029,1030,1031,1033,1034,1037],{},"Only if you have already used ",[45,1032,61],{}," for something else, which is worth avoiding — it is one of the\nfew short flags people expect to mean a specific thing. Add it with ",[45,1035,1036],{},"help_option_names"," and leave\nit alone.",[1007,1039,1041],{"id":1040},"how-do-i-document-an-option-that-only-applies-to-some-subcommands","How do I document an option that only applies to some subcommands?",[10,1043,1044],{},"Declare it on those subcommands rather than globally. An option on the root callback appears on\nevery help screen beneath it, which is misleading if only one command honours it — and confusing\nbehaviour when a user passes it to a command that ignores it.",[1007,1046,1048],{"id":1047},"should-required-options-be-marked-in-help","Should required options be marked in help?",[10,1050,1051,1052,1055,1056,1059],{},"Both frameworks mark required ",[207,1053,1054],{},"arguments"," automatically. For a required ",[207,1057,1058],{},"option",", consider whether\nit should be an argument instead: a value the command cannot run without is usually positional. If\nit must be an option, say so in the help text as well, since scanners miss the marker.",[1007,1061,1063],{"id":1062},"how-do-i-handle-help-text-for-an-option-with-a-computed-default","How do I handle help text for an option with a computed default?",[10,1065,1066,1067,1070,1071,1074],{},"Say what the rule is rather than showing a value that will be wrong on someone else's machine:\n\"Defaults to the number of CPUs\" or \"Defaults to $XDG_CONFIG_HOME\u002Fmytool\u002Fconfig.toml\". Both\nframeworks accept a ",[45,1068,1069],{},"show_default"," string for exactly this, so the help line reads\n",[45,1072,1073],{},"[default: (number of CPUs)]"," instead of a number that happens to be true on the machine that\nbuilt the docs.",[1007,1076,1078],{"id":1077},"should-the-help-text-mention-the-environment-variable-equivalent","Should the help text mention the environment variable equivalent?",[10,1080,1081,1082,1085,1086,1089],{},"Yes, when one exists. A line ending \"(env: MYTOOL_RETRIES)\" is short and saves a search through\ndocumentation. Click adds it automatically when you use ",[45,1083,1084],{},"envvar="," with ",[45,1087,1088],{},"show_envvar=True",", which\nis the least effort for the most benefit.",[14,1091,1093],{"id":1092},"related","Related",[19,1095,1096,1103,1110,1116,1123],{},[22,1097,1098,1099],{},"Up: ",[991,1100,1102],{"href":1101},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002F","CLI help output and documentation",[22,1104,1105,1106],{},"Sideways: ",[991,1107,1109],{"href":1108},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output\u002F","Adding examples and epilogs to help output",[22,1111,1105,1112],{},[991,1113,1115],{"href":1114},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli\u002F","Generating man pages and docs from a CLI",[22,1117,1118,1119],{},"Related: ",[991,1120,1122],{"href":1121},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002F","Advanced argument validation strategies",[22,1124,1118,1125],{},[991,1126,1128],{"href":1127},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Typer vs Click: when to use each",[1130,1131,1132],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":99,"searchDepth":117,"depth":117,"links":1134},[1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1154],{"id":16,"depth":117,"text":17},{"id":72,"depth":117,"text":73},{"id":91,"depth":117,"text":92},{"id":197,"depth":117,"text":198},{"id":392,"depth":117,"text":393},{"id":618,"depth":117,"text":619},{"id":777,"depth":117,"text":778},{"id":812,"depth":117,"text":813},{"id":997,"depth":117,"text":998},{"id":1004,"depth":117,"text":1005,"children":1145},[1146,1147,1148,1150,1151,1152,1153],{"id":1009,"depth":137,"text":1010},{"id":1016,"depth":137,"text":1017},{"id":1023,"depth":137,"text":1149},"Does -h conflict with anything?",{"id":1040,"depth":137,"text":1041},{"id":1047,"depth":137,"text":1048},{"id":1062,"depth":137,"text":1063},{"id":1077,"depth":137,"text":1078},{"id":1092,"depth":117,"text":1093},"2026-08-01","Write CLI help text people can scan - summaries, one-line option help, metavars, defaults, and the wording that survives a hurried reader.","beginner",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read",{"title":5,"description":1156},"advanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002Findex",[236,1165,1166,1167,1168],"documentation","ux","typer","click","Lgr3zee4KPJwneSrQfD9iqnzjoy-F7RxuQgBFhWDiTg",[1171,1174,1177,1180,1183,1186,1189,1192,1195,1196,1199,1202,1205,1208,1211,1214,1217,1220,1223,1226,1229,1232,1235,1238,1241,1244,1247,1250,1253,1256,1259,1262,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295,1298,1301,1304,1307,1310,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1370,1373,1376,1379,1382,1385,1388,1391,1394,1397,1400,1403,1406,1409,1412,1415],{"path":1172,"title":1173},"\u002Fabout","About Python CLI Toolcraft",{"path":1175,"title":1176},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1178,"title":1179},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1181,"title":1182},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1184,"title":1185},"\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":1187,"title":1188},"\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":1190,"title":1191},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1193,"title":1194},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1161,"title":5},{"path":1197,"title":1198},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1200,"title":1201},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1203,"title":1204},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1206,"title":1207},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1209,"title":1210},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1212,"title":1213},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1215,"title":1216},"\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":1218,"title":1219},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1221,"title":1222},"\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":1224,"title":1225},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1227,"title":1228},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1230,"title":1231},"\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":1233,"title":1234},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1236,"title":1237},"\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":1239,"title":1240},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1242,"title":1243},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1245,"title":1246},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1248,"title":1249},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1251,"title":1252},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1254,"title":1255},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1257,"title":1258},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1260,"title":1261},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1263,"title":1264},"\u002F","Python CLI Toolcraft",{"path":1266,"title":1267},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1269,"title":1270},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1272,"title":1273},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1275,"title":1276},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1278,"title":1279},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1281,"title":1282},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1284,"title":1285},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1287,"title":1288},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1290,"title":1291},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1293,"title":1294},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1296,"title":1297},"\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":1299,"title":1300},"\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":1302,"title":1303},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1305,"title":1306},"\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":1308,"title":1309},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1311,"title":1312},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1314,"title":1315},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1317,"title":1318},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1320,"title":1321},"\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":1323,"title":1324},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1326,"title":1327},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1329,"title":1330},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1332,"title":1333},"\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":1335,"title":1336},"\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":1338,"title":1339},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1341,"title":1342},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1344,"title":1345},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1347,"title":1348},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1350,"title":1351},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1353,"title":1354},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1356,"title":1357},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1359,"title":1360},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1362,"title":1363},"\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":1365,"title":1366},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1368,"title":1369},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1371,"title":1372},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1374,"title":1375},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1377,"title":1378},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1380,"title":1381},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1383,"title":1384},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1386,"title":1387},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1389,"title":1390},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1392,"title":1393},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1395,"title":1396},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1398,"title":1399},"\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":1401,"title":1402},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1404,"title":1405},"\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":1407,"title":1408},"\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":1410,"title":1411},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1413,"title":1414},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1416,"title":1417},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690027]