[{"data":1,"prerenderedAt":1693},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002F":3,"content-directory":1445},{"id":4,"title":5,"body":6,"date":1430,"description":1431,"difficulty":1432,"draft":1433,"extension":1434,"meta":1435,"navigation":177,"path":1436,"seo":1437,"stem":1438,"tags":1439,"updated":1430,"__hash__":1444},"content\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Findex.md","CLI Help Output and Documentation",{"type":7,"value":8,"toc":1404},"minimark",[9,13,18,53,57,61,64,67,70,73,123,126,130,133,207,214,254,257,261,264,271,279,288,292,295,385,392,399,403,406,436,446,454,457,461,464,472,476,487,490,524,604,614,621,625,628,631,637,640,676,682,689,693,696,709,715,725,731,737,740,744,747,918,926,930,933,939,954,957,1209,1212,1218,1221,1228,1234,1238,1267,1271,1288,1292,1297,1300,1304,1307,1311,1318,1322,1325,1329,1336,1340,1347,1351,1362,1366,1400],[10,11,12],"p",{},"Help output is the documentation that ships with your tool. It is read far more often than\nanything on a website, it cannot go stale relative to the code it describes, and it is where a\nuser decides whether your tool is worth learning. It also tends to be the least deliberately\nwritten text in the whole project.",[14,15,17],"h2",{"id":16},"tldr","TL;DR",[19,20,21,25,33,40,46],"ul",{},[22,23,24],"li",{},"One line per option: what it does, what it accepts, what the default is.",[22,26,27,28,32],{},"Keep the root help screen a ",[29,30,31],"strong",{},"map",", not a catalogue — group commands by subject area.",[22,34,35,36,39],{},"Put two to four ",[29,37,38],{},"real examples"," in an epilog; people copy rather than read.",[22,41,42,45],{},[29,43,44],{},"Generate"," reference docs and man pages from the command tree so they cannot drift.",[22,47,48,49,52],{},"Renaming a flag or changing a default is a ",[29,50,51],{},"breaking change",": alias, warn, then remove.",[54,55],"inline-diagram",{"name":56},"help-section-map",[14,58,60],{"id":59},"what-a-help-screen-is-made-of","What a help screen is made of",[54,62],{"name":63},"help-screen-anatomy",[10,65,66],{},"Three of the four parts are generated for you. The usage line comes from your parameter\ndeclarations, the option list from their help strings, and the summary from the first line of the\ndocstring. What you control is the words inside them — and the epilog, which is the only part you\nwrite from scratch.",[10,68,69],{},"The usage line follows conventions that predate every framework in this handbook, and following\nthem means users already know how to read it:",[10,71,72],{},"Where authors go wrong is the summary. It appears twice — on the command's own help screen and,\nmore importantly, next to the command name on its parent's screen — so it is the single most-read\nsentence in your tool:",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"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","python","",[81,82,83,96,116],"code",{"__ignoreMap":79},[84,85,88,92],"span",{"class":86,"line":87},"line",1,[84,89,91],{"class":90},"sScJk","@app.command",[84,93,95],{"class":94},"sVt8B","()\n",[84,97,99,103,106,109,113],{"class":86,"line":98},2,[84,100,102],{"class":101},"szBVR","def",[84,104,105],{"class":90}," sync",[84,107,108],{"class":94},"(source: Path) -> ",[84,110,112],{"class":111},"sj4cs","None",[84,114,115],{"class":94},":\n",[84,117,119],{"class":86,"line":118},3,[84,120,122],{"class":121},"sZZnC","    \"\"\"Upload changed files from SOURCE to the configured bucket.\"\"\"\n",[10,124,125],{},"That first line is the summary. Everything after a blank line becomes the longer description, which\nmeans a docstring written for developers (\"Implements the sync algorithm described in ADR-14\")\nbecomes user-facing text nobody can act on.",[14,127,129],{"id":128},"option-lines-that-carry-information","Option lines that carry information",[10,131,132],{},"A user scanning twelve options is reading twelve lines of about eight words each. The ones that\nsay nothing cost them attention they needed for the ones that do:",[74,134,136],{"className":76,"code":135,"language":78,"meta":79,"style":79},"# says nothing\nretries: Annotated[int, typer.Option(help=\"Sets the retries.\")] = 3\n\n# says something\nretries: Annotated[int, typer.Option(help=\"Attempts per file before giving up.\")] = 3\n",[81,137,138,144,173,179,185],{"__ignoreMap":79},[84,139,140],{"class":86,"line":87},[84,141,143],{"class":142},"sJ8bj","# says nothing\n",[84,145,146,149,152,155,159,162,165,168,170],{"class":86,"line":98},[84,147,148],{"class":94},"retries: Annotated[",[84,150,151],{"class":111},"int",[84,153,154],{"class":94},", typer.Option(",[84,156,158],{"class":157},"s4XuR","help",[84,160,161],{"class":101},"=",[84,163,164],{"class":121},"\"Sets the retries.\"",[84,166,167],{"class":94},")] ",[84,169,161],{"class":101},[84,171,172],{"class":111}," 3\n",[84,174,175],{"class":86,"line":118},[84,176,178],{"emptyLinePlaceholder":177},true,"\n",[84,180,182],{"class":86,"line":181},4,[84,183,184],{"class":142},"# says something\n",[84,186,188,190,192,194,196,198,201,203,205],{"class":86,"line":187},5,[84,189,148],{"class":94},[84,191,151],{"class":111},[84,193,154],{"class":94},[84,195,158],{"class":157},[84,197,161],{"class":101},[84,199,200],{"class":121},"\"Attempts per file before giving up.\"",[84,202,167],{"class":94},[84,204,161],{"class":101},[84,206,172],{"class":111},[10,208,209,210,213],{},"Add ",[81,211,212],{},"show_default=True"," (Click) or let Typer print it automatically, so the reader does not have to\nguess what happens when they omit the flag. For values with units, put the unit in the help text\nand, where it helps, in the metavar:",[74,215,217],{"className":76,"code":216,"language":78,"meta":79,"style":79},"timeout: Annotated[float, typer.Option(help=\"Seconds to wait per attempt.\", metavar=\"SECONDS\")] = 30.0\n",[81,218,219],{"__ignoreMap":79},[84,220,221,224,227,229,231,233,236,239,242,244,247,249,251],{"class":86,"line":87},[84,222,223],{"class":94},"timeout: Annotated[",[84,225,226],{"class":111},"float",[84,228,154],{"class":94},[84,230,158],{"class":157},[84,232,161],{"class":101},[84,234,235],{"class":121},"\"Seconds to wait per attempt.\"",[84,237,238],{"class":94},", ",[84,240,241],{"class":157},"metavar",[84,243,161],{"class":101},[84,245,246],{"class":121},"\"SECONDS\"",[84,248,167],{"class":94},[84,250,161],{"class":101},[84,252,253],{"class":111}," 30.0\n",[10,255,256],{},"Three more habits worth adopting. Start with a verb — \"Attempts per file\", \"Skip verification\",\n\"Write output to PATH\". Mention the partner flag when two interact (\"Ignored unless --watch is\nset\"). And keep each line to one screen width, because the framework will wrap it and a wrapped\noption line is much harder to scan.",[14,258,260],{"id":259},"the-root-screen-is-a-map","The root screen is a map",[54,262],{"name":263},"help-hierarchy-flow",[10,265,266,267,270],{},"A root ",[81,268,269],{},"--help"," that lists thirty verbs is a catalogue. A root that lists five subject areas is a\nmap, and the difference decides whether a new user finds anything.",[74,272,277],{"className":273,"code":275,"language":276},[274],"language-text","Usage: mytool [OPTIONS] COMMAND [ARGS]...\n\n  Manage deployments and the data behind them.\n\nCommands:\n  sync     Upload changed files to the configured bucket.\n  db       Database maintenance: migrate, seed, dump.\n  remote   Manage remote endpoints.\n  doctor   Print environment and configuration diagnostics.\n","text",[81,278,275],{"__ignoreMap":79},[10,280,281,282,287],{},"Four entries, each with a summary that says what lives underneath. When the list stops fitting on\na screen, that is the signal to add a level rather than another top-level verb — the same rule as\nin ",[283,284,286],"a",{"href":285},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command CLIs",".",[14,289,291],{"id":290},"examples-belong-in-the-tool","Examples belong in the tool",[10,293,294],{},"The question a help screen most often fails to answer is \"what do I actually type\". An epilog with\ntwo to four real invocations answers it:",[74,296,298],{"className":76,"code":297,"language":78,"meta":79,"style":79},"app = typer.Typer(\n    help=\"Manage deployments and the data behind them.\",\n    epilog=(\n        \"Examples:\\n\"\n        \"  mytool sync .\u002Fdata --retries 5\\n\"\n        \"  mytool sync .\u002Fdata --dry-run | tee plan.txt\\n\"\n        \"  mytool db migrate --yes            # non-interactive, for CI\\n\"\n    ),\n)\n",[81,299,300,310,323,333,344,353,363,373,379],{"__ignoreMap":79},[84,301,302,305,307],{"class":86,"line":87},[84,303,304],{"class":94},"app ",[84,306,161],{"class":101},[84,308,309],{"class":94}," typer.Typer(\n",[84,311,312,315,317,320],{"class":86,"line":98},[84,313,314],{"class":157},"    help",[84,316,161],{"class":101},[84,318,319],{"class":121},"\"Manage deployments and the data behind them.\"",[84,321,322],{"class":94},",\n",[84,324,325,328,330],{"class":86,"line":118},[84,326,327],{"class":157},"    epilog",[84,329,161],{"class":101},[84,331,332],{"class":94},"(\n",[84,334,335,338,341],{"class":86,"line":181},[84,336,337],{"class":121},"        \"Examples:",[84,339,340],{"class":111},"\\n",[84,342,343],{"class":121},"\"\n",[84,345,346,349,351],{"class":86,"line":187},[84,347,348],{"class":121},"        \"  mytool sync .\u002Fdata --retries 5",[84,350,340],{"class":111},[84,352,343],{"class":121},[84,354,356,359,361],{"class":86,"line":355},6,[84,357,358],{"class":121},"        \"  mytool sync .\u002Fdata --dry-run | tee plan.txt",[84,360,340],{"class":111},[84,362,343],{"class":121},[84,364,366,369,371],{"class":86,"line":365},7,[84,367,368],{"class":121},"        \"  mytool db migrate --yes            # non-interactive, for CI",[84,370,340],{"class":111},[84,372,343],{"class":121},[84,374,376],{"class":86,"line":375},8,[84,377,378],{"class":94},"    ),\n",[84,380,382],{"class":86,"line":381},9,[84,383,384],{"class":94},")\n",[10,386,387,388,391],{},"For argparse, the same thing needs ",[81,389,390],{},"formatter_class=argparse.RawDescriptionHelpFormatter",", or the\nnewlines are collapsed and the examples become a paragraph.",[10,393,394,395,398],{},"Choose examples that teach a habit, not just a syntax: the piped form shows that stdout is data,\nand the ",[81,396,397],{},"--yes"," form shows the automation path exists. And make sure they work — an example that\nerrors is worse than none, which is a good argument for running them in a test.",[14,400,402],{"id":401},"documentation-generated-from-the-tree","Documentation generated from the tree",[10,404,405],{},"Reference documentation retyped into a README drifts within one release. Generating it from the\ncommand definitions makes drift impossible:",[74,407,411],{"className":408,"code":409,"language":410,"meta":79,"style":79},"language-bash shiki shiki-themes github-light github-dark","typer mytool.cli utils docs --output docs\u002Freference.md    # Typer's own generator\n","bash",[81,412,413],{"__ignoreMap":79},[84,414,415,418,421,424,427,430,433],{"class":86,"line":87},[84,416,417],{"class":90},"typer",[84,419,420],{"class":121}," mytool.cli",[84,422,423],{"class":121}," utils",[84,425,426],{"class":121}," docs",[84,428,429],{"class":111}," --output",[84,431,432],{"class":121}," docs\u002Freference.md",[84,434,435],{"class":142},"    # Typer's own generator\n",[10,437,438,441,442,445],{},[81,439,440],{},"click-man"," and ",[81,443,444],{},"sphinx-click"," do the equivalent for Click, producing man pages and Sphinx pages\nrespectively. Whichever you use, run it in CI and fail the build when the generated output differs\nfrom what is committed — that turns \"the docs are out of date\" into a failing check rather than a\ndiscovery.",[10,447,448,449,453],{},"What generation cannot produce is the ",[450,451,452],"em",{},"why",": when to use a command, what the trade-offs are, how it\nfits a workflow. Keep those hand-written and keep the reference generated, and neither has to\npretend to be the other.",[54,455],{"name":456},"docs-drift-checklist",[14,458,460],{"id":459},"changing-the-interface-later","Changing the interface later",[10,462,463],{},"Flags are a public API. The changes people underestimate are the ones that leave the name intact —\nchanging a default, or changing what a flag means — because nothing in a user's script looks\ndifferent and the behaviour has moved underneath them.",[10,465,466,467,471],{},"The mechanics are cheap: accept the old name as a hidden alias, warn on stderr naming the\nreplacement and the removal version, and remove it at a major release. The\n",[283,468,470],{"href":469},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags\u002F","deprecation guide","\ncovers the implementation and the changelog entries that go with it.",[14,473,475],{"id":474},"width-colour-and-the-other-terminal","Width, colour and the other terminal",[10,477,478,479,482,483,486],{},"Help output is read in three places — an interactive terminal, a pipe into ",[81,480,481],{},"less"," or ",[81,484,485],{},"grep",", and a\nCI log — and it should look sensible in all three.",[10,488,489],{},"Both frameworks size output to the terminal and fall back to a fixed width when there is none.\nThe one thing worth overriding is the upper bound: a help screen stretched across a 200-column\nwindow is much harder to read than one clamped to around 100.",[74,491,493],{"className":76,"code":492,"language":78,"meta":79,"style":79},"app = typer.Typer(context_settings={\"max_content_width\": 100})\n",[81,494,495],{"__ignoreMap":79},[84,496,497,499,501,504,507,509,512,515,518,521],{"class":86,"line":87},[84,498,304],{"class":94},[84,500,161],{"class":101},[84,502,503],{"class":94}," typer.Typer(",[84,505,506],{"class":157},"context_settings",[84,508,161],{"class":101},[84,510,511],{"class":94},"{",[84,513,514],{"class":121},"\"max_content_width\"",[84,516,517],{"class":94},": ",[84,519,520],{"class":111},"100",[84,522,523],{"class":94},"})\n",[74,525,527],{"className":76,"code":526,"language":78,"meta":79,"style":79},"CONTEXT_SETTINGS = {\"max_content_width\": 100, \"help_option_names\": [\"-h\", \"--help\"]}\n\n@click.group(context_settings=CONTEXT_SETTINGS)\ndef cli() -> None:\n    ...\n",[81,528,529,565,569,585,599],{"__ignoreMap":79},[84,530,531,534,537,540,542,544,546,548,551,554,557,559,562],{"class":86,"line":87},[84,532,533],{"class":111},"CONTEXT_SETTINGS",[84,535,536],{"class":101}," =",[84,538,539],{"class":94}," {",[84,541,514],{"class":121},[84,543,517],{"class":94},[84,545,520],{"class":111},[84,547,238],{"class":94},[84,549,550],{"class":121},"\"help_option_names\"",[84,552,553],{"class":94},": [",[84,555,556],{"class":121},"\"-h\"",[84,558,238],{"class":94},[84,560,561],{"class":121},"\"--help\"",[84,563,564],{"class":94},"]}\n",[84,566,567],{"class":86,"line":98},[84,568,178],{"emptyLinePlaceholder":177},[84,570,571,574,577,579,581,583],{"class":86,"line":118},[84,572,573],{"class":90},"@click.group",[84,575,576],{"class":94},"(",[84,578,506],{"class":157},[84,580,161],{"class":101},[84,582,533],{"class":111},[84,584,384],{"class":94},[84,586,587,589,592,595,597],{"class":86,"line":181},[84,588,102],{"class":101},[84,590,591],{"class":90}," cli",[84,593,594],{"class":94},"() -> ",[84,596,112],{"class":111},[84,598,115],{"class":94},[84,600,601],{"class":86,"line":187},[84,602,603],{"class":111},"    ...\n",[10,605,606,609,610,613],{},[81,607,608],{},"help_option_names"," is worth setting while you are there. ",[81,611,612],{},"-h"," is what most people type first, and\nneither framework enables it by default.",[10,615,616,617,620],{},"Colour follows the same rule as the rest of your output: on for a terminal, off for anything else,\nand off when ",[81,618,619],{},"NO_COLOR"," is set. Both frameworks handle the detection; what they cannot know is\nwhether your epilog's ANSI art is essential, which is an argument for not having any.",[14,622,624],{"id":623},"errors-are-help-output-too","Errors are help output too",[10,626,627],{},"The text a user sees when they get an invocation wrong belongs in the same category, and it is\nread more carefully than the help screen — because at that moment they are stuck.",[10,629,630],{},"Both frameworks produce a decent default:",[74,632,635],{"className":633,"code":634,"language":276},[274],"Usage: mytool sync [OPTIONS] SOURCE\nTry 'mytool sync --help' for help.\n\nError: Invalid value for '--retries': 'x' is not a valid integer.\n",[81,636,634],{"__ignoreMap":79},[10,638,639],{},"Three things make that work: the usage line reminds them of the shape, the hint names the command\nto get more, and the error names the offending option and value. When you write your own errors,\nkeep the same three ingredients:",[74,641,643],{"className":76,"code":642,"language":78,"meta":79,"style":79},"raise typer.BadParameter(\n    \"expected a duration like 30s, 5m or 2h\",\n    param_hint=\"--timeout\",\n)\n",[81,644,645,653,660,672],{"__ignoreMap":79},[84,646,647,650],{"class":86,"line":87},[84,648,649],{"class":101},"raise",[84,651,652],{"class":94}," typer.BadParameter(\n",[84,654,655,658],{"class":86,"line":98},[84,656,657],{"class":121},"    \"expected a duration like 30s, 5m or 2h\"",[84,659,322],{"class":94},[84,661,662,665,667,670],{"class":86,"line":118},[84,663,664],{"class":157},"    param_hint",[84,666,161],{"class":101},[84,668,669],{"class":121},"\"--timeout\"",[84,671,322],{"class":94},[84,673,674],{"class":86,"line":181},[84,675,384],{"class":94},[10,677,678,681],{},[81,679,680],{},"param_hint"," is what puts the flag name in the message. Without it the user gets a sentence with\nno indication of which of their twelve arguments was wrong.",[10,683,684,685,287],{},"For failures that are not usage errors — a missing file, an unreachable service — the equivalent\ncourtesy is naming the input and suggesting the next step, which is covered in\n",[283,686,688],{"href":687},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages",[14,690,692],{"id":691},"documenting-the-parts-that-are-not-flags","Documenting the parts that are not flags",[10,694,695],{},"A complete CLI has three surfaces beyond its options, and all three are routinely undocumented.",[10,697,698,701,702,705,706,708],{},[29,699,700],{},"Environment variables."," If your tool reads ",[81,703,704],{},"MYTOOL_TOKEN"," or honours ",[81,707,619],{},", say so. The\nnatural place is the root epilog and the man page's ENVIRONMENT section:",[74,710,713],{"className":711,"code":712,"language":276},[274],"Environment:\n  MYTOOL_TOKEN     API credential; required for remote commands.\n  MYTOOL_CONFIG    Path to a config file, equivalent to --config.\n  NO_COLOR         Disable colour output entirely.\n",[81,714,712],{"__ignoreMap":79},[10,716,717,720,721,724],{},[29,718,719],{},"Configuration files."," Users need to know which paths are searched and in what order, because\n\"the tool is ignoring my setting\" is almost always a question about search order. Documenting the\nprecedence chain — and offering a ",[81,722,723],{},"config show --origin"," command — removes most of those questions.",[10,726,727,730],{},[29,728,729],{},"Exit codes."," Anyone scripting your tool needs the list, and a tool that documents its codes is\none people are willing to branch on:",[74,732,735],{"className":733,"code":734,"language":276},[274],"Exit codes:\n  0   success\n  1   the operation failed\n  2   the command line was invalid\n  65  input data was malformed\n  78  configuration was invalid\n  130 cancelled by the user\n",[81,736,734],{"__ignoreMap":79},[10,738,739],{},"All three are cheap to write once and expensive to leave out, because each one generates a\nrecurring class of question that nothing in the option list answers.",[14,741,743],{"id":742},"keeping-help-honest-in-ci","Keeping help honest in CI",[10,745,746],{},"Two small checks stop help output regressing, and both are worth having before the tool has many\nusers.",[74,748,750],{"className":76,"code":749,"language":78,"meta":79,"style":79},"def test_root_help_lists_every_command(cli):\n    result = cli.invoke(app, [\"--help\"])\n\n    assert result.exit_code == 0\n    for name in (\"sync\", \"db\", \"remote\", \"doctor\"):\n        assert name in result.stdout\n\ndef test_every_option_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} has no help text\"\n",[81,751,752,762,777,781,795,830,842,846,856,868,882],{"__ignoreMap":79},[84,753,754,756,759],{"class":86,"line":87},[84,755,102],{"class":101},[84,757,758],{"class":90}," test_root_help_lists_every_command",[84,760,761],{"class":94},"(cli):\n",[84,763,764,767,769,772,774],{"class":86,"line":98},[84,765,766],{"class":94},"    result ",[84,768,161],{"class":101},[84,770,771],{"class":94}," cli.invoke(app, [",[84,773,561],{"class":121},[84,775,776],{"class":94},"])\n",[84,778,779],{"class":86,"line":118},[84,780,178],{"emptyLinePlaceholder":177},[84,782,783,786,789,792],{"class":86,"line":181},[84,784,785],{"class":101},"    assert",[84,787,788],{"class":94}," result.exit_code ",[84,790,791],{"class":101},"==",[84,793,794],{"class":111}," 0\n",[84,796,797,800,803,806,809,812,814,817,819,822,824,827],{"class":86,"line":187},[84,798,799],{"class":101},"    for",[84,801,802],{"class":94}," name ",[84,804,805],{"class":101},"in",[84,807,808],{"class":94}," (",[84,810,811],{"class":121},"\"sync\"",[84,813,238],{"class":94},[84,815,816],{"class":121},"\"db\"",[84,818,238],{"class":94},[84,820,821],{"class":121},"\"remote\"",[84,823,238],{"class":94},[84,825,826],{"class":121},"\"doctor\"",[84,828,829],{"class":94},"):\n",[84,831,832,835,837,839],{"class":86,"line":355},[84,833,834],{"class":101},"        assert",[84,836,802],{"class":94},[84,838,805],{"class":101},[84,840,841],{"class":94}," result.stdout\n",[84,843,844],{"class":86,"line":365},[84,845,178],{"emptyLinePlaceholder":177},[84,847,848,850,853],{"class":86,"line":375},[84,849,102],{"class":101},[84,851,852],{"class":90}," test_every_option_has_help_text",[84,854,855],{"class":94},"():\n",[84,857,858,860,863,865],{"class":86,"line":381},[84,859,799],{"class":101},[84,861,862],{"class":94}," command ",[84,864,805],{"class":101},[84,866,867],{"class":94}," app.registered_commands:\n",[84,869,871,874,877,879],{"class":86,"line":870},10,[84,872,873],{"class":101},"        for",[84,875,876],{"class":94}," param ",[84,878,805],{"class":101},[84,880,881],{"class":94}," command.params:\n",[84,883,885,888,891,894,897,899,902,905,908,910,913,915],{"class":86,"line":884},11,[84,886,887],{"class":101},"            assert",[84,889,890],{"class":94}," param.help, ",[84,892,893],{"class":101},"f",[84,895,896],{"class":121},"\"",[84,898,511],{"class":111},[84,900,901],{"class":94},"command.name",[84,903,904],{"class":111},"}",[84,906,907],{"class":121},": --",[84,909,511],{"class":111},[84,911,912],{"class":94},"param.name",[84,914,904],{"class":111},[84,916,917],{"class":121}," has no help text\"\n",[10,919,920,921,925],{},"The second one is the useful one. It is three lines, it catches the option someone added in a hurry,\nand it is the difference between a help screen that stays complete and one that quietly develops\ngaps. Pair it with a snapshot of the root help screen — see\n",[283,922,924],{"href":923},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output\u002F","snapshot testing CLI output"," —\nand any change to what users read shows up as a reviewable diff.",[14,927,929],{"id":928},"a-worked-rewrite","A worked rewrite",[10,931,932],{},"The fastest way to see the difference is to take a real help screen and fix it. Here is one that a\ntool might ship without thinking about:",[74,934,937],{"className":935,"code":936,"language":276},[274],"Usage: mytool sync [OPTIONS] SRC\n\n  Sync command.\n\nOptions:\n  --retries INTEGER\n  --timeout FLOAT\n  --mode TEXT\n  --force\n  --help              Show this message and exit.\n",[81,938,936],{"__ignoreMap":79},[10,940,941,942,945,946,949,950,953],{},"Six problems, all of them common. The summary says nothing. No option documents its default. ",[81,943,944],{},"SRC","\nis an abbreviation the user has to guess at. ",[81,947,948],{},"--mode"," accepts \"TEXT\", which could mean anything.\n",[81,951,952],{},"--force"," gives no hint of what it overrides. And there are no examples.",[10,955,956],{},"The declarations that fix it:",[74,958,960],{"className":76,"code":959,"language":78,"meta":79,"style":79},"@app.command()\ndef sync(\n    source: Annotated[Path, typer.Argument(\n        exists=True, file_okay=False, metavar=\"SOURCE\",\n        help=\"Directory to upload.\",\n    )],\n    retries: Annotated[int, typer.Option(\n        min=1, max=10, help=\"Attempts per file before giving up.\",\n    )] = 3,\n    timeout: Annotated[float, typer.Option(\n        metavar=\"SECONDS\", help=\"Seconds to wait for each upload.\",\n    )] = 30.0,\n    mode: Annotated[SyncMode, typer.Option(\n        case_sensitive=False, help=\"What to do about files missing locally.\",\n    )] = SyncMode.keep,\n    force: Annotated[bool, typer.Option(\n        help=\"Upload even when the remote copy looks newer.\",\n    )] = False,\n) -> None:\n    \"\"\"Upload changed files from SOURCE to the configured bucket.\"\"\"\n",[81,961,962,968,976,981,1012,1024,1029,1039,1069,1081,1090,1110,1122,1128,1149,1159,1170,1182,1194,1204],{"__ignoreMap":79},[84,963,964,966],{"class":86,"line":87},[84,965,91],{"class":90},[84,967,95],{"class":94},[84,969,970,972,974],{"class":86,"line":98},[84,971,102],{"class":101},[84,973,105],{"class":90},[84,975,332],{"class":94},[84,977,978],{"class":86,"line":118},[84,979,980],{"class":94},"    source: Annotated[Path, typer.Argument(\n",[84,982,983,986,988,991,993,996,998,1001,1003,1005,1007,1010],{"class":86,"line":181},[84,984,985],{"class":157},"        exists",[84,987,161],{"class":101},[84,989,990],{"class":111},"True",[84,992,238],{"class":94},[84,994,995],{"class":157},"file_okay",[84,997,161],{"class":101},[84,999,1000],{"class":111},"False",[84,1002,238],{"class":94},[84,1004,241],{"class":157},[84,1006,161],{"class":101},[84,1008,1009],{"class":121},"\"SOURCE\"",[84,1011,322],{"class":94},[84,1013,1014,1017,1019,1022],{"class":86,"line":187},[84,1015,1016],{"class":157},"        help",[84,1018,161],{"class":101},[84,1020,1021],{"class":121},"\"Directory to upload.\"",[84,1023,322],{"class":94},[84,1025,1026],{"class":86,"line":355},[84,1027,1028],{"class":94},"    )],\n",[84,1030,1031,1034,1036],{"class":86,"line":365},[84,1032,1033],{"class":94},"    retries: Annotated[",[84,1035,151],{"class":111},[84,1037,1038],{"class":94},", typer.Option(\n",[84,1040,1041,1044,1046,1049,1051,1054,1056,1059,1061,1063,1065,1067],{"class":86,"line":375},[84,1042,1043],{"class":157},"        min",[84,1045,161],{"class":101},[84,1047,1048],{"class":111},"1",[84,1050,238],{"class":94},[84,1052,1053],{"class":157},"max",[84,1055,161],{"class":101},[84,1057,1058],{"class":111},"10",[84,1060,238],{"class":94},[84,1062,158],{"class":157},[84,1064,161],{"class":101},[84,1066,200],{"class":121},[84,1068,322],{"class":94},[84,1070,1071,1074,1076,1079],{"class":86,"line":381},[84,1072,1073],{"class":94},"    )] ",[84,1075,161],{"class":101},[84,1077,1078],{"class":111}," 3",[84,1080,322],{"class":94},[84,1082,1083,1086,1088],{"class":86,"line":870},[84,1084,1085],{"class":94},"    timeout: Annotated[",[84,1087,226],{"class":111},[84,1089,1038],{"class":94},[84,1091,1092,1095,1097,1099,1101,1103,1105,1108],{"class":86,"line":884},[84,1093,1094],{"class":157},"        metavar",[84,1096,161],{"class":101},[84,1098,246],{"class":121},[84,1100,238],{"class":94},[84,1102,158],{"class":157},[84,1104,161],{"class":101},[84,1106,1107],{"class":121},"\"Seconds to wait for each upload.\"",[84,1109,322],{"class":94},[84,1111,1113,1115,1117,1120],{"class":86,"line":1112},12,[84,1114,1073],{"class":94},[84,1116,161],{"class":101},[84,1118,1119],{"class":111}," 30.0",[84,1121,322],{"class":94},[84,1123,1125],{"class":86,"line":1124},13,[84,1126,1127],{"class":94},"    mode: Annotated[SyncMode, typer.Option(\n",[84,1129,1131,1134,1136,1138,1140,1142,1144,1147],{"class":86,"line":1130},14,[84,1132,1133],{"class":157},"        case_sensitive",[84,1135,161],{"class":101},[84,1137,1000],{"class":111},[84,1139,238],{"class":94},[84,1141,158],{"class":157},[84,1143,161],{"class":101},[84,1145,1146],{"class":121},"\"What to do about files missing locally.\"",[84,1148,322],{"class":94},[84,1150,1152,1154,1156],{"class":86,"line":1151},15,[84,1153,1073],{"class":94},[84,1155,161],{"class":101},[84,1157,1158],{"class":94}," SyncMode.keep,\n",[84,1160,1162,1165,1168],{"class":86,"line":1161},16,[84,1163,1164],{"class":94},"    force: Annotated[",[84,1166,1167],{"class":111},"bool",[84,1169,1038],{"class":94},[84,1171,1173,1175,1177,1180],{"class":86,"line":1172},17,[84,1174,1016],{"class":157},[84,1176,161],{"class":101},[84,1178,1179],{"class":121},"\"Upload even when the remote copy looks newer.\"",[84,1181,322],{"class":94},[84,1183,1185,1187,1189,1192],{"class":86,"line":1184},18,[84,1186,1073],{"class":94},[84,1188,161],{"class":101},[84,1190,1191],{"class":111}," False",[84,1193,322],{"class":94},[84,1195,1197,1200,1202],{"class":86,"line":1196},19,[84,1198,1199],{"class":94},") -> ",[84,1201,112],{"class":111},[84,1203,115],{"class":94},[84,1205,1207],{"class":86,"line":1206},20,[84,1208,122],{"class":121},[10,1210,1211],{},"Which renders as:",[74,1213,1216],{"className":1214,"code":1215,"language":276},[274],"Usage: mytool sync [OPTIONS] SOURCE\n\n  Upload changed files from SOURCE to the configured bucket.\n\nArguments:\n  SOURCE  Directory to upload.  [required]\n\nOptions:\n  --retries INTEGER RANGE  Attempts per file before giving up.  [1\u003C=x\u003C=10; default: 3]\n  --timeout SECONDS        Seconds to wait for each upload.  [default: 30.0]\n  --mode [keep|delete]     What to do about files missing locally.  [default: keep]\n  --force \u002F --no-force     Upload even when the remote copy looks newer.  [default: no-force]\n  --help                   Show this message and exit.\n",[81,1217,1215],{"__ignoreMap":79},[10,1219,1220],{},"Nothing was added to the implementation — the extra information came from the enum, the range, the\nmetavar and four sentences. That is the general shape of improving help output: most of it is\nalready available to the framework, and the rest is one line of English per option.",[10,1222,1223,1224,1227],{},"The remaining gap is examples, which the epilog covers. Two lines under this command turn \"what\ndoes ",[81,1225,1226],{},"--mode delete"," do to my files\" into something a cautious user can test:",[74,1229,1232],{"className":1230,"code":1231,"language":276},[274],"Examples:\n  mytool sync .\u002Fdata --dry-run          # show what would change\n  mytool sync .\u002Fdata --mode delete      # mirror: remove remote files gone locally\n",[81,1233,1231],{"__ignoreMap":79},[14,1235,1237],{"id":1236},"where-to-go-next","Where to go next",[19,1239,1240,1247,1254,1261],{},[22,1241,1242,1246],{},[283,1243,1245],{"href":1244},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read\u002F","Writing help text users actually read","\n— summaries, option lines, metavars and the wording that survives scanning.",[22,1248,1249,1253],{},[283,1250,1252],{"href":1251},"\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","\n— where examples go in each framework, and how to keep them correct.",[22,1255,1256,1260],{},[283,1257,1259],{"href":1258},"\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","\n— man pages, markdown reference and the CI check that stops drift.",[22,1262,1263,1266],{},[283,1264,1265],{"href":469},"Versioning and deprecating CLI flags","\n— aliases, warnings and removals that do not break scripts.",[14,1268,1270],{"id":1269},"key-takeaways","Key takeaways",[19,1272,1273,1276,1279,1282,1285],{},[22,1274,1275],{},"The command summary is the most-read sentence in your tool; write it for users, not developers.",[22,1277,1278],{},"Every option line should say what it does, what it takes and what the default is.",[22,1280,1281],{},"The root help screen should route, not enumerate.",[22,1283,1284],{},"Examples in an epilog answer the question the option list cannot.",[22,1286,1287],{},"Generate reference documentation; hand-write the reasoning; check both in CI.",[14,1289,1291],{"id":1290},"frequently-asked-questions","Frequently asked questions",[1293,1294,1296],"h3",{"id":1295},"how-long-should-help-output-be","How long should help output be?",[10,1298,1299],{},"The root screen should fit on one terminal screen. A command's own screen can be longer, but if it\nruns past two screens the command probably has too many options — or some of them belong in a\nconfig file, where they can be set once instead of typed.",[1293,1301,1303],{"id":1302},"should-help-text-be-localised","Should help text be localised?",[10,1305,1306],{},"Rarely worth it for developer tooling, where English is the working language and the flag names are\nEnglish anyway. If your audience genuinely needs it, both Click and Typer accept translated strings,\nand the practical difficulty is not translation but keeping examples and error messages consistent\nwith it.",[1293,1308,1310],{"id":1309},"is-rich-formatted-help-a-good-idea","Is Rich-formatted help a good idea?",[10,1312,1313,1314,1317],{},"Typer's default Rich help is pleasant for humans and occasionally awkward in logs and narrow\nterminals. It degrades reasonably, and ",[81,1315,1316],{},"rich_markup_mode=None"," turns it off if you need plain\noutput. What matters more than the styling is the wording underneath it.",[1293,1319,1321],{"id":1320},"where-should-the-exit-codes-be-documented","Where should the exit codes be documented?",[10,1323,1324],{},"In the help epilog for the root command, and in the man page's EXIT STATUS section. Users of\nscripts look for them there before they look on a website, and a tool that documents its codes is\none people are willing to branch on.",[1293,1326,1328],{"id":1327},"do-i-need-a-man-page","Do I need a man page?",[10,1330,1331,1332,1335],{},"If your tool is installed by a system package manager, yes — users expect ",[81,1333,1334],{},"man mytool"," to work.\nFor a tool installed with pipx, it is a nice extra rather than an expectation, and generating one\nfrom the command tree costs a build step.",[1293,1337,1339],{"id":1338},"should-hidden-commands-appear-in-help","Should hidden commands appear in help?",[10,1341,1342,1343,1346],{},"Internal or experimental commands can be hidden (",[81,1344,1345],{},"hidden=True"," in both frameworks) and still work\nwhen typed, which is a reasonable way to ship something before committing to it. Be aware that\nhidden is not private: users find these commands and come to depend on them, so anything hidden for\nmore than a release or two should either be documented or removed.",[1293,1348,1350],{"id":1349},"how-do-i-document-a-command-that-only-makes-sense-with-another","How do I document a command that only makes sense with another?",[10,1352,1353,1354,1357,1358,1361],{},"Say so in the summary, in both directions: \"Requires a prior ",[81,1355,1356],{},"mytool init","\" on one and \"Prepares a\ndirectory for ",[81,1359,1360],{},"mytool sync","\" on the other. Cross-references in help text are cheap and they are the\nonly navigation a reader has while inside the terminal.",[14,1363,1365],{"id":1364},"related","Related",[19,1367,1368,1375,1382,1388,1395],{},[22,1369,1370,1371],{},"Up: ",[283,1372,1374],{"href":1373},"\u002Fadvanced-input-parsing-user-experience\u002F","Advanced Input Parsing & User Experience",[22,1376,1377,1378],{},"Sideways: ",[283,1379,1381],{"href":1380},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002F","Shell completion for Python CLIs",[22,1383,1377,1384],{},[283,1385,1387],{"href":1386},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002F","Error handling and exit codes for CLIs",[22,1389,1390,1391],{},"Related: ",[283,1392,1394],{"href":1393},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","Managing CLI versioning and changelogs",[22,1396,1390,1397],{},[283,1398,1399],{"href":285},"Structuring multi-command Python CLIs",[1401,1402,1403],"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":79,"searchDepth":98,"depth":98,"links":1405},[1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1429],{"id":16,"depth":98,"text":17},{"id":59,"depth":98,"text":60},{"id":128,"depth":98,"text":129},{"id":259,"depth":98,"text":260},{"id":290,"depth":98,"text":291},{"id":401,"depth":98,"text":402},{"id":459,"depth":98,"text":460},{"id":474,"depth":98,"text":475},{"id":623,"depth":98,"text":624},{"id":691,"depth":98,"text":692},{"id":742,"depth":98,"text":743},{"id":928,"depth":98,"text":929},{"id":1236,"depth":98,"text":1237},{"id":1269,"depth":98,"text":1270},{"id":1290,"depth":98,"text":1291,"children":1421},[1422,1423,1424,1425,1426,1427,1428],{"id":1295,"depth":118,"text":1296},{"id":1302,"depth":118,"text":1303},{"id":1309,"depth":118,"text":1310},{"id":1320,"depth":118,"text":1321},{"id":1327,"depth":118,"text":1328},{"id":1338,"depth":118,"text":1339},{"id":1349,"depth":118,"text":1350},{"id":1364,"depth":98,"text":1365},"2026-08-01","Write help output people read - option lines, epilog examples, generated man pages and docs, and deprecating flags without breaking anyone's scripts.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation",{"title":5,"description":1431},"advanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Findex",[158,1440,1441,1442,1443],"documentation","ux","deprecation","man-pages","UH3Sj1WEdd96pXXNN9HKYo07LDXdfdv7pscEqq_4q7w",[1446,1449,1452,1455,1458,1461,1464,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1510,1513,1516,1519,1522,1525,1528,1531,1534,1537,1540,1543,1546,1549,1552,1555,1558,1561,1564,1567,1570,1573,1576,1579,1582,1585,1588,1591,1594,1597,1600,1603,1606,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645,1648,1651,1654,1657,1660,1663,1666,1669,1672,1675,1678,1681,1684,1687,1690],{"path":1447,"title":1448},"\u002Fabout","About Python CLI Toolcraft",{"path":1450,"title":1451},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1453,"title":1454},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1456,"title":1457},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1459,"title":1460},"\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":1462,"title":1463},"\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":1436,"title":5},{"path":1466,"title":1467},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1469,"title":1470},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1472,"title":1473},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1475,"title":1476},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1478,"title":1479},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1481,"title":1482},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1484,"title":1485},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1487,"title":1488},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1490,"title":1491},"\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":1493,"title":1494},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1496,"title":1497},"\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":1499,"title":1500},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1502,"title":1503},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1505,"title":1506},"\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":1508,"title":1509},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1511,"title":1512},"\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":1514,"title":1515},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1517,"title":1518},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1520,"title":1521},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1523,"title":1524},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1526,"title":1527},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1529,"title":1530},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1532,"title":1533},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1535,"title":1536},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1538,"title":1539},"\u002F","Python CLI Toolcraft",{"path":1541,"title":1542},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1544,"title":1545},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1547,"title":1548},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1550,"title":1551},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1553,"title":1554},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1556,"title":1557},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1559,"title":1560},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1562,"title":1563},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1565,"title":1566},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1568,"title":1569},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1571,"title":1572},"\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":1574,"title":1575},"\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":1577,"title":1578},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1580,"title":1581},"\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":1583,"title":1584},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1586,"title":1587},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1589,"title":1590},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1592,"title":1593},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1595,"title":1596},"\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":1598,"title":1599},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1601,"title":1602},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1604,"title":1605},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1607,"title":1608},"\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":1610,"title":1611},"\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":1613,"title":1614},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1616,"title":1617},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1619,"title":1620},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1622,"title":1623},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1625,"title":1626},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":1628,"title":1629},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":1631,"title":1632},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1634,"title":1635},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1637,"title":1638},"\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":1640,"title":1641},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1643,"title":1644},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1646,"title":1647},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1649,"title":1650},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1652,"title":1653},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1655,"title":1656},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1658,"title":1659},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1661,"title":1662},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":1664,"title":1665},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1667,"title":1668},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1670,"title":1671},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1673,"title":1674},"\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":1676,"title":1677},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1679,"title":1680},"\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":1682,"title":1683},"\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":1685,"title":1686},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1688,"title":1689},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1691,"title":1692},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690027]