[{"data":1,"prerenderedAt":2578},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002F":3,"content-directory":2430},{"id":4,"title":5,"body":6,"date":2415,"description":2416,"difficulty":2417,"draft":2418,"extension":2419,"meta":2420,"navigation":145,"path":2421,"seo":2422,"stem":2423,"tags":2424,"updated":2415,"__hash__":2429},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002Findex.md","argparse Subparsers for Subcommands",{"type":7,"value":8,"toc":2402},"minimark",[9,38,43,98,102,111,396,472,478,509,513,530,865,892,908,912,939,1175,1199,1203,1213,1483,1512,1533,1537,1544,1600,1668,1677,1803,1816,1820,1839,1877,1890,1989,2041,2060,2064,2080,2125,2131,2135,2146,2295,2306,2310,2367,2371,2398],[10,11,12,13,17,18,17,21,24,25,28,29,33,34,37],"p",{},"Once a stdlib CLI does more than one thing — ",[14,15,16],"code",{},"tool build",", ",[14,19,20],{},"tool deploy",[14,22,23],{},"tool clean"," — you\nwant git-style subcommands, each with its own arguments and help page. ",[14,26,27],{},"argparse"," handles\nthis with ",[30,31,32],"strong",{},"subparsers",": a special group where each entry is a full parser in its own right.\nThis guide builds a clean, dispatchable subcommand tree with no third-party dependencies, and\nshows the ",[14,35,36],{},"set_defaults(func=...)"," pattern that keeps the routing tidy.",[39,40,42],"h2",{"id":41},"tldr","TL;DR",[44,45,46,58,73,83,89],"ul",{},[47,48,49,50,53,54,57],"li",{},"Create the group with ",[14,51,52],{},"parser.add_subparsers(dest=\"command\", required=True)",", then add one\nparser per subcommand with ",[14,55,56],{},"sub.add_parser(\"name\")",".",[47,59,60,61,64,65,68,69,72],{},"Attach a handler to each subparser with ",[14,62,63],{},"set_defaults(func=handler)"," and dispatch with a\nsingle ",[14,66,67],{},"args.func(args)"," call — no ",[14,70,71],{},"if\u002Felif"," ladder.",[47,74,75,76,79,80,57],{},"Share options across subcommands with a ",[30,77,78],{},"parent parser"," passed via ",[14,81,82],{},"parents=[...]",[47,84,85,86,57],{},"Nest subcommands by giving a subparser its own ",[14,87,88],{},"add_subparsers()",[47,90,91,92,97],{},"This is the manual version of what ",[93,94,96],"a",{"href":95},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click\u002F","Click groups","\ndo for free.",[39,99,101],{"id":100},"the-core-mechanism-add_subparsers","The core mechanism: add_subparsers",[10,103,104,106,107,110],{},[14,105,88],{}," turns one positional slot into a switch over named subparsers. Each\nsubparser is a normal ",[14,108,109],{},"ArgumentParser"," you configure independently.",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-python shiki shiki-themes github-light github-dark","# tool.py\nimport argparse\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(prog=\"tool\", description=\"Project tool.\")\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    build = subparsers.add_parser(\"build\", help=\"Build the project.\")\n    build.add_argument(\"--release\", action=\"store_true\", help=\"Optimized build.\")\n\n    deploy = subparsers.add_parser(\"deploy\", help=\"Deploy the project.\")\n    deploy.add_argument(\"target\", help=\"Environment to deploy to.\")\n\n    args = parser.parse_args()\n    print(args)\n\nif __name__ == \"__main__\":\n    main()\n","python","",[14,119,120,129,140,147,167,202,233,238,264,294,299,323,343,348,359,368,373,390],{"__ignoreMap":117},[121,122,125],"span",{"class":123,"line":124},"line",1,[121,126,128],{"class":127},"sJ8bj","# tool.py\n",[121,130,132,136],{"class":123,"line":131},2,[121,133,135],{"class":134},"szBVR","import",[121,137,139],{"class":138},"sVt8B"," argparse\n",[121,141,143],{"class":123,"line":142},3,[121,144,146],{"emptyLinePlaceholder":145},true,"\n",[121,148,150,153,157,160,164],{"class":123,"line":149},4,[121,151,152],{"class":134},"def",[121,154,156],{"class":155},"sScJk"," main",[121,158,159],{"class":138},"() -> ",[121,161,163],{"class":162},"sj4cs","None",[121,165,166],{"class":138},":\n",[121,168,170,173,176,179,183,185,189,191,194,196,199],{"class":123,"line":169},5,[121,171,172],{"class":138},"    parser ",[121,174,175],{"class":134},"=",[121,177,178],{"class":138}," argparse.ArgumentParser(",[121,180,182],{"class":181},"s4XuR","prog",[121,184,175],{"class":134},[121,186,188],{"class":187},"sZZnC","\"tool\"",[121,190,17],{"class":138},[121,192,193],{"class":181},"description",[121,195,175],{"class":134},[121,197,198],{"class":187},"\"Project tool.\"",[121,200,201],{"class":138},")\n",[121,203,205,208,210,213,216,218,221,223,226,228,231],{"class":123,"line":204},6,[121,206,207],{"class":138},"    subparsers ",[121,209,175],{"class":134},[121,211,212],{"class":138}," parser.add_subparsers(",[121,214,215],{"class":181},"dest",[121,217,175],{"class":134},[121,219,220],{"class":187},"\"command\"",[121,222,17],{"class":138},[121,224,225],{"class":181},"required",[121,227,175],{"class":134},[121,229,230],{"class":162},"True",[121,232,201],{"class":138},[121,234,236],{"class":123,"line":235},7,[121,237,146],{"emptyLinePlaceholder":145},[121,239,241,244,246,249,252,254,257,259,262],{"class":123,"line":240},8,[121,242,243],{"class":138},"    build ",[121,245,175],{"class":134},[121,247,248],{"class":138}," subparsers.add_parser(",[121,250,251],{"class":187},"\"build\"",[121,253,17],{"class":138},[121,255,256],{"class":181},"help",[121,258,175],{"class":134},[121,260,261],{"class":187},"\"Build the project.\"",[121,263,201],{"class":138},[121,265,267,270,273,275,278,280,283,285,287,289,292],{"class":123,"line":266},9,[121,268,269],{"class":138},"    build.add_argument(",[121,271,272],{"class":187},"\"--release\"",[121,274,17],{"class":138},[121,276,277],{"class":181},"action",[121,279,175],{"class":134},[121,281,282],{"class":187},"\"store_true\"",[121,284,17],{"class":138},[121,286,256],{"class":181},[121,288,175],{"class":134},[121,290,291],{"class":187},"\"Optimized build.\"",[121,293,201],{"class":138},[121,295,297],{"class":123,"line":296},10,[121,298,146],{"emptyLinePlaceholder":145},[121,300,302,305,307,309,312,314,316,318,321],{"class":123,"line":301},11,[121,303,304],{"class":138},"    deploy ",[121,306,175],{"class":134},[121,308,248],{"class":138},[121,310,311],{"class":187},"\"deploy\"",[121,313,17],{"class":138},[121,315,256],{"class":181},[121,317,175],{"class":134},[121,319,320],{"class":187},"\"Deploy the project.\"",[121,322,201],{"class":138},[121,324,326,329,332,334,336,338,341],{"class":123,"line":325},12,[121,327,328],{"class":138},"    deploy.add_argument(",[121,330,331],{"class":187},"\"target\"",[121,333,17],{"class":138},[121,335,256],{"class":181},[121,337,175],{"class":134},[121,339,340],{"class":187},"\"Environment to deploy to.\"",[121,342,201],{"class":138},[121,344,346],{"class":123,"line":345},13,[121,347,146],{"emptyLinePlaceholder":145},[121,349,351,354,356],{"class":123,"line":350},14,[121,352,353],{"class":138},"    args ",[121,355,175],{"class":134},[121,357,358],{"class":138}," parser.parse_args()\n",[121,360,362,365],{"class":123,"line":361},15,[121,363,364],{"class":162},"    print",[121,366,367],{"class":138},"(args)\n",[121,369,371],{"class":123,"line":370},16,[121,372,146],{"emptyLinePlaceholder":145},[121,374,376,379,382,385,388],{"class":123,"line":375},17,[121,377,378],{"class":134},"if",[121,380,381],{"class":162}," __name__",[121,383,384],{"class":134}," ==",[121,386,387],{"class":187}," \"__main__\"",[121,389,166],{"class":138},[121,391,393],{"class":123,"line":392},18,[121,394,395],{"class":138},"    main()\n",[112,397,401],{"className":398,"code":399,"language":400,"meta":117,"style":117},"language-bash shiki shiki-themes github-light github-dark","$ python tool.py build --release\nNamespace(command='build', release=True)\n\n$ python tool.py deploy prod\nNamespace(command='deploy', target='prod')\n","bash",[14,402,403,420,438,442,456],{"__ignoreMap":117},[121,404,405,408,411,414,417],{"class":123,"line":124},[121,406,407],{"class":155},"$",[121,409,410],{"class":187}," python",[121,412,413],{"class":187}," tool.py",[121,415,416],{"class":187}," build",[121,418,419],{"class":162}," --release\n",[121,421,422,425,427,430,433,436],{"class":123,"line":131},[121,423,424],{"class":155},"Namespace(command",[121,426,175],{"class":187},[121,428,429],{"class":155},"'build'",[121,431,432],{"class":155},",",[121,434,435],{"class":187}," release=True",[121,437,201],{"class":138},[121,439,440],{"class":123,"line":142},[121,441,146],{"emptyLinePlaceholder":145},[121,443,444,446,448,450,453],{"class":123,"line":149},[121,445,407],{"class":155},[121,447,410],{"class":187},[121,449,413],{"class":187},[121,451,452],{"class":187}," deploy",[121,454,455],{"class":187}," prod\n",[121,457,458,460,462,465,467,470],{"class":123,"line":169},[121,459,424],{"class":155},[121,461,175],{"class":187},[121,463,464],{"class":155},"'deploy'",[121,466,432],{"class":155},[121,468,469],{"class":187}," target='prod'",[121,471,201],{"class":138},[10,473,474,475,477],{},"Two arguments to ",[14,476,88],{}," matter most:",[44,479,480,492],{},[47,481,482,487,488,491],{},[30,483,484],{},[14,485,486],{},"dest=\"command\""," stores the chosen subcommand name in ",[14,489,490],{},"args.command",", so you know which\none ran.",[47,493,494,499,500,503,504,508],{},[30,495,496],{},[14,497,498],{},"required=True"," makes running ",[14,501,502],{},"tool"," with no subcommand an error instead of silently\ndoing nothing. On Python 3, subparsers are ",[505,506,507],"em",{},"optional"," by default — always set this\nexplicitly, or a bare invocation falls through to code that assumes a command was given.",[39,510,512],{"id":511},"dispatching-with-set_defaultsfunc","Dispatching with set_defaults(func=...)",[10,514,515,516,518,519,522,523,526,527,57],{},"Reading ",[14,517,490],{}," and branching with ",[14,520,521],{},"if command == \"build\": ... elif ..."," works but\nrots as commands multiply. The idiomatic argparse pattern is to attach the handler function\nto each subparser with ",[14,524,525],{},"set_defaults",", then call whatever landed in ",[14,528,529],{},"args.func",[112,531,533],{"className":114,"code":532,"language":116,"meta":117,"style":117},"# tool.py\nimport argparse\n\ndef cmd_build(args: argparse.Namespace) -> int:\n    print(f\"Building (release={args.release})\")\n    return 0\n\ndef cmd_deploy(args: argparse.Namespace) -> int:\n    print(f\"Deploying to {args.target}\")\n    return 0\n\ndef main() -> int:\n    parser = argparse.ArgumentParser(prog=\"tool\", description=\"Project tool.\")\n    subparsers = parser.add_subparsers(dest=\"command\", required=True)\n\n    build = subparsers.add_parser(\"build\", help=\"Build the project.\")\n    build.add_argument(\"--release\", action=\"store_true\")\n    build.set_defaults(func=cmd_build)\n\n    deploy = subparsers.add_parser(\"deploy\", help=\"Deploy the project.\")\n    deploy.add_argument(\"target\")\n    deploy.set_defaults(func=cmd_deploy)\n\n    args = parser.parse_args()\n    return args.func(args)          # dispatch: no if\u002Felif needed\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n",[14,534,535,539,545,549,564,591,599,603,616,639,645,649,661,685,709,713,733,749,762,767,788,797,810,815,824,835,840,853],{"__ignoreMap":117},[121,536,537],{"class":123,"line":124},[121,538,128],{"class":127},[121,540,541,543],{"class":123,"line":131},[121,542,135],{"class":134},[121,544,139],{"class":138},[121,546,547],{"class":123,"line":142},[121,548,146],{"emptyLinePlaceholder":145},[121,550,551,553,556,559,562],{"class":123,"line":149},[121,552,152],{"class":134},[121,554,555],{"class":155}," cmd_build",[121,557,558],{"class":138},"(args: argparse.Namespace) -> ",[121,560,561],{"class":162},"int",[121,563,166],{"class":138},[121,565,566,568,571,574,577,580,583,586,589],{"class":123,"line":169},[121,567,364],{"class":162},[121,569,570],{"class":138},"(",[121,572,573],{"class":134},"f",[121,575,576],{"class":187},"\"Building (release=",[121,578,579],{"class":162},"{",[121,581,582],{"class":138},"args.release",[121,584,585],{"class":162},"}",[121,587,588],{"class":187},")\"",[121,590,201],{"class":138},[121,592,593,596],{"class":123,"line":204},[121,594,595],{"class":134},"    return",[121,597,598],{"class":162}," 0\n",[121,600,601],{"class":123,"line":235},[121,602,146],{"emptyLinePlaceholder":145},[121,604,605,607,610,612,614],{"class":123,"line":240},[121,606,152],{"class":134},[121,608,609],{"class":155}," cmd_deploy",[121,611,558],{"class":138},[121,613,561],{"class":162},[121,615,166],{"class":138},[121,617,618,620,622,624,627,629,632,634,637],{"class":123,"line":266},[121,619,364],{"class":162},[121,621,570],{"class":138},[121,623,573],{"class":134},[121,625,626],{"class":187},"\"Deploying to ",[121,628,579],{"class":162},[121,630,631],{"class":138},"args.target",[121,633,585],{"class":162},[121,635,636],{"class":187},"\"",[121,638,201],{"class":138},[121,640,641,643],{"class":123,"line":296},[121,642,595],{"class":134},[121,644,598],{"class":162},[121,646,647],{"class":123,"line":301},[121,648,146],{"emptyLinePlaceholder":145},[121,650,651,653,655,657,659],{"class":123,"line":325},[121,652,152],{"class":134},[121,654,156],{"class":155},[121,656,159],{"class":138},[121,658,561],{"class":162},[121,660,166],{"class":138},[121,662,663,665,667,669,671,673,675,677,679,681,683],{"class":123,"line":345},[121,664,172],{"class":138},[121,666,175],{"class":134},[121,668,178],{"class":138},[121,670,182],{"class":181},[121,672,175],{"class":134},[121,674,188],{"class":187},[121,676,17],{"class":138},[121,678,193],{"class":181},[121,680,175],{"class":134},[121,682,198],{"class":187},[121,684,201],{"class":138},[121,686,687,689,691,693,695,697,699,701,703,705,707],{"class":123,"line":350},[121,688,207],{"class":138},[121,690,175],{"class":134},[121,692,212],{"class":138},[121,694,215],{"class":181},[121,696,175],{"class":134},[121,698,220],{"class":187},[121,700,17],{"class":138},[121,702,225],{"class":181},[121,704,175],{"class":134},[121,706,230],{"class":162},[121,708,201],{"class":138},[121,710,711],{"class":123,"line":361},[121,712,146],{"emptyLinePlaceholder":145},[121,714,715,717,719,721,723,725,727,729,731],{"class":123,"line":370},[121,716,243],{"class":138},[121,718,175],{"class":134},[121,720,248],{"class":138},[121,722,251],{"class":187},[121,724,17],{"class":138},[121,726,256],{"class":181},[121,728,175],{"class":134},[121,730,261],{"class":187},[121,732,201],{"class":138},[121,734,735,737,739,741,743,745,747],{"class":123,"line":375},[121,736,269],{"class":138},[121,738,272],{"class":187},[121,740,17],{"class":138},[121,742,277],{"class":181},[121,744,175],{"class":134},[121,746,282],{"class":187},[121,748,201],{"class":138},[121,750,751,754,757,759],{"class":123,"line":392},[121,752,753],{"class":138},"    build.set_defaults(",[121,755,756],{"class":181},"func",[121,758,175],{"class":134},[121,760,761],{"class":138},"cmd_build)\n",[121,763,765],{"class":123,"line":764},19,[121,766,146],{"emptyLinePlaceholder":145},[121,768,770,772,774,776,778,780,782,784,786],{"class":123,"line":769},20,[121,771,304],{"class":138},[121,773,175],{"class":134},[121,775,248],{"class":138},[121,777,311],{"class":187},[121,779,17],{"class":138},[121,781,256],{"class":181},[121,783,175],{"class":134},[121,785,320],{"class":187},[121,787,201],{"class":138},[121,789,791,793,795],{"class":123,"line":790},21,[121,792,328],{"class":138},[121,794,331],{"class":187},[121,796,201],{"class":138},[121,798,800,803,805,807],{"class":123,"line":799},22,[121,801,802],{"class":138},"    deploy.set_defaults(",[121,804,756],{"class":181},[121,806,175],{"class":134},[121,808,809],{"class":138},"cmd_deploy)\n",[121,811,813],{"class":123,"line":812},23,[121,814,146],{"emptyLinePlaceholder":145},[121,816,818,820,822],{"class":123,"line":817},24,[121,819,353],{"class":138},[121,821,175],{"class":134},[121,823,358],{"class":138},[121,825,827,829,832],{"class":123,"line":826},25,[121,828,595],{"class":134},[121,830,831],{"class":138}," args.func(args)          ",[121,833,834],{"class":127},"# dispatch: no if\u002Felif needed\n",[121,836,838],{"class":123,"line":837},26,[121,839,146],{"emptyLinePlaceholder":145},[121,841,843,845,847,849,851],{"class":123,"line":842},27,[121,844,378],{"class":134},[121,846,381],{"class":162},[121,848,384],{"class":134},[121,850,387],{"class":187},[121,852,166],{"class":138},[121,854,856,859,862],{"class":123,"line":855},28,[121,857,858],{"class":134},"    raise",[121,860,861],{"class":162}," SystemExit",[121,863,864],{"class":138},"(main())\n",[10,866,867,870,871,873,874,876,877,879,880,882,883,886,887,891],{},[14,868,869],{},"set_defaults(func=cmd_build)"," injects ",[14,872,756],{}," into the namespace only when that subparser is\nselected, so ",[14,875,529],{}," is always the right handler. Adding a fifth or fiftieth command\nnever touches the dispatch line — you write a handler and one ",[14,878,525],{},". Returning an\n",[14,881,561],{}," from each handler and passing it to ",[14,884,885],{},"SystemExit"," gives you real\n",[93,888,890],{"href":889},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","exit codes","\nfor callers and CI.",[10,893,894,895,897,898,900,901,904,905,57],{},"Because ",[14,896,498],{}," guarantees a subcommand, ",[14,899,529],{}," always exists. If you ever set\n",[14,902,903],{},"required=False"," on purpose, guard with ",[14,906,907],{},"if not hasattr(args, \"func\"): parser.print_help()",[39,909,911],{"id":910},"sharing-options-with-a-parent-parser","Sharing options with a parent parser",[10,913,914,915,17,918,921,922,925,926,929,930,932,933,936,937,57],{},"Several subcommands often need the same flags — ",[14,916,917],{},"--verbose",[14,919,920],{},"--config",". Declaring them on\neach subparser is duplication; declaring them on the ",[505,923,924],{},"top-level"," parser puts them before the\nsubcommand (",[14,927,928],{},"tool --verbose build","), which is not always what you want. The clean fix is a\n",[30,931,78],{},": a parser marked ",[14,934,935],{},"add_help=False"," whose arguments are inherited via\n",[14,938,82],{},[112,940,942],{"className":114,"code":941,"language":116,"meta":117,"style":117},"import argparse\n\n# Shared options live once, in a parent that adds no help of its own.\ncommon = argparse.ArgumentParser(add_help=False)\ncommon.add_argument(\"-v\", \"--verbose\", action=\"store_true\", help=\"Verbose output.\")\ncommon.add_argument(\"--config\", default=\"config.toml\", help=\"Config file path.\")\n\nparser = argparse.ArgumentParser(prog=\"tool\")\nsubparsers = parser.add_subparsers(dest=\"command\", required=True)\n\nbuild = subparsers.add_parser(\"build\", parents=[common], help=\"Build the project.\")\nbuild.add_argument(\"--release\", action=\"store_true\")\n\ndeploy = subparsers.add_parser(\"deploy\", parents=[common], help=\"Deploy.\")\ndeploy.add_argument(\"target\")\n",[14,943,944,950,954,959,978,1010,1038,1042,1059,1084,1088,1117,1134,1138,1166],{"__ignoreMap":117},[121,945,946,948],{"class":123,"line":124},[121,947,135],{"class":134},[121,949,139],{"class":138},[121,951,952],{"class":123,"line":131},[121,953,146],{"emptyLinePlaceholder":145},[121,955,956],{"class":123,"line":142},[121,957,958],{"class":127},"# Shared options live once, in a parent that adds no help of its own.\n",[121,960,961,964,966,968,971,973,976],{"class":123,"line":149},[121,962,963],{"class":138},"common ",[121,965,175],{"class":134},[121,967,178],{"class":138},[121,969,970],{"class":181},"add_help",[121,972,175],{"class":134},[121,974,975],{"class":162},"False",[121,977,201],{"class":138},[121,979,980,983,986,988,991,993,995,997,999,1001,1003,1005,1008],{"class":123,"line":169},[121,981,982],{"class":138},"common.add_argument(",[121,984,985],{"class":187},"\"-v\"",[121,987,17],{"class":138},[121,989,990],{"class":187},"\"--verbose\"",[121,992,17],{"class":138},[121,994,277],{"class":181},[121,996,175],{"class":134},[121,998,282],{"class":187},[121,1000,17],{"class":138},[121,1002,256],{"class":181},[121,1004,175],{"class":134},[121,1006,1007],{"class":187},"\"Verbose output.\"",[121,1009,201],{"class":138},[121,1011,1012,1014,1017,1019,1022,1024,1027,1029,1031,1033,1036],{"class":123,"line":204},[121,1013,982],{"class":138},[121,1015,1016],{"class":187},"\"--config\"",[121,1018,17],{"class":138},[121,1020,1021],{"class":181},"default",[121,1023,175],{"class":134},[121,1025,1026],{"class":187},"\"config.toml\"",[121,1028,17],{"class":138},[121,1030,256],{"class":181},[121,1032,175],{"class":134},[121,1034,1035],{"class":187},"\"Config file path.\"",[121,1037,201],{"class":138},[121,1039,1040],{"class":123,"line":235},[121,1041,146],{"emptyLinePlaceholder":145},[121,1043,1044,1047,1049,1051,1053,1055,1057],{"class":123,"line":240},[121,1045,1046],{"class":138},"parser ",[121,1048,175],{"class":134},[121,1050,178],{"class":138},[121,1052,182],{"class":181},[121,1054,175],{"class":134},[121,1056,188],{"class":187},[121,1058,201],{"class":138},[121,1060,1061,1064,1066,1068,1070,1072,1074,1076,1078,1080,1082],{"class":123,"line":266},[121,1062,1063],{"class":138},"subparsers ",[121,1065,175],{"class":134},[121,1067,212],{"class":138},[121,1069,215],{"class":181},[121,1071,175],{"class":134},[121,1073,220],{"class":187},[121,1075,17],{"class":138},[121,1077,225],{"class":181},[121,1079,175],{"class":134},[121,1081,230],{"class":162},[121,1083,201],{"class":138},[121,1085,1086],{"class":123,"line":296},[121,1087,146],{"emptyLinePlaceholder":145},[121,1089,1090,1093,1095,1097,1099,1101,1104,1106,1109,1111,1113,1115],{"class":123,"line":301},[121,1091,1092],{"class":138},"build ",[121,1094,175],{"class":134},[121,1096,248],{"class":138},[121,1098,251],{"class":187},[121,1100,17],{"class":138},[121,1102,1103],{"class":181},"parents",[121,1105,175],{"class":134},[121,1107,1108],{"class":138},"[common], ",[121,1110,256],{"class":181},[121,1112,175],{"class":134},[121,1114,261],{"class":187},[121,1116,201],{"class":138},[121,1118,1119,1122,1124,1126,1128,1130,1132],{"class":123,"line":325},[121,1120,1121],{"class":138},"build.add_argument(",[121,1123,272],{"class":187},[121,1125,17],{"class":138},[121,1127,277],{"class":181},[121,1129,175],{"class":134},[121,1131,282],{"class":187},[121,1133,201],{"class":138},[121,1135,1136],{"class":123,"line":345},[121,1137,146],{"emptyLinePlaceholder":145},[121,1139,1140,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161,1164],{"class":123,"line":350},[121,1141,1142],{"class":138},"deploy ",[121,1144,175],{"class":134},[121,1146,248],{"class":138},[121,1148,311],{"class":187},[121,1150,17],{"class":138},[121,1152,1103],{"class":181},[121,1154,175],{"class":134},[121,1156,1108],{"class":138},[121,1158,256],{"class":181},[121,1160,175],{"class":134},[121,1162,1163],{"class":187},"\"Deploy.\"",[121,1165,201],{"class":138},[121,1167,1168,1171,1173],{"class":123,"line":361},[121,1169,1170],{"class":138},"deploy.add_argument(",[121,1172,331],{"class":187},[121,1174,201],{"class":138},[10,1176,1177,1178,1181,1182,1185,1186,1188,1189,1191,1192,1194,1195,1198],{},"Now both ",[14,1179,1180],{},"tool build --verbose"," and ",[14,1183,1184],{},"tool deploy prod --verbose --config prod.toml"," work, and\n",[14,1187,917],{},"\u002F",[14,1190,920],{}," are defined in exactly one place. ",[14,1193,935],{}," on the parent is\nessential — without it, both the parent and child try to add ",[14,1196,1197],{},"-h",", and argparse raises an\n\"conflicting option string\" error at startup.",[39,1200,1202],{"id":1201},"nested-subcommands","Nested subcommands",[10,1204,1205,1206,1209,1210,1212],{},"A subparser can host its own subparsers, giving you ",[14,1207,1208],{},"tool remote add ...",". Give the\nintermediate subparser a fresh ",[14,1211,88],{}," and attach children to it:",[112,1214,1216],{"className":114,"code":1215,"language":116,"meta":117,"style":117},"import argparse\n\nparser = argparse.ArgumentParser(prog=\"tool\")\ntop = parser.add_subparsers(dest=\"command\", required=True)\n\nremote = top.add_parser(\"remote\", help=\"Manage remotes.\")\nremote_sub = remote.add_subparsers(dest=\"remote_command\", required=True)\n\nadd = remote_sub.add_parser(\"add\", help=\"Add a remote.\")\nadd.add_argument(\"url\")\nadd.set_defaults(func=lambda a: print(f\"Added {a.url}\"))\n\nrm = remote_sub.add_parser(\"remove\", help=\"Remove a remote.\")\nrm.add_argument(\"name\")\nrm.set_defaults(func=lambda a: print(f\"Removed {a.name}\"))\n\nargs = parser.parse_args()\nargs.func(args)\n",[14,1217,1218,1224,1228,1244,1269,1273,1297,1324,1328,1352,1362,1397,1401,1424,1434,1465,1469,1478],{"__ignoreMap":117},[121,1219,1220,1222],{"class":123,"line":124},[121,1221,135],{"class":134},[121,1223,139],{"class":138},[121,1225,1226],{"class":123,"line":131},[121,1227,146],{"emptyLinePlaceholder":145},[121,1229,1230,1232,1234,1236,1238,1240,1242],{"class":123,"line":142},[121,1231,1046],{"class":138},[121,1233,175],{"class":134},[121,1235,178],{"class":138},[121,1237,182],{"class":181},[121,1239,175],{"class":134},[121,1241,188],{"class":187},[121,1243,201],{"class":138},[121,1245,1246,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267],{"class":123,"line":149},[121,1247,1248],{"class":138},"top ",[121,1250,175],{"class":134},[121,1252,212],{"class":138},[121,1254,215],{"class":181},[121,1256,175],{"class":134},[121,1258,220],{"class":187},[121,1260,17],{"class":138},[121,1262,225],{"class":181},[121,1264,175],{"class":134},[121,1266,230],{"class":162},[121,1268,201],{"class":138},[121,1270,1271],{"class":123,"line":169},[121,1272,146],{"emptyLinePlaceholder":145},[121,1274,1275,1278,1280,1283,1286,1288,1290,1292,1295],{"class":123,"line":204},[121,1276,1277],{"class":138},"remote ",[121,1279,175],{"class":134},[121,1281,1282],{"class":138}," top.add_parser(",[121,1284,1285],{"class":187},"\"remote\"",[121,1287,17],{"class":138},[121,1289,256],{"class":181},[121,1291,175],{"class":134},[121,1293,1294],{"class":187},"\"Manage remotes.\"",[121,1296,201],{"class":138},[121,1298,1299,1302,1304,1307,1309,1311,1314,1316,1318,1320,1322],{"class":123,"line":235},[121,1300,1301],{"class":138},"remote_sub ",[121,1303,175],{"class":134},[121,1305,1306],{"class":138}," remote.add_subparsers(",[121,1308,215],{"class":181},[121,1310,175],{"class":134},[121,1312,1313],{"class":187},"\"remote_command\"",[121,1315,17],{"class":138},[121,1317,225],{"class":181},[121,1319,175],{"class":134},[121,1321,230],{"class":162},[121,1323,201],{"class":138},[121,1325,1326],{"class":123,"line":240},[121,1327,146],{"emptyLinePlaceholder":145},[121,1329,1330,1333,1335,1338,1341,1343,1345,1347,1350],{"class":123,"line":266},[121,1331,1332],{"class":138},"add ",[121,1334,175],{"class":134},[121,1336,1337],{"class":138}," remote_sub.add_parser(",[121,1339,1340],{"class":187},"\"add\"",[121,1342,17],{"class":138},[121,1344,256],{"class":181},[121,1346,175],{"class":134},[121,1348,1349],{"class":187},"\"Add a remote.\"",[121,1351,201],{"class":138},[121,1353,1354,1357,1360],{"class":123,"line":296},[121,1355,1356],{"class":138},"add.add_argument(",[121,1358,1359],{"class":187},"\"url\"",[121,1361,201],{"class":138},[121,1363,1364,1367,1369,1372,1375,1378,1380,1382,1385,1387,1390,1392,1394],{"class":123,"line":301},[121,1365,1366],{"class":138},"add.set_defaults(",[121,1368,756],{"class":181},[121,1370,1371],{"class":134},"=lambda",[121,1373,1374],{"class":138}," a: ",[121,1376,1377],{"class":162},"print",[121,1379,570],{"class":138},[121,1381,573],{"class":134},[121,1383,1384],{"class":187},"\"Added ",[121,1386,579],{"class":162},[121,1388,1389],{"class":138},"a.url",[121,1391,585],{"class":162},[121,1393,636],{"class":187},[121,1395,1396],{"class":138},"))\n",[121,1398,1399],{"class":123,"line":325},[121,1400,146],{"emptyLinePlaceholder":145},[121,1402,1403,1406,1408,1410,1413,1415,1417,1419,1422],{"class":123,"line":345},[121,1404,1405],{"class":138},"rm ",[121,1407,175],{"class":134},[121,1409,1337],{"class":138},[121,1411,1412],{"class":187},"\"remove\"",[121,1414,17],{"class":138},[121,1416,256],{"class":181},[121,1418,175],{"class":134},[121,1420,1421],{"class":187},"\"Remove a remote.\"",[121,1423,201],{"class":138},[121,1425,1426,1429,1432],{"class":123,"line":350},[121,1427,1428],{"class":138},"rm.add_argument(",[121,1430,1431],{"class":187},"\"name\"",[121,1433,201],{"class":138},[121,1435,1436,1439,1441,1443,1445,1447,1449,1451,1454,1456,1459,1461,1463],{"class":123,"line":361},[121,1437,1438],{"class":138},"rm.set_defaults(",[121,1440,756],{"class":181},[121,1442,1371],{"class":134},[121,1444,1374],{"class":138},[121,1446,1377],{"class":162},[121,1448,570],{"class":138},[121,1450,573],{"class":134},[121,1452,1453],{"class":187},"\"Removed ",[121,1455,579],{"class":162},[121,1457,1458],{"class":138},"a.name",[121,1460,585],{"class":162},[121,1462,636],{"class":187},[121,1464,1396],{"class":138},[121,1466,1467],{"class":123,"line":370},[121,1468,146],{"emptyLinePlaceholder":145},[121,1470,1471,1474,1476],{"class":123,"line":375},[121,1472,1473],{"class":138},"args ",[121,1475,175],{"class":134},[121,1477,358],{"class":138},[121,1479,1480],{"class":123,"line":392},[121,1481,1482],{"class":138},"args.func(args)\n",[112,1484,1486],{"className":398,"code":1485,"language":400,"meta":117,"style":117},"$ python tool.py remote add https:\u002F\u002Fexample.com\u002Freg\nAdded https:\u002F\u002Fexample.com\u002Freg\n",[14,1487,1488,1505],{"__ignoreMap":117},[121,1489,1490,1492,1494,1496,1499,1502],{"class":123,"line":124},[121,1491,407],{"class":155},[121,1493,410],{"class":187},[121,1495,413],{"class":187},[121,1497,1498],{"class":187}," remote",[121,1500,1501],{"class":187}," add",[121,1503,1504],{"class":187}," https:\u002F\u002Fexample.com\u002Freg\n",[121,1506,1507,1510],{"class":123,"line":131},[121,1508,1509],{"class":155},"Added",[121,1511,1504],{"class":187},[10,1513,1514,1515,1518,1519,1521,1522,1525,1526,1529,1530,1532],{},"Use a ",[505,1516,1517],{},"distinct"," ",[14,1520,215],{}," at each level (",[14,1523,1524],{},"command",", then ",[14,1527,1528],{},"remote_command",") so the two choices\ndon't overwrite each other in the namespace. The ",[14,1531,36],{}," dispatch keeps\nworking no matter how deep you nest, because the innermost selected subparser wins. In\npractice two levels is the usability ceiling — beyond that, help output and muscle memory\nboth suffer.",[39,1534,1536],{"id":1535},"keeping-help-output-clean","Keeping help output clean",[10,1538,1539,1540,1543],{},"Subparsers can make ",[14,1541,1542],{},"--help"," noisy. A few habits keep it readable:",[44,1545,1546,1568,1590],{},[47,1547,1548,1518,1558,1560,1561,1563,1564,1567],{},[30,1549,1550,1551,1554,1555,57],{},"Give every subparser a ",[14,1552,1553],{},"help="," and a ",[14,1556,1557],{},"description=",[14,1559,1553],{}," is the one-liner shown in\nthe parent's command list; ",[14,1562,1557],{}," is the paragraph shown on ",[14,1565,1566],{},"tool build --help",".\nThey are different strings and both matter.",[47,1569,1570,1577,1578,1581,1582,1585,1586,1589],{},[30,1571,1572,1573,1576],{},"Set ",[14,1574,1575],{},"metavar"," on the subparsers object"," to control the placeholder in usage text:\n",[14,1579,1580],{},"parser.add_subparsers(dest=\"command\", metavar=\"COMMAND\")"," prints ",[14,1583,1584],{},"COMMAND"," instead of the\nauto-generated ",[14,1587,1588],{},"{build,deploy,clean}"," brace list, which gets unwieldy past a few commands.",[47,1591,1592,1595,1596,1599],{},[30,1593,1594],{},"Group related flags"," with ",[14,1597,1598],{},"parser.add_argument_group(\"output options\")"," inside a busy\nsubparser so its help splits into labeled sections.",[112,1601,1603],{"className":114,"code":1602,"language":116,"meta":117,"style":117},"subparsers = parser.add_subparsers(\n    dest=\"command\", required=True, metavar=\"COMMAND\",\n    title=\"commands\", help=\"Run 'tool COMMAND --help' for details.\",\n)\n",[14,1604,1605,1614,1643,1664],{"__ignoreMap":117},[121,1606,1607,1609,1611],{"class":123,"line":124},[121,1608,1063],{"class":138},[121,1610,175],{"class":134},[121,1612,1613],{"class":138}," parser.add_subparsers(\n",[121,1615,1616,1619,1621,1623,1625,1627,1629,1631,1633,1635,1637,1640],{"class":123,"line":131},[121,1617,1618],{"class":181},"    dest",[121,1620,175],{"class":134},[121,1622,220],{"class":187},[121,1624,17],{"class":138},[121,1626,225],{"class":181},[121,1628,175],{"class":134},[121,1630,230],{"class":162},[121,1632,17],{"class":138},[121,1634,1575],{"class":181},[121,1636,175],{"class":134},[121,1638,1639],{"class":187},"\"COMMAND\"",[121,1641,1642],{"class":138},",\n",[121,1644,1645,1648,1650,1653,1655,1657,1659,1662],{"class":123,"line":142},[121,1646,1647],{"class":181},"    title",[121,1649,175],{"class":134},[121,1651,1652],{"class":187},"\"commands\"",[121,1654,17],{"class":138},[121,1656,256],{"class":181},[121,1658,175],{"class":134},[121,1660,1661],{"class":187},"\"Run 'tool COMMAND --help' for details.\"",[121,1663,1642],{"class":138},[121,1665,1666],{"class":123,"line":149},[121,1667,201],{"class":138},[10,1669,1670,1671,1181,1674,1676],{},"With ",[14,1672,1673],{},"title",[14,1675,1575],{}," set, the top-level help reads like a real command index instead of\na brace-list dump:",[112,1678,1680],{"className":398,"code":1679,"language":400,"meta":117,"style":117},"$ python tool.py --help\nusage: tool [-h] COMMAND ...\n\nProject tool.\n\noptions:\n  -h, --help  show this help message and exit\n\ncommands:\n  COMMAND     Run 'tool COMMAND --help' for details.\n    build     Build the project.\n    deploy    Deploy the project.\n",[14,1681,1682,1693,1704,1708,1716,1720,1725,1751,1755,1760,1777,1791],{"__ignoreMap":117},[121,1683,1684,1686,1688,1690],{"class":123,"line":124},[121,1685,407],{"class":155},[121,1687,410],{"class":187},[121,1689,413],{"class":187},[121,1691,1692],{"class":162}," --help\n",[121,1694,1695,1698,1701],{"class":123,"line":131},[121,1696,1697],{"class":155},"usage:",[121,1699,1700],{"class":187}," tool",[121,1702,1703],{"class":138}," [-h] COMMAND ...\n",[121,1705,1706],{"class":123,"line":142},[121,1707,146],{"emptyLinePlaceholder":145},[121,1709,1710,1713],{"class":123,"line":149},[121,1711,1712],{"class":155},"Project",[121,1714,1715],{"class":187}," tool.\n",[121,1717,1718],{"class":123,"line":169},[121,1719,146],{"emptyLinePlaceholder":145},[121,1721,1722],{"class":123,"line":204},[121,1723,1724],{"class":155},"options:\n",[121,1726,1727,1730,1733,1736,1739,1742,1745,1748],{"class":123,"line":235},[121,1728,1729],{"class":155},"  -h,",[121,1731,1732],{"class":162}," --help",[121,1734,1735],{"class":187},"  show",[121,1737,1738],{"class":187}," this",[121,1740,1741],{"class":187}," help",[121,1743,1744],{"class":187}," message",[121,1746,1747],{"class":187}," and",[121,1749,1750],{"class":187}," exit\n",[121,1752,1753],{"class":123,"line":240},[121,1754,146],{"emptyLinePlaceholder":145},[121,1756,1757],{"class":123,"line":266},[121,1758,1759],{"class":155},"commands:\n",[121,1761,1762,1765,1768,1771,1774],{"class":123,"line":296},[121,1763,1764],{"class":155},"  COMMAND",[121,1766,1767],{"class":187},"     Run",[121,1769,1770],{"class":187}," 'tool COMMAND --help'",[121,1772,1773],{"class":187}," for",[121,1775,1776],{"class":187}," details.\n",[121,1778,1779,1782,1785,1788],{"class":123,"line":301},[121,1780,1781],{"class":155},"    build",[121,1783,1784],{"class":187},"     Build",[121,1786,1787],{"class":187}," the",[121,1789,1790],{"class":187}," project.\n",[121,1792,1793,1796,1799,1801],{"class":123,"line":325},[121,1794,1795],{"class":155},"    deploy",[121,1797,1798],{"class":187},"    Deploy",[121,1800,1787],{"class":187},[121,1802,1790],{"class":187},[10,1804,1805,1806,1808,1809,1811,1812,1815],{},"Each subcommand's own ",[14,1807,1197],{}," then shows its ",[14,1810,193],{}," and arguments, so users discover the\ntree one level at a time — the same progressive-disclosure shape a\n",[93,1813,1814],{"href":95},"Click group","\ngives you automatically.",[39,1817,1819],{"id":1818},"aliases-and-mutually-exclusive-options","Aliases and mutually exclusive options",[10,1821,1822,1823,1826,1827,1830,1831,1834,1835,1838],{},"Two features round out a realistic subcommand tree. ",[14,1824,1825],{},"add_parser"," accepts ",[14,1828,1829],{},"aliases",", so\n",[14,1832,1833],{},"tool rm"," can be a shorthand for ",[14,1836,1837],{},"tool remove"," without a second handler:",[112,1840,1842],{"className":114,"code":1841,"language":116,"meta":117,"style":117},"rm = remote_sub.add_parser(\"remove\", aliases=[\"rm\"], help=\"Remove a remote.\")\n",[14,1843,1844],{"__ignoreMap":117},[121,1845,1846,1848,1850,1852,1854,1856,1858,1860,1863,1866,1869,1871,1873,1875],{"class":123,"line":124},[121,1847,1405],{"class":138},[121,1849,175],{"class":134},[121,1851,1337],{"class":138},[121,1853,1412],{"class":187},[121,1855,17],{"class":138},[121,1857,1829],{"class":181},[121,1859,175],{"class":134},[121,1861,1862],{"class":138},"[",[121,1864,1865],{"class":187},"\"rm\"",[121,1867,1868],{"class":138},"], ",[121,1870,256],{"class":181},[121,1872,175],{"class":134},[121,1874,1421],{"class":187},[121,1876,201],{"class":138},[10,1878,1879,1880,1181,1883,1886,1887,1889],{},"Both ",[14,1881,1882],{},"tool remote remove foo",[14,1884,1885],{},"tool remote rm foo"," now route to the same subparser and\nthe same ",[14,1888,756],{},". Within a subcommand you often want \"exactly one of these flags\"; a mutually\nexclusive group enforces that at parse time:",[112,1891,1893],{"className":114,"code":1892,"language":116,"meta":117,"style":117},"push = subparsers.add_parser(\"push\", help=\"Push changes.\")\nmode = push.add_mutually_exclusive_group(required=True)\nmode.add_argument(\"--force\", action=\"store_true\", help=\"Overwrite remote history.\")\nmode.add_argument(\"--safe\", action=\"store_true\", help=\"Refuse on conflict.\")\n",[14,1894,1895,1918,1936,1963],{"__ignoreMap":117},[121,1896,1897,1900,1902,1904,1907,1909,1911,1913,1916],{"class":123,"line":124},[121,1898,1899],{"class":138},"push ",[121,1901,175],{"class":134},[121,1903,248],{"class":138},[121,1905,1906],{"class":187},"\"push\"",[121,1908,17],{"class":138},[121,1910,256],{"class":181},[121,1912,175],{"class":134},[121,1914,1915],{"class":187},"\"Push changes.\"",[121,1917,201],{"class":138},[121,1919,1920,1923,1925,1928,1930,1932,1934],{"class":123,"line":131},[121,1921,1922],{"class":138},"mode ",[121,1924,175],{"class":134},[121,1926,1927],{"class":138}," push.add_mutually_exclusive_group(",[121,1929,225],{"class":181},[121,1931,175],{"class":134},[121,1933,230],{"class":162},[121,1935,201],{"class":138},[121,1937,1938,1941,1944,1946,1948,1950,1952,1954,1956,1958,1961],{"class":123,"line":142},[121,1939,1940],{"class":138},"mode.add_argument(",[121,1942,1943],{"class":187},"\"--force\"",[121,1945,17],{"class":138},[121,1947,277],{"class":181},[121,1949,175],{"class":134},[121,1951,282],{"class":187},[121,1953,17],{"class":138},[121,1955,256],{"class":181},[121,1957,175],{"class":134},[121,1959,1960],{"class":187},"\"Overwrite remote history.\"",[121,1962,201],{"class":138},[121,1964,1965,1967,1970,1972,1974,1976,1978,1980,1982,1984,1987],{"class":123,"line":149},[121,1966,1940],{"class":138},[121,1968,1969],{"class":187},"\"--safe\"",[121,1971,17],{"class":138},[121,1973,277],{"class":181},[121,1975,175],{"class":134},[121,1977,282],{"class":187},[121,1979,17],{"class":138},[121,1981,256],{"class":181},[121,1983,175],{"class":134},[121,1985,1986],{"class":187},"\"Refuse on conflict.\"",[121,1988,201],{"class":138},[112,1990,1992],{"className":398,"code":1991,"language":400,"meta":117,"style":117},"$ python tool.py push --force --safe\ntool push: error: argument --safe: not allowed with argument --force\n",[14,1993,1994,2011],{"__ignoreMap":117},[121,1995,1996,1998,2000,2002,2005,2008],{"class":123,"line":124},[121,1997,407],{"class":155},[121,1999,410],{"class":187},[121,2001,413],{"class":187},[121,2003,2004],{"class":187}," push",[121,2006,2007],{"class":162}," --force",[121,2009,2010],{"class":162}," --safe\n",[121,2012,2013,2015,2018,2021,2024,2027,2030,2033,2036,2038],{"class":123,"line":131},[121,2014,502],{"class":155},[121,2016,2017],{"class":187}," push:",[121,2019,2020],{"class":187}," error:",[121,2022,2023],{"class":187}," argument",[121,2025,2026],{"class":162}," --safe:",[121,2028,2029],{"class":187}," not",[121,2031,2032],{"class":187}," allowed",[121,2034,2035],{"class":187}," with",[121,2037,2023],{"class":187},[121,2039,2040],{"class":162}," --force\n",[10,2042,2043,2045,2046,1181,2049,2052,2053,2055,2056,2059],{},[14,2044,27],{}," rejects the illegal combination itself, with a clear message, so your handler\nnever has to validate that ",[14,2047,2048],{},"--force",[14,2050,2051],{},"--safe"," weren't both passed. ",[14,2054,498],{}," on the\ngroup makes supplying ",[505,2057,2058],{},"neither"," an error too.",[39,2061,2063],{"id":2062},"a-default-subcommand","A default subcommand",[10,2065,2066,2067,2069,2070,2073,2074,2076,2077,2079],{},"Sometimes running the bare tool should behave like one of the subcommands — ",[14,2068,502],{}," alone\nacting as ",[14,2071,2072],{},"tool status",". Keep ",[14,2075,903],{},", then fall back when no ",[14,2078,756],{}," was set:",[112,2081,2083],{"className":114,"code":2082,"language":116,"meta":117,"style":117},"args = parser.parse_args()\nhandler = getattr(args, \"func\", cmd_status)   # default to status\nraise SystemExit(handler(args))\n",[14,2084,2085,2093,2115],{"__ignoreMap":117},[121,2086,2087,2089,2091],{"class":123,"line":124},[121,2088,1473],{"class":138},[121,2090,175],{"class":134},[121,2092,358],{"class":138},[121,2094,2095,2098,2100,2103,2106,2109,2112],{"class":123,"line":131},[121,2096,2097],{"class":138},"handler ",[121,2099,175],{"class":134},[121,2101,2102],{"class":162}," getattr",[121,2104,2105],{"class":138},"(args, ",[121,2107,2108],{"class":187},"\"func\"",[121,2110,2111],{"class":138},", cmd_status)   ",[121,2113,2114],{"class":127},"# default to status\n",[121,2116,2117,2120,2122],{"class":123,"line":142},[121,2118,2119],{"class":134},"raise",[121,2121,861],{"class":162},[121,2123,2124],{"class":138},"(handler(args))\n",[10,2126,2127,2128,2130],{},"This is the one case where you deliberately leave the subparsers group optional. Everywhere\nelse, ",[14,2129,498],{}," and an explicit error is the safer default — a silent no-op confuses\nusers who fat-fingered a command name.",[39,2132,2134],{"id":2133},"testing-subcommand-dispatch","Testing subcommand dispatch",[10,2136,2137,2138,2141,2142,2145],{},"Because parsing is separate from the handlers, you can test both cheaply. Feed an explicit\nargument list to ",[14,2139,2140],{},"parse_args"," — it only reads ",[14,2143,2144],{},"sys.argv"," when you pass nothing — and assert\nthat the right handler and values landed in the namespace.",[112,2147,2149],{"className":114,"code":2148,"language":116,"meta":117,"style":117},"# test_tool.py\nfrom tool import build_parser        # factor the parser into a function that returns it\n\ndef test_build_routes_to_handler() -> None:\n    args = build_parser().parse_args([\"build\", \"--release\"])\n    assert args.command == \"build\"\n    assert args.release is True\n    assert args.func.__name__ == \"cmd_build\"\n\ndef test_missing_subcommand_errors() -> None:\n    import pytest\n    with pytest.raises(SystemExit):        # required=True exits on no subcommand\n        build_parser().parse_args([])\n",[14,2150,2151,2156,2172,2176,2189,2207,2221,2234,2249,2253,2266,2274,2290],{"__ignoreMap":117},[121,2152,2153],{"class":123,"line":124},[121,2154,2155],{"class":127},"# test_tool.py\n",[121,2157,2158,2161,2164,2166,2169],{"class":123,"line":131},[121,2159,2160],{"class":134},"from",[121,2162,2163],{"class":138}," tool ",[121,2165,135],{"class":134},[121,2167,2168],{"class":138}," build_parser        ",[121,2170,2171],{"class":127},"# factor the parser into a function that returns it\n",[121,2173,2174],{"class":123,"line":142},[121,2175,146],{"emptyLinePlaceholder":145},[121,2177,2178,2180,2183,2185,2187],{"class":123,"line":149},[121,2179,152],{"class":134},[121,2181,2182],{"class":155}," test_build_routes_to_handler",[121,2184,159],{"class":138},[121,2186,163],{"class":162},[121,2188,166],{"class":138},[121,2190,2191,2193,2195,2198,2200,2202,2204],{"class":123,"line":169},[121,2192,353],{"class":138},[121,2194,175],{"class":134},[121,2196,2197],{"class":138}," build_parser().parse_args([",[121,2199,251],{"class":187},[121,2201,17],{"class":138},[121,2203,272],{"class":187},[121,2205,2206],{"class":138},"])\n",[121,2208,2209,2212,2215,2218],{"class":123,"line":204},[121,2210,2211],{"class":134},"    assert",[121,2213,2214],{"class":138}," args.command ",[121,2216,2217],{"class":134},"==",[121,2219,2220],{"class":187}," \"build\"\n",[121,2222,2223,2225,2228,2231],{"class":123,"line":235},[121,2224,2211],{"class":134},[121,2226,2227],{"class":138}," args.release ",[121,2229,2230],{"class":134},"is",[121,2232,2233],{"class":162}," True\n",[121,2235,2236,2238,2241,2244,2246],{"class":123,"line":240},[121,2237,2211],{"class":134},[121,2239,2240],{"class":138}," args.func.",[121,2242,2243],{"class":162},"__name__",[121,2245,384],{"class":134},[121,2247,2248],{"class":187}," \"cmd_build\"\n",[121,2250,2251],{"class":123,"line":266},[121,2252,146],{"emptyLinePlaceholder":145},[121,2254,2255,2257,2260,2262,2264],{"class":123,"line":296},[121,2256,152],{"class":134},[121,2258,2259],{"class":155}," test_missing_subcommand_errors",[121,2261,159],{"class":138},[121,2263,163],{"class":162},[121,2265,166],{"class":138},[121,2267,2268,2271],{"class":123,"line":301},[121,2269,2270],{"class":134},"    import",[121,2272,2273],{"class":138}," pytest\n",[121,2275,2276,2279,2282,2284,2287],{"class":123,"line":325},[121,2277,2278],{"class":134},"    with",[121,2280,2281],{"class":138}," pytest.raises(",[121,2283,885],{"class":162},[121,2285,2286],{"class":138},"):        ",[121,2288,2289],{"class":127},"# required=True exits on no subcommand\n",[121,2291,2292],{"class":123,"line":345},[121,2293,2294],{"class":138},"        build_parser().parse_args([])\n",[10,2296,2297,2298,2301,2302,2305],{},"Refactoring parser construction into a ",[14,2299,2300],{},"build_parser() -> argparse.ArgumentParser"," function\n(instead of building it inline in ",[14,2303,2304],{},"main()",") is what makes this testable — do it early.",[39,2307,2309],{"id":2308},"production-notes","Production notes",[44,2311,2312,2320,2337,2348],{},[47,2313,2314,2319],{},[30,2315,2316,2318],{},[14,2317,498],{}," is not the default."," The single most common argparse subcommand bug is a\nbare invocation silently doing nothing because the subparsers group was optional. Always set\nit, and test the empty-args case.",[47,2321,2322,2328,2329,2331,2332,17,2334,2336],{},[30,2323,2324,2325,2327],{},"Distinct ",[14,2326,215],{}," per level."," Reusing ",[14,2330,486],{}," for nested subparsers overwrites the\nouter value. Name them ",[14,2333,1524],{},[14,2335,1528],{},", and so on.",[47,2338,2339,2344,2345,2347],{},[30,2340,2341,2343],{},[14,2342,935],{}," on parent parsers",", always — otherwise the inherited ",[14,2346,1197],{}," collides.",[47,2349,2350,2353,2354,2357,2358,2361,2362,2366],{},[30,2351,2352],{},"This scales to a point."," Manual subparsers are fine for a dozen commands. When you find\nyourself wanting shared context objects, lazy loading for startup time, or completion, the\nhand-rolled version stops paying off. The Click equivalent —\n",[93,2355,2356],{"href":95},"building a CLI with subcommands in Click","\n— gives you groups, ",[14,2359,2360],{},"ctx.obj",", and completion out of the box, and\n",[93,2363,2365],{"href":2364},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002F","migrating from argparse to Typer","\nmaps subparsers directly onto Typer commands.",[39,2368,2370],{"id":2369},"related","Related",[44,2372,2373,2380,2386,2391],{},[47,2374,2375,2376],{},"Up: ",[93,2377,2379],{"href":2378},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F","Command-Line Parsing with argparse",[47,2381,2382,2383],{},"Sideways: ",[93,2384,2385],{"href":95},"Building a CLI with subcommands in Click",[47,2387,2382,2388],{},[93,2389,2390],{"href":2364},"Migrating from argparse to Typer",[47,2392,2393,2394],{},"Related: ",[93,2395,2397],{"href":2396},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","Structuring multi-command Python CLIs",[2399,2400,2401],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}",{"title":117,"searchDepth":131,"depth":131,"links":2403},[2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414],{"id":41,"depth":131,"text":42},{"id":100,"depth":131,"text":101},{"id":511,"depth":131,"text":512},{"id":910,"depth":131,"text":911},{"id":1201,"depth":131,"text":1202},{"id":1535,"depth":131,"text":1536},{"id":1818,"depth":131,"text":1819},{"id":2062,"depth":131,"text":2063},{"id":2133,"depth":131,"text":2134},{"id":2308,"depth":131,"text":2309},{"id":2369,"depth":131,"text":2370},"2026-07-05","Add git-style subcommands to an argparse CLI with add_subparsers, dispatch with set_defaults, share common options, and keep help output clean.","intermediate",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands",{"title":5,"description":2416},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002Findex",[27,2425,2426,2427,2428],"cli","subcommands","structure","click","JGJkKMRvp-Dfsels3IA9sPSF4HwiN8tFJykryVc9b1s",[2431,2434,2437,2440,2443,2446,2449,2452,2455,2458,2461,2464,2467,2470,2473,2476,2479,2482,2485,2487,2490,2493,2496,2497,2499,2501,2504,2507,2510,2513,2516,2519,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572,2575],{"path":2432,"title":2433},"\u002Fabout","About Python CLI Toolcraft",{"path":2435,"title":2436},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2438,"title":2439},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2441,"title":2442},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2444,"title":2445},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2447,"title":2448},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2450,"title":2451},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2453,"title":2454},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2456,"title":2457},"\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":2459,"title":2460},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2462,"title":2463},"\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":2465,"title":2466},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2468,"title":2469},"\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":2471,"title":2472},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2474,"title":2475},"\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":2477,"title":2478},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2480,"title":2481},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2483,"title":2484},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1188,"title":2486},"Python CLI Toolcraft",{"path":2488,"title":2489},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2491,"title":2492},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2494,"title":2495},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2421,"title":5},{"path":2498,"title":2379},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse",{"path":2500,"title":2390},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer",{"path":2502,"title":2503},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2505,"title":2506},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2508,"title":2509},"\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":2511,"title":2512},"\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":2514,"title":2515},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2517,"title":2518},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2520,"title":2385},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click",{"path":2522,"title":2523},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2525,"title":2526},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2528,"title":2529},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2531,"title":2532},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2534,"title":2535},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2537,"title":2538},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2540,"title":2541},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2543,"title":2544},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2546,"title":2547},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2549,"title":2550},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2552,"title":2553},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2555,"title":2556},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2558,"title":2559},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2561,"title":2562},"\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":2564,"title":2565},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2567,"title":2568},"\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":2570,"title":2571},"\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":2573,"title":2574},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2576,"title":2577},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867197]