[{"data":1,"prerenderedAt":3370},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F":3,"content-directory":3124},{"id":4,"title":5,"body":6,"date":3108,"description":3109,"difficulty":3110,"draft":3111,"extension":3112,"meta":3113,"navigation":194,"path":3114,"seo":3115,"stem":3116,"tags":3117,"updated":3122,"__hash__":3123},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Findex.md","Command-Line Parsing with argparse",{"type":7,"value":8,"toc":3083},"minimark",[9,26,31,111,115,119,125,135,139,146,530,626,662,683,687,690,693,889,950,954,977,980,1119,1169,1188,1224,1238,1242,1262,1410,1427,1431,1455,1620,1631,1635,1640,1643,1734,1752,1756,1834,1838,1844,1853,1931,1937,2005,2017,2098,2108,2137,2141,2146,2252,2265,2271,2337,2353,2357,2360,2373,2382,2443,2453,2462,2465,2469,2475,2489,2570,2576,2750,2761,2770,2882,2893,2897,2902,2905,2909,2912,2960,2971,2978,2995,2999,3009,3013,3023,3027,3046,3050,3079],[10,11,12,16,17,21,22,25],"p",{},[13,14,15],"code",{},"argparse"," is the argument parser in the Python standard library, and for a surprising\nnumber of tools it is all you need. It ships with every interpreter, so a CLI built on it\nhas ",[18,19,20],"strong",{},"zero third-party dependencies"," — nothing to pin, nothing to break on a ",[13,23,24],{},"pip","\nresolution, nothing extra to audit. This overview shows you how to build a real parser with\nit, validate input properly, and recognize the point where a heavier framework earns its\nplace.",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,58,80,90,99],"ul",{},[35,36,37,38,41,42,45,46,49,50,53,54,57],"li",{},"Create a parser with ",[13,39,40],{},"argparse.ArgumentParser",", add ",[18,43,44],{},"positional"," arguments with\n",[13,47,48],{},"add_argument(\"name\")"," and ",[18,51,52],{},"optional"," ones with ",[13,55,56],{},"add_argument(\"--flag\")",".",[35,59,60,61,64,65,68,69,72,73,76,77,57],{},"Coerce and validate with ",[13,62,63],{},"type=",", constrain with ",[13,66,67],{},"choices=",", set fallbacks with\n",[13,70,71],{},"default=",", and collect multiples with ",[13,74,75],{},"nargs=",". Boolean flags use\n",[13,78,79],{},"action=\"store_true\"",[35,81,82,85,86,89],{},[13,83,84],{},"parse_args()"," returns a plain ",[13,87,88],{},"Namespace","; read values as attributes.",[35,91,92,93,98],{},"Reach for ",[94,95,97],"a",{"href":96},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002F","subparsers","\nfor git-style subcommands.",[35,100,101,102,106,107,57],{},"Graduate to ",[94,103,105],{"href":104},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002F","Click or Typer","\nonce you want nested groups, shell completion, and less boilerplate — and when you do,\nfollow the ",[94,108,110],{"href":109},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer\u002F","argparse-to-Typer migration guide",[112,113],"inline-diagram",{"name":114},"argparse-parser-anatomy",[27,116,118],{"id":117},"why-start-with-argparse","Why start with argparse",[10,120,121,122,124],{},"Every third-party CLI framework has to justify a dependency. ",[13,123,15],{}," never does — it is\npart of Python, documented alongside the language, and stable across releases. If you are\nwriting an internal script, a build helper, or a tool that must run in a locked-down\nenvironment where installing packages is painful, the calculus is simple: reach for the\nstdlib first.",[10,126,127,128,131,132,134],{},"It also teaches you the model that Click and Typer sit on top of. Positional vs optional\narguments, type coercion, ",[13,129,130],{},"nargs",", and subcommand dispatch are the same ideas everywhere;\n",[13,133,15],{}," just makes you spell them out. Learn it here and the frameworks feel like\nshortcuts rather than magic.",[27,136,138],{"id":137},"a-runnable-parser","A runnable parser",[10,140,141,142,145],{},"Here is a complete program — a small file-copier — that uses a positional argument, a typed\noption, and a boolean flag. Save it as ",[13,143,144],{},"mvcp.py"," and run it directly.",[147,148,153],"pre",{"className":149,"code":150,"language":151,"meta":152,"style":152},"language-python shiki shiki-themes github-light github-dark","# mvcp.py\nimport argparse\nfrom pathlib import Path\n\ndef main() -> None:\n    parser = argparse.ArgumentParser(\n        prog=\"mvcp\",\n        description=\"Copy a file, optionally renaming it.\",\n    )\n    parser.add_argument(\"source\", type=Path, help=\"File to copy.\")\n    parser.add_argument(\n        \"--dest-dir\",\n        type=Path,\n        default=Path.cwd(),\n        help=\"Directory to copy into (default: current directory).\",\n    )\n    parser.add_argument(\n        \"--overwrite\",\n        action=\"store_true\",\n        help=\"Replace the destination if it already exists.\",\n    )\n\n    args = parser.parse_args()\n    target = args.dest_dir \u002F args.source.name\n    if target.exists() and not args.overwrite:\n        parser.error(f\"{target} exists; pass --overwrite to replace it\")\n    print(f\"Would copy {args.source} -> {target}\")\n\nif __name__ == \"__main__\":\n    main()\n","python","",[13,154,155,164,175,189,196,216,228,244,257,263,294,300,308,319,330,343,348,353,361,374,386,391,396,407,424,442,468,502,507,524],{"__ignoreMap":152},[156,157,160],"span",{"class":158,"line":159},"line",1,[156,161,163],{"class":162},"sJ8bj","# mvcp.py\n",[156,165,167,171],{"class":158,"line":166},2,[156,168,170],{"class":169},"szBVR","import",[156,172,174],{"class":173},"sVt8B"," argparse\n",[156,176,178,181,184,186],{"class":158,"line":177},3,[156,179,180],{"class":169},"from",[156,182,183],{"class":173}," pathlib ",[156,185,170],{"class":169},[156,187,188],{"class":173}," Path\n",[156,190,192],{"class":158,"line":191},4,[156,193,195],{"emptyLinePlaceholder":194},true,"\n",[156,197,199,202,206,209,213],{"class":158,"line":198},5,[156,200,201],{"class":169},"def",[156,203,205],{"class":204},"sScJk"," main",[156,207,208],{"class":173},"() -> ",[156,210,212],{"class":211},"sj4cs","None",[156,214,215],{"class":173},":\n",[156,217,219,222,225],{"class":158,"line":218},6,[156,220,221],{"class":173},"    parser ",[156,223,224],{"class":169},"=",[156,226,227],{"class":173}," argparse.ArgumentParser(\n",[156,229,231,235,237,241],{"class":158,"line":230},7,[156,232,234],{"class":233},"s4XuR","        prog",[156,236,224],{"class":169},[156,238,240],{"class":239},"sZZnC","\"mvcp\"",[156,242,243],{"class":173},",\n",[156,245,247,250,252,255],{"class":158,"line":246},8,[156,248,249],{"class":233},"        description",[156,251,224],{"class":169},[156,253,254],{"class":239},"\"Copy a file, optionally renaming it.\"",[156,256,243],{"class":173},[156,258,260],{"class":158,"line":259},9,[156,261,262],{"class":173},"    )\n",[156,264,266,269,272,275,278,280,283,286,288,291],{"class":158,"line":265},10,[156,267,268],{"class":173},"    parser.add_argument(",[156,270,271],{"class":239},"\"source\"",[156,273,274],{"class":173},", ",[156,276,277],{"class":233},"type",[156,279,224],{"class":169},[156,281,282],{"class":173},"Path, ",[156,284,285],{"class":233},"help",[156,287,224],{"class":169},[156,289,290],{"class":239},"\"File to copy.\"",[156,292,293],{"class":173},")\n",[156,295,297],{"class":158,"line":296},11,[156,298,299],{"class":173},"    parser.add_argument(\n",[156,301,303,306],{"class":158,"line":302},12,[156,304,305],{"class":239},"        \"--dest-dir\"",[156,307,243],{"class":173},[156,309,311,314,316],{"class":158,"line":310},13,[156,312,313],{"class":233},"        type",[156,315,224],{"class":169},[156,317,318],{"class":173},"Path,\n",[156,320,322,325,327],{"class":158,"line":321},14,[156,323,324],{"class":233},"        default",[156,326,224],{"class":169},[156,328,329],{"class":173},"Path.cwd(),\n",[156,331,333,336,338,341],{"class":158,"line":332},15,[156,334,335],{"class":233},"        help",[156,337,224],{"class":169},[156,339,340],{"class":239},"\"Directory to copy into (default: current directory).\"",[156,342,243],{"class":173},[156,344,346],{"class":158,"line":345},16,[156,347,262],{"class":173},[156,349,351],{"class":158,"line":350},17,[156,352,299],{"class":173},[156,354,356,359],{"class":158,"line":355},18,[156,357,358],{"class":239},"        \"--overwrite\"",[156,360,243],{"class":173},[156,362,364,367,369,372],{"class":158,"line":363},19,[156,365,366],{"class":233},"        action",[156,368,224],{"class":169},[156,370,371],{"class":239},"\"store_true\"",[156,373,243],{"class":173},[156,375,377,379,381,384],{"class":158,"line":376},20,[156,378,335],{"class":233},[156,380,224],{"class":169},[156,382,383],{"class":239},"\"Replace the destination if it already exists.\"",[156,385,243],{"class":173},[156,387,389],{"class":158,"line":388},21,[156,390,262],{"class":173},[156,392,394],{"class":158,"line":393},22,[156,395,195],{"emptyLinePlaceholder":194},[156,397,399,402,404],{"class":158,"line":398},23,[156,400,401],{"class":173},"    args ",[156,403,224],{"class":169},[156,405,406],{"class":173}," parser.parse_args()\n",[156,408,410,413,415,418,421],{"class":158,"line":409},24,[156,411,412],{"class":173},"    target ",[156,414,224],{"class":169},[156,416,417],{"class":173}," args.dest_dir ",[156,419,420],{"class":169},"\u002F",[156,422,423],{"class":173}," args.source.name\n",[156,425,427,430,433,436,439],{"class":158,"line":426},25,[156,428,429],{"class":169},"    if",[156,431,432],{"class":173}," target.exists() ",[156,434,435],{"class":169},"and",[156,437,438],{"class":169}," not",[156,440,441],{"class":173}," args.overwrite:\n",[156,443,445,448,451,454,457,460,463,466],{"class":158,"line":444},26,[156,446,447],{"class":173},"        parser.error(",[156,449,450],{"class":169},"f",[156,452,453],{"class":239},"\"",[156,455,456],{"class":211},"{",[156,458,459],{"class":173},"target",[156,461,462],{"class":211},"}",[156,464,465],{"class":239}," exists; pass --overwrite to replace it\"",[156,467,293],{"class":173},[156,469,471,474,477,479,482,484,487,489,492,494,496,498,500],{"class":158,"line":470},27,[156,472,473],{"class":211},"    print",[156,475,476],{"class":173},"(",[156,478,450],{"class":169},[156,480,481],{"class":239},"\"Would copy ",[156,483,456],{"class":211},[156,485,486],{"class":173},"args.source",[156,488,462],{"class":211},[156,490,491],{"class":239}," -> ",[156,493,456],{"class":211},[156,495,459],{"class":173},[156,497,462],{"class":211},[156,499,453],{"class":239},[156,501,293],{"class":173},[156,503,505],{"class":158,"line":504},28,[156,506,195],{"emptyLinePlaceholder":194},[156,508,510,513,516,519,522],{"class":158,"line":509},29,[156,511,512],{"class":169},"if",[156,514,515],{"class":211}," __name__",[156,517,518],{"class":169}," ==",[156,520,521],{"class":239}," \"__main__\"",[156,523,215],{"class":173},[156,525,527],{"class":158,"line":526},30,[156,528,529],{"class":173},"    main()\n",[147,531,535],{"className":532,"code":533,"language":534,"meta":152,"style":152},"language-bash shiki shiki-themes github-light github-dark","$ python mvcp.py notes.txt --dest-dir backup\u002F\nWould copy notes.txt -> backup\u002Fnotes.txt\n\n$ python mvcp.py notes.txt --dest-dir backup\u002F\nmvcp.py: error: backup\u002Fnotes.txt exists; pass --overwrite to replace it\n","bash",[13,536,537,557,576,580,594],{"__ignoreMap":152},[156,538,539,542,545,548,551,554],{"class":158,"line":159},[156,540,541],{"class":204},"$",[156,543,544],{"class":239}," python",[156,546,547],{"class":239}," mvcp.py",[156,549,550],{"class":239}," notes.txt",[156,552,553],{"class":211}," --dest-dir",[156,555,556],{"class":239}," backup\u002F\n",[156,558,559,562,565,567,570,573],{"class":158,"line":166},[156,560,561],{"class":204},"Would",[156,563,564],{"class":239}," copy",[156,566,550],{"class":239},[156,568,569],{"class":173}," -",[156,571,572],{"class":169},">",[156,574,575],{"class":239}," backup\u002Fnotes.txt\n",[156,577,578],{"class":158,"line":177},[156,579,195],{"emptyLinePlaceholder":194},[156,581,582,584,586,588,590,592],{"class":158,"line":191},[156,583,541],{"class":204},[156,585,544],{"class":239},[156,587,547],{"class":239},[156,589,550],{"class":239},[156,591,553],{"class":211},[156,593,556],{"class":239},[156,595,596,599,602,605,608,611,614,617,620,623],{"class":158,"line":198},[156,597,598],{"class":204},"mvcp.py:",[156,600,601],{"class":239}," error:",[156,603,604],{"class":239}," backup\u002Fnotes.txt",[156,606,607],{"class":239}," exists",[156,609,610],{"class":173},"; ",[156,612,613],{"class":204},"pass",[156,615,616],{"class":211}," --overwrite",[156,618,619],{"class":239}," to",[156,621,622],{"class":239}," replace",[156,624,625],{"class":239}," it\n",[10,627,628,629,632,633,635,636,639,640,642,643,646,647,650,651,653,654,657,658,661],{},"Three things are happening. ",[13,630,631],{},"source"," has no leading dash, so it is ",[18,634,44],{}," and\nrequired. ",[13,637,638],{},"--dest-dir"," starts with dashes, so it is ",[18,641,52],{}," and takes a value.\n",[13,644,645],{},"--overwrite"," is a ",[18,648,649],{},"flag"," — ",[13,652,79],{}," means it defaults to ",[13,655,656],{},"False"," and flips\nto ",[13,659,660],{},"True"," when present, taking no value.",[10,663,664,665,668,669,671,672,674,675,678,679,682],{},"Notice ",[13,666,667],{},"type=Path",". ",[13,670,15],{}," calls that callable on the raw string, so ",[13,673,486],{}," is a\n",[13,676,677],{},"pathlib.Path",", not a ",[13,680,681],{},"str",". Any one-argument callable works here, which is the hook you\nwill use for validation below.",[27,684,686],{"id":685},"choices-default-nargs-and-flags","choices, default, nargs, and flags",[10,688,689],{},"These four knobs cover the vast majority of real arguments.",[112,691],{"name":692},"argparse-nargs-matrix",[147,694,696],{"className":149,"code":695,"language":151,"meta":152,"style":152},"parser.add_argument(\n    \"--log-level\",\n    choices=[\"debug\", \"info\", \"warning\", \"error\"],\n    default=\"info\",\n    help=\"Verbosity (default: info).\",\n)\nparser.add_argument(\n    \"paths\",\n    nargs=\"+\",              # one or more, collected into a list\n    type=Path,\n    help=\"One or more files to process.\",\n)\nparser.add_argument(\n    \"--tag\",\n    action=\"append\",        # repeatable: --tag a --tag b -> [\"a\", \"b\"]\n    default=[],\n    help=\"Attach a tag; repeat for several.\",\n)\nparser.add_argument(\"--dry-run\", action=\"store_true\")\n",[13,697,698,703,710,741,752,764,768,772,779,795,804,815,819,823,830,846,855,866,870],{"__ignoreMap":152},[156,699,700],{"class":158,"line":159},[156,701,702],{"class":173},"parser.add_argument(\n",[156,704,705,708],{"class":158,"line":166},[156,706,707],{"class":239},"    \"--log-level\"",[156,709,243],{"class":173},[156,711,712,715,717,720,723,725,728,730,733,735,738],{"class":158,"line":177},[156,713,714],{"class":233},"    choices",[156,716,224],{"class":169},[156,718,719],{"class":173},"[",[156,721,722],{"class":239},"\"debug\"",[156,724,274],{"class":173},[156,726,727],{"class":239},"\"info\"",[156,729,274],{"class":173},[156,731,732],{"class":239},"\"warning\"",[156,734,274],{"class":173},[156,736,737],{"class":239},"\"error\"",[156,739,740],{"class":173},"],\n",[156,742,743,746,748,750],{"class":158,"line":191},[156,744,745],{"class":233},"    default",[156,747,224],{"class":169},[156,749,727],{"class":239},[156,751,243],{"class":173},[156,753,754,757,759,762],{"class":158,"line":198},[156,755,756],{"class":233},"    help",[156,758,224],{"class":169},[156,760,761],{"class":239},"\"Verbosity (default: info).\"",[156,763,243],{"class":173},[156,765,766],{"class":158,"line":218},[156,767,293],{"class":173},[156,769,770],{"class":158,"line":230},[156,771,702],{"class":173},[156,773,774,777],{"class":158,"line":246},[156,775,776],{"class":239},"    \"paths\"",[156,778,243],{"class":173},[156,780,781,784,786,789,792],{"class":158,"line":259},[156,782,783],{"class":233},"    nargs",[156,785,224],{"class":169},[156,787,788],{"class":239},"\"+\"",[156,790,791],{"class":173},",              ",[156,793,794],{"class":162},"# one or more, collected into a list\n",[156,796,797,800,802],{"class":158,"line":265},[156,798,799],{"class":233},"    type",[156,801,224],{"class":169},[156,803,318],{"class":173},[156,805,806,808,810,813],{"class":158,"line":296},[156,807,756],{"class":233},[156,809,224],{"class":169},[156,811,812],{"class":239},"\"One or more files to process.\"",[156,814,243],{"class":173},[156,816,817],{"class":158,"line":302},[156,818,293],{"class":173},[156,820,821],{"class":158,"line":310},[156,822,702],{"class":173},[156,824,825,828],{"class":158,"line":321},[156,826,827],{"class":239},"    \"--tag\"",[156,829,243],{"class":173},[156,831,832,835,837,840,843],{"class":158,"line":332},[156,833,834],{"class":233},"    action",[156,836,224],{"class":169},[156,838,839],{"class":239},"\"append\"",[156,841,842],{"class":173},",        ",[156,844,845],{"class":162},"# repeatable: --tag a --tag b -> [\"a\", \"b\"]\n",[156,847,848,850,852],{"class":158,"line":345},[156,849,745],{"class":233},[156,851,224],{"class":169},[156,853,854],{"class":173},"[],\n",[156,856,857,859,861,864],{"class":158,"line":350},[156,858,756],{"class":233},[156,860,224],{"class":169},[156,862,863],{"class":239},"\"Attach a tag; repeat for several.\"",[156,865,243],{"class":173},[156,867,868],{"class":158,"line":355},[156,869,293],{"class":173},[156,871,872,875,878,880,883,885,887],{"class":158,"line":363},[156,873,874],{"class":173},"parser.add_argument(",[156,876,877],{"class":239},"\"--dry-run\"",[156,879,274],{"class":173},[156,881,882],{"class":233},"action",[156,884,224],{"class":169},[156,886,371],{"class":239},[156,888,293],{"class":173},[32,890,891,907,917,939],{},[35,892,893,898,899,901,902,906],{},[18,894,895],{},[13,896,897],{},"choices"," restricts a value to a fixed set. ",[13,900,15],{}," rejects anything else ",[903,904,905],"em",{},"before","\nyour code runs and lists the valid options in the error, so you never validate the enum by\nhand.",[35,908,909,914,915,57],{},[18,910,911],{},[13,912,913],{},"default"," supplies a value when the flag is absent. Optionals without a default get\n",[13,916,212],{},[35,918,919,923,924,926,927,930,931,934,935,938],{},[18,920,921],{},[13,922,130],{}," controls how many values an argument consumes: ",[13,925,788],{}," (one or more), ",[13,928,929],{},"\"*\"","\n(zero or more), ",[13,932,933],{},"\"?\""," (optional single), or an integer for an exact count. ",[13,936,937],{},"nargs=\"+\""," on\na positional is how you accept a list of files.",[35,940,941,945,946,949],{},[18,942,943],{},[13,944,79],{}," for boolean switches, ",[13,947,948],{},"action=\"append\""," to collect a repeatable\noption into a list.",[27,951,953],{"id":952},"validation-with-type-callables-and-parsererror","Validation with type= callables and parser.error()",[10,955,956,958,959,49,962,965,966,969,970,973,974,976],{},[13,957,63],{}," is not just for ",[13,960,961],{},"int",[13,963,964],{},"Path"," — any callable that takes a string and either\nreturns a value or raises is fair game. Raise ",[13,967,968],{},"argparse.ArgumentTypeError"," (or ",[13,971,972],{},"ValueError",")\nand ",[13,975,15],{}," turns it into a clean, non-zero-exit error message instead of a traceback.",[112,978],{"name":979},"argparse-type-validation-flow",[147,981,983],{"className":149,"code":982,"language":151,"meta":152,"style":152},"import argparse\n\ndef positive_int(raw: str) -> int:\n    value = int(raw)              # ValueError here is caught by argparse too\n    if value \u003C= 0:\n        raise argparse.ArgumentTypeError(f\"{raw!r} is not a positive integer\")\n    return value\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--workers\", type=positive_int, default=4)\n",[13,984,985,991,995,1014,1030,1045,1072,1080,1084,1094],{"__ignoreMap":152},[156,986,987,989],{"class":158,"line":159},[156,988,170],{"class":169},[156,990,174],{"class":173},[156,992,993],{"class":158,"line":166},[156,994,195],{"emptyLinePlaceholder":194},[156,996,997,999,1002,1005,1007,1010,1012],{"class":158,"line":177},[156,998,201],{"class":169},[156,1000,1001],{"class":204}," positive_int",[156,1003,1004],{"class":173},"(raw: ",[156,1006,681],{"class":211},[156,1008,1009],{"class":173},") -> ",[156,1011,961],{"class":211},[156,1013,215],{"class":173},[156,1015,1016,1019,1021,1024,1027],{"class":158,"line":191},[156,1017,1018],{"class":173},"    value ",[156,1020,224],{"class":169},[156,1022,1023],{"class":211}," int",[156,1025,1026],{"class":173},"(raw)              ",[156,1028,1029],{"class":162},"# ValueError here is caught by argparse too\n",[156,1031,1032,1034,1037,1040,1043],{"class":158,"line":198},[156,1033,429],{"class":169},[156,1035,1036],{"class":173}," value ",[156,1038,1039],{"class":169},"\u003C=",[156,1041,1042],{"class":211}," 0",[156,1044,215],{"class":173},[156,1046,1047,1050,1053,1055,1057,1059,1062,1065,1067,1070],{"class":158,"line":218},[156,1048,1049],{"class":169},"        raise",[156,1051,1052],{"class":173}," argparse.ArgumentTypeError(",[156,1054,450],{"class":169},[156,1056,453],{"class":239},[156,1058,456],{"class":211},[156,1060,1061],{"class":173},"raw",[156,1063,1064],{"class":169},"!r",[156,1066,462],{"class":211},[156,1068,1069],{"class":239}," is not a positive integer\"",[156,1071,293],{"class":173},[156,1073,1074,1077],{"class":158,"line":230},[156,1075,1076],{"class":169},"    return",[156,1078,1079],{"class":173}," value\n",[156,1081,1082],{"class":158,"line":246},[156,1083,195],{"emptyLinePlaceholder":194},[156,1085,1086,1089,1091],{"class":158,"line":259},[156,1087,1088],{"class":173},"parser ",[156,1090,224],{"class":169},[156,1092,1093],{"class":173}," argparse.ArgumentParser()\n",[156,1095,1096,1098,1101,1103,1105,1107,1110,1112,1114,1117],{"class":158,"line":265},[156,1097,874],{"class":173},[156,1099,1100],{"class":239},"\"--workers\"",[156,1102,274],{"class":173},[156,1104,277],{"class":233},[156,1106,224],{"class":169},[156,1108,1109],{"class":173},"positive_int, ",[156,1111,913],{"class":233},[156,1113,224],{"class":169},[156,1115,1116],{"class":211},"4",[156,1118,293],{"class":173},[147,1120,1122],{"className":532,"code":1121,"language":534,"meta":152,"style":152},"$ python app.py --workers 0\napp.py: error: argument --workers: '0' is not a positive integer\n",[13,1123,1124,1139],{"__ignoreMap":152},[156,1125,1126,1128,1130,1133,1136],{"class":158,"line":159},[156,1127,541],{"class":204},[156,1129,544],{"class":239},[156,1131,1132],{"class":239}," app.py",[156,1134,1135],{"class":211}," --workers",[156,1137,1138],{"class":211}," 0\n",[156,1140,1141,1144,1146,1149,1152,1155,1158,1160,1163,1166],{"class":158,"line":166},[156,1142,1143],{"class":204},"app.py:",[156,1145,601],{"class":239},[156,1147,1148],{"class":239}," argument",[156,1150,1151],{"class":211}," --workers:",[156,1153,1154],{"class":239}," '0'",[156,1156,1157],{"class":239}," is",[156,1159,438],{"class":239},[156,1161,1162],{"class":239}," a",[156,1164,1165],{"class":239}," positive",[156,1167,1168],{"class":239}," integer\n",[10,1170,1171,1172,1175,1176,1179,1180,1183,1184,1187],{},"For validation that spans ",[903,1173,1174],{},"several"," arguments (say, \"",[13,1177,1178],{},"--end"," must be after ",[13,1181,1182],{},"--start","\"), do\nit after parsing and report failures through ",[13,1185,1186],{},"parser.error()",", which prints to stderr and\nexits with status 2 — the conventional argparse usage-error code:",[147,1189,1191],{"className":149,"code":1190,"language":151,"meta":152,"style":152},"args = parser.parse_args()\nif args.end \u003C= args.start:\n    parser.error(\"--end must be later than --start\")\n",[13,1192,1193,1202,1214],{"__ignoreMap":152},[156,1194,1195,1198,1200],{"class":158,"line":159},[156,1196,1197],{"class":173},"args ",[156,1199,224],{"class":169},[156,1201,406],{"class":173},[156,1203,1204,1206,1209,1211],{"class":158,"line":166},[156,1205,512],{"class":169},[156,1207,1208],{"class":173}," args.end ",[156,1210,1039],{"class":169},[156,1212,1213],{"class":173}," args.start:\n",[156,1215,1216,1219,1222],{"class":158,"line":177},[156,1217,1218],{"class":173},"    parser.error(",[156,1220,1221],{"class":239},"\"--end must be later than --start\"",[156,1223,293],{"class":173},[10,1225,1226,1227,1229,1230,1233,1234,57],{},"Using ",[13,1228,1186],{}," rather than ",[13,1231,1232],{},"print(); sys.exit()"," keeps your error output consistent\nwith the parser's own messages. For the broader picture of exit statuses, see\n",[94,1235,1237],{"href":1236},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",[27,1239,1241],{"id":1240},"subcommands-a-first-look","Subcommands: a first look",[10,1243,1244,1245,274,1248,274,1251,1254,1255,1257,1258,1261],{},"Once a tool does more than one job — ",[13,1246,1247],{},"tool build",[13,1249,1250],{},"tool deploy",[13,1252,1253],{},"tool clean"," — you want\nsubcommands, each with its own arguments and help. ",[13,1256,15],{}," provides these through\n",[13,1259,1260],{},"add_subparsers()",":",[147,1263,1265],{"className":149,"code":1264,"language":151,"meta":152,"style":152},"parser = argparse.ArgumentParser(prog=\"tool\")\nsub = parser.add_subparsers(dest=\"command\", required=True)\n\nbuild = sub.add_parser(\"build\", help=\"Build the project.\")\nbuild.add_argument(\"--release\", action=\"store_true\")\n\ndeploy = sub.add_parser(\"deploy\", help=\"Deploy the project.\")\ndeploy.add_argument(\"target\")\n\nargs = parser.parse_args()\n",[13,1266,1267,1286,1315,1319,1343,1361,1365,1388,1398,1402],{"__ignoreMap":152},[156,1268,1269,1271,1273,1276,1279,1281,1284],{"class":158,"line":159},[156,1270,1088],{"class":173},[156,1272,224],{"class":169},[156,1274,1275],{"class":173}," argparse.ArgumentParser(",[156,1277,1278],{"class":233},"prog",[156,1280,224],{"class":169},[156,1282,1283],{"class":239},"\"tool\"",[156,1285,293],{"class":173},[156,1287,1288,1291,1293,1296,1299,1301,1304,1306,1309,1311,1313],{"class":158,"line":166},[156,1289,1290],{"class":173},"sub ",[156,1292,224],{"class":169},[156,1294,1295],{"class":173}," parser.add_subparsers(",[156,1297,1298],{"class":233},"dest",[156,1300,224],{"class":169},[156,1302,1303],{"class":239},"\"command\"",[156,1305,274],{"class":173},[156,1307,1308],{"class":233},"required",[156,1310,224],{"class":169},[156,1312,660],{"class":211},[156,1314,293],{"class":173},[156,1316,1317],{"class":158,"line":177},[156,1318,195],{"emptyLinePlaceholder":194},[156,1320,1321,1324,1326,1329,1332,1334,1336,1338,1341],{"class":158,"line":191},[156,1322,1323],{"class":173},"build ",[156,1325,224],{"class":169},[156,1327,1328],{"class":173}," sub.add_parser(",[156,1330,1331],{"class":239},"\"build\"",[156,1333,274],{"class":173},[156,1335,285],{"class":233},[156,1337,224],{"class":169},[156,1339,1340],{"class":239},"\"Build the project.\"",[156,1342,293],{"class":173},[156,1344,1345,1348,1351,1353,1355,1357,1359],{"class":158,"line":198},[156,1346,1347],{"class":173},"build.add_argument(",[156,1349,1350],{"class":239},"\"--release\"",[156,1352,274],{"class":173},[156,1354,882],{"class":233},[156,1356,224],{"class":169},[156,1358,371],{"class":239},[156,1360,293],{"class":173},[156,1362,1363],{"class":158,"line":218},[156,1364,195],{"emptyLinePlaceholder":194},[156,1366,1367,1370,1372,1374,1377,1379,1381,1383,1386],{"class":158,"line":230},[156,1368,1369],{"class":173},"deploy ",[156,1371,224],{"class":169},[156,1373,1328],{"class":173},[156,1375,1376],{"class":239},"\"deploy\"",[156,1378,274],{"class":173},[156,1380,285],{"class":233},[156,1382,224],{"class":169},[156,1384,1385],{"class":239},"\"Deploy the project.\"",[156,1387,293],{"class":173},[156,1389,1390,1393,1396],{"class":158,"line":246},[156,1391,1392],{"class":173},"deploy.add_argument(",[156,1394,1395],{"class":239},"\"target\"",[156,1397,293],{"class":173},[156,1399,1400],{"class":158,"line":259},[156,1401,195],{"emptyLinePlaceholder":194},[156,1403,1404,1406,1408],{"class":158,"line":265},[156,1405,1197],{"class":173},[156,1407,224],{"class":169},[156,1409,406],{"class":173},[10,1411,1412,1413,49,1416,1419,1420,1423,1424,57],{},"That is enough to give ",[13,1414,1415],{},"tool build --release",[13,1417,1418],{},"tool deploy prod"," their own parsers. The\nclean way to route each subcommand to a handler function — with ",[13,1421,1422],{},"set_defaults(func=...)",",\nshared parent parsers, and nesting — is a topic of its own:\n",[94,1425,1426],{"href":96},"argparse subparsers for subcommands",[27,1428,1430],{"id":1429},"help-output-for-free","Help output for free",[10,1432,1433,1434,668,1437,1439,1440,274,1442,1445,1446,1448,1449,420,1452,1454],{},"You never write ",[13,1435,1436],{},"--help",[13,1438,15],{}," builds usage text from your ",[13,1441,1278],{},[13,1443,1444],{},"description",", and\nevery argument's ",[13,1447,285],{}," string, and wires up ",[13,1450,1451],{},"-h",[13,1453,1436],{}," automatically:",[147,1456,1458],{"className":532,"code":1457,"language":534,"meta":152,"style":152},"$ python mvcp.py --help\nusage: mvcp [-h] [--dest-dir DEST_DIR] [--overwrite] source\n\nCopy a file, optionally renaming it.\n\npositional arguments:\n  source               File to copy.\n\noptions:\n  -h, --help           show this help message and exit\n  --dest-dir DEST_DIR  Directory to copy into (default: current directory).\n  --overwrite          Replace the destination if it already exists.\n",[13,1459,1460,1471,1482,1486,1505,1509,1516,1529,1533,1538,1564,1594],{"__ignoreMap":152},[156,1461,1462,1464,1466,1468],{"class":158,"line":159},[156,1463,541],{"class":204},[156,1465,544],{"class":239},[156,1467,547],{"class":239},[156,1469,1470],{"class":211}," --help\n",[156,1472,1473,1476,1479],{"class":158,"line":166},[156,1474,1475],{"class":204},"usage:",[156,1477,1478],{"class":239}," mvcp",[156,1480,1481],{"class":173}," [-h] [--dest-dir DEST_DIR] [--overwrite] source\n",[156,1483,1484],{"class":158,"line":177},[156,1485,195],{"emptyLinePlaceholder":194},[156,1487,1488,1491,1493,1496,1499,1502],{"class":158,"line":191},[156,1489,1490],{"class":204},"Copy",[156,1492,1162],{"class":239},[156,1494,1495],{"class":239}," file,",[156,1497,1498],{"class":239}," optionally",[156,1500,1501],{"class":239}," renaming",[156,1503,1504],{"class":239}," it.\n",[156,1506,1507],{"class":158,"line":198},[156,1508,195],{"emptyLinePlaceholder":194},[156,1510,1511,1513],{"class":158,"line":218},[156,1512,44],{"class":204},[156,1514,1515],{"class":239}," arguments:\n",[156,1517,1518,1521,1524,1526],{"class":158,"line":230},[156,1519,1520],{"class":211},"  source",[156,1522,1523],{"class":239},"               File",[156,1525,619],{"class":239},[156,1527,1528],{"class":239}," copy.\n",[156,1530,1531],{"class":158,"line":246},[156,1532,195],{"emptyLinePlaceholder":194},[156,1534,1535],{"class":158,"line":259},[156,1536,1537],{"class":204},"options:\n",[156,1539,1540,1543,1546,1549,1552,1555,1558,1561],{"class":158,"line":265},[156,1541,1542],{"class":204},"  -h,",[156,1544,1545],{"class":211}," --help",[156,1547,1548],{"class":239},"           show",[156,1550,1551],{"class":239}," this",[156,1553,1554],{"class":239}," help",[156,1556,1557],{"class":239}," message",[156,1559,1560],{"class":239}," and",[156,1562,1563],{"class":239}," exit\n",[156,1565,1566,1569,1572,1575,1577,1579,1582,1585,1588,1591],{"class":158,"line":296},[156,1567,1568],{"class":204},"  --dest-dir",[156,1570,1571],{"class":239}," DEST_DIR",[156,1573,1574],{"class":239},"  Directory",[156,1576,619],{"class":239},[156,1578,564],{"class":239},[156,1580,1581],{"class":239}," into",[156,1583,1584],{"class":173}," (default: ",[156,1586,1587],{"class":239},"current",[156,1589,1590],{"class":239}," directory",[156,1592,1593],{"class":173},").\n",[156,1595,1596,1599,1602,1605,1608,1611,1614,1617],{"class":158,"line":302},[156,1597,1598],{"class":204},"  --overwrite",[156,1600,1601],{"class":239},"          Replace",[156,1603,1604],{"class":239}," the",[156,1606,1607],{"class":239}," destination",[156,1609,1610],{"class":239}," if",[156,1612,1613],{"class":239}," it",[156,1615,1616],{"class":239}," already",[156,1618,1619],{"class":239}," exists.\n",[10,1621,1622,1623,1626,1627,1630],{},"Add an ",[13,1624,1625],{},"epilog="," for examples, and set ",[13,1628,1629],{},"formatter_class=argparse.RawDescriptionHelpFormatter","\nif you want to control the wrapping of your description yourself.",[27,1632,1634],{"id":1633},"when-to-graduate-to-click-or-typer","When to graduate to Click or Typer",[10,1636,1637,1639],{},[13,1638,15],{}," starts to fight you at a predictable point. The trade-offs:",[112,1641],{"name":1642},"argparse-graduate-decision",[1644,1645,1646,1661],"table",{},[1647,1648,1649],"thead",{},[1650,1651,1652,1656,1658],"tr",{},[1653,1654,1655],"th",{},"Concern",[1653,1657,15],{},[1653,1659,1660],{},"Click \u002F Typer",[1662,1663,1664,1676,1687,1698,1712,1723],"tbody",{},[1650,1665,1666,1670,1673],{},[1667,1668,1669],"td",{},"Dependencies",[1667,1671,1672],{},"None (stdlib)",[1667,1674,1675],{},"One framework",[1650,1677,1678,1681,1684],{},[1667,1679,1680],{},"Boilerplate",[1667,1682,1683],{},"High — every arg spelled out",[1667,1685,1686],{},"Low, especially with Typer's type hints",[1650,1688,1689,1692,1695],{},[1667,1690,1691],{},"Nested subcommands",[1667,1693,1694],{},"Manual and verbose",[1667,1696,1697],{},"First-class groups",[1650,1699,1700,1703,1706],{},[1667,1701,1702],{},"Shared context between commands",[1667,1704,1705],{},"Roll your own",[1667,1707,1708,1711],{},[13,1709,1710],{},"ctx.obj"," \u002F dependency injection",[1650,1713,1714,1717,1720],{},[1667,1715,1716],{},"Shell completion",[1667,1718,1719],{},"Not built in",[1667,1721,1722],{},"Built in",[1650,1724,1725,1728,1731],{},[1667,1726,1727],{},"Rich help, colors, prompts",[1667,1729,1730],{},"Manual",[1667,1732,1733],{},"Included",[10,1735,1736,1737,1739,1740,1743,1744,1748,1749,57],{},"If your tool is a handful of commands with simple options, ",[13,1738,15],{}," is the right tool and\nadding a dependency is over-engineering. Once you find yourself hand-rolling subcommand\ndispatch, wanting tab completion, or copy-pasting the same global options onto every\ncommand, a framework pays for itself. Start with\n",[94,1741,1742],{"href":104},"Typer vs Click: when to use each","\nto pick one, then either\n",[94,1745,1747],{"href":1746},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click\u002F","build subcommands in Click","\nor follow the ",[94,1750,1751],{"href":109},"migration path to Typer",[27,1753,1755],{"id":1754},"production-notes","Production notes",[32,1757,1758,1775,1792,1809,1822],{},[35,1759,1760,1763,1764,1766,1767,1770,1771,1774],{},[18,1761,1762],{},"Namespace is intentionally dumb."," ",[13,1765,84],{}," returns an ",[13,1768,1769],{},"argparse.Namespace"," with\nno validation of its own. For a typed object, feed it into a dataclass:\n",[13,1772,1773],{},"Config(**vars(args))",". That gives you editor autocompletion and mypy coverage downstream.",[35,1776,1777,1780,1781,1784,1785,1788,1789,1791],{},[18,1778,1779],{},"Test without a subprocess."," Call ",[13,1782,1783],{},"parser.parse_args([\"notes.txt\", \"--overwrite\"])"," with\nan explicit list in unit tests — it reads ",[13,1786,1787],{},"sys.argv"," only when you pass ",[13,1790,212],{},". Assert on\nthe returned namespace directly.",[35,1793,1794,1763,1797,1799,1800,668,1803,1805,1806,57],{},[18,1795,1796],{},"Hyphens become underscores.",[13,1798,638],{}," is available as ",[13,1801,1802],{},"args.dest_dir",[13,1804,15],{},"\ntranslates automatically; do not look for ",[13,1807,1808],{},"args[\"dest-dir\"]",[35,1810,1811,1763,1814,1816,1817,1821],{},[18,1812,1813],{},"Exit code 2 for usage errors.",[13,1815,1186],{}," and unknown-argument failures exit with\nstatus 2, a convention worth preserving if you later migrate. See\n",[94,1818,1820],{"href":1819},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002F","structuring multi-command Python CLIs","\nfor keeping parsing separate from logic so a future framework swap stays cheap.",[35,1823,1824,1829,1830,1833],{},[18,1825,1826],{},[13,1827,1828],{},"parse_known_args()"," returns a ",[13,1831,1832],{},"(namespace, leftovers)"," tuple when you need to forward\nunrecognized flags to a wrapped tool, rather than erroring on them.",[27,1835,1837],{"id":1836},"argument-groups-prefixes-and-the-details-that-show","Argument groups, prefixes and the details that show",[10,1839,1840,1841,1843],{},"Beyond the basics, four ",[13,1842,15],{}," features do most of the work of making a stdlib CLI feel\nfinished.",[10,1845,1846,1849,1850,1852],{},[18,1847,1848],{},"Argument groups"," organise ",[13,1851,1436],{}," into sections, which is the difference between a readable\nhelp screen and a list of thirty flags:",[147,1854,1856],{"className":149,"code":1855,"language":151,"meta":152,"style":152},"output = parser.add_argument_group(\"output options\")\noutput.add_argument(\"--json\", action=\"store_true\", help=\"Emit machine-readable output.\")\noutput.add_argument(\"--quiet\", \"-q\", action=\"store_true\", help=\"Only report failures.\")\n",[13,1857,1858,1873,1900],{"__ignoreMap":152},[156,1859,1860,1863,1865,1868,1871],{"class":158,"line":159},[156,1861,1862],{"class":173},"output ",[156,1864,224],{"class":169},[156,1866,1867],{"class":173}," parser.add_argument_group(",[156,1869,1870],{"class":239},"\"output options\"",[156,1872,293],{"class":173},[156,1874,1875,1878,1881,1883,1885,1887,1889,1891,1893,1895,1898],{"class":158,"line":166},[156,1876,1877],{"class":173},"output.add_argument(",[156,1879,1880],{"class":239},"\"--json\"",[156,1882,274],{"class":173},[156,1884,882],{"class":233},[156,1886,224],{"class":169},[156,1888,371],{"class":239},[156,1890,274],{"class":173},[156,1892,285],{"class":233},[156,1894,224],{"class":169},[156,1896,1897],{"class":239},"\"Emit machine-readable output.\"",[156,1899,293],{"class":173},[156,1901,1902,1904,1907,1909,1912,1914,1916,1918,1920,1922,1924,1926,1929],{"class":158,"line":177},[156,1903,1877],{"class":173},[156,1905,1906],{"class":239},"\"--quiet\"",[156,1908,274],{"class":173},[156,1910,1911],{"class":239},"\"-q\"",[156,1913,274],{"class":173},[156,1915,882],{"class":233},[156,1917,224],{"class":169},[156,1919,371],{"class":239},[156,1921,274],{"class":173},[156,1923,285],{"class":233},[156,1925,224],{"class":169},[156,1927,1928],{"class":239},"\"Only report failures.\"",[156,1930,293],{"class":173},[10,1932,1933,1936],{},[18,1934,1935],{},"Mutually exclusive groups"," express a rule the parser can enforce, so you do not check it in the\nbody:",[147,1938,1940],{"className":149,"code":1939,"language":151,"meta":152,"style":152},"mode = parser.add_mutually_exclusive_group()\nmode.add_argument(\"--fast\", action=\"store_true\", help=\"Skip verification.\")\nmode.add_argument(\"--thorough\", action=\"store_true\", help=\"Verify every file.\")\n",[13,1941,1942,1952,1979],{"__ignoreMap":152},[156,1943,1944,1947,1949],{"class":158,"line":159},[156,1945,1946],{"class":173},"mode ",[156,1948,224],{"class":169},[156,1950,1951],{"class":173}," parser.add_mutually_exclusive_group()\n",[156,1953,1954,1957,1960,1962,1964,1966,1968,1970,1972,1974,1977],{"class":158,"line":166},[156,1955,1956],{"class":173},"mode.add_argument(",[156,1958,1959],{"class":239},"\"--fast\"",[156,1961,274],{"class":173},[156,1963,882],{"class":233},[156,1965,224],{"class":169},[156,1967,371],{"class":239},[156,1969,274],{"class":173},[156,1971,285],{"class":233},[156,1973,224],{"class":169},[156,1975,1976],{"class":239},"\"Skip verification.\"",[156,1978,293],{"class":173},[156,1980,1981,1983,1986,1988,1990,1992,1994,1996,1998,2000,2003],{"class":158,"line":177},[156,1982,1956],{"class":173},[156,1984,1985],{"class":239},"\"--thorough\"",[156,1987,274],{"class":173},[156,1989,882],{"class":233},[156,1991,224],{"class":169},[156,1993,371],{"class":239},[156,1995,274],{"class":173},[156,1997,285],{"class":233},[156,1999,224],{"class":169},[156,2001,2002],{"class":239},"\"Verify every file.\"",[156,2004,293],{"class":173},[10,2006,2007,2012,2013,2016],{},[18,2008,2009],{},[13,2010,2011],{},"argparse.SUPPRESS"," is the key to layered configuration. An option with\n",[13,2014,2015],{},"default=argparse.SUPPRESS"," simply does not appear in the namespace when the user did not pass it,\nwhich is what lets a config file supply the value instead:",[147,2018,2020],{"className":149,"code":2019,"language":151,"meta":152,"style":152},"parser.add_argument(\"--retries\", type=int, default=argparse.SUPPRESS)\noverrides = vars(parser.parse_args())        # only the keys the user actually set\nsettings = {**defaults, **from_file, **from_env, **overrides}\n",[13,2021,2022,2051,2067],{"__ignoreMap":152},[156,2023,2024,2026,2029,2031,2033,2035,2037,2039,2041,2043,2046,2049],{"class":158,"line":159},[156,2025,874],{"class":173},[156,2027,2028],{"class":239},"\"--retries\"",[156,2030,274],{"class":173},[156,2032,277],{"class":233},[156,2034,224],{"class":169},[156,2036,961],{"class":211},[156,2038,274],{"class":173},[156,2040,913],{"class":233},[156,2042,224],{"class":169},[156,2044,2045],{"class":173},"argparse.",[156,2047,2048],{"class":211},"SUPPRESS",[156,2050,293],{"class":173},[156,2052,2053,2056,2058,2061,2064],{"class":158,"line":166},[156,2054,2055],{"class":173},"overrides ",[156,2057,224],{"class":169},[156,2059,2060],{"class":211}," vars",[156,2062,2063],{"class":173},"(parser.parse_args())        ",[156,2065,2066],{"class":162},"# only the keys the user actually set\n",[156,2068,2069,2072,2074,2077,2080,2083,2085,2088,2090,2093,2095],{"class":158,"line":177},[156,2070,2071],{"class":173},"settings ",[156,2073,224],{"class":169},[156,2075,2076],{"class":173}," {",[156,2078,2079],{"class":169},"**",[156,2081,2082],{"class":173},"defaults, ",[156,2084,2079],{"class":169},[156,2086,2087],{"class":173},"from_file, ",[156,2089,2079],{"class":169},[156,2091,2092],{"class":173},"from_env, ",[156,2094,2079],{"class":169},[156,2096,2097],{"class":173},"overrides}\n",[10,2099,2100,2101,2104,2105,2107],{},"Without it, a parser default of ",[13,2102,2103],{},"3"," is indistinguishable from the user typing ",[13,2106,2103],{},", and the config\nfile can never win.",[10,2109,2110,2118,2119,2121,2122,2125,2126,2128,2129,2132,2133,2136],{},[18,2111,2112,49,2115],{},[13,2113,2114],{},"prefix_chars",[13,2116,2117],{},"allow_abbrev"," are worth one decision each. Leave ",[13,2120,2114],{}," alone.\nTurn ",[13,2123,2124],{},"allow_abbrev=False"," on: by default ",[13,2127,15],{}," accepts any unambiguous prefix, so ",[13,2130,2131],{},"--ret","\nworks today and breaks the moment you add ",[13,2134,2135],{},"--retry-delay",". Every script using the short form\nbreaks at once, for a convenience nobody asked for.",[27,2138,2140],{"id":2139},"better-error-messages-and-help-output","Better error messages and help output",[10,2142,2143,2145],{},[13,2144,15],{}," gives you three levers, and using them costs a few lines.",[147,2147,2149],{"className":149,"code":2148,"language":151,"meta":152,"style":152},"parser = argparse.ArgumentParser(\n    prog=\"mytool\",\n    description=\"Sync a directory to a bucket.\",\n    epilog=(\n        \"examples:\\n\"\n        \"  mytool sync .\u002Fdata --retries 5\\n\"\n        \"  mytool sync .\u002Fdata --dry-run | tee plan.txt\\n\"\n    ),\n    formatter_class=argparse.RawDescriptionHelpFormatter,\n    allow_abbrev=False,\n)\n",[13,2150,2151,2159,2171,2183,2193,2204,2213,2222,2227,2237,2248],{"__ignoreMap":152},[156,2152,2153,2155,2157],{"class":158,"line":159},[156,2154,1088],{"class":173},[156,2156,224],{"class":169},[156,2158,227],{"class":173},[156,2160,2161,2164,2166,2169],{"class":158,"line":166},[156,2162,2163],{"class":233},"    prog",[156,2165,224],{"class":169},[156,2167,2168],{"class":239},"\"mytool\"",[156,2170,243],{"class":173},[156,2172,2173,2176,2178,2181],{"class":158,"line":177},[156,2174,2175],{"class":233},"    description",[156,2177,224],{"class":169},[156,2179,2180],{"class":239},"\"Sync a directory to a bucket.\"",[156,2182,243],{"class":173},[156,2184,2185,2188,2190],{"class":158,"line":191},[156,2186,2187],{"class":233},"    epilog",[156,2189,224],{"class":169},[156,2191,2192],{"class":173},"(\n",[156,2194,2195,2198,2201],{"class":158,"line":198},[156,2196,2197],{"class":239},"        \"examples:",[156,2199,2200],{"class":211},"\\n",[156,2202,2203],{"class":239},"\"\n",[156,2205,2206,2209,2211],{"class":158,"line":218},[156,2207,2208],{"class":239},"        \"  mytool sync .\u002Fdata --retries 5",[156,2210,2200],{"class":211},[156,2212,2203],{"class":239},[156,2214,2215,2218,2220],{"class":158,"line":230},[156,2216,2217],{"class":239},"        \"  mytool sync .\u002Fdata --dry-run | tee plan.txt",[156,2219,2200],{"class":211},[156,2221,2203],{"class":239},[156,2223,2224],{"class":158,"line":246},[156,2225,2226],{"class":173},"    ),\n",[156,2228,2229,2232,2234],{"class":158,"line":259},[156,2230,2231],{"class":233},"    formatter_class",[156,2233,224],{"class":169},[156,2235,2236],{"class":173},"argparse.RawDescriptionHelpFormatter,\n",[156,2238,2239,2242,2244,2246],{"class":158,"line":265},[156,2240,2241],{"class":233},"    allow_abbrev",[156,2243,224],{"class":169},[156,2245,656],{"class":211},[156,2247,243],{"class":173},[156,2249,2250],{"class":158,"line":296},[156,2251,293],{"class":173},[10,2253,2254,2257,2258,2261,2262,2264],{},[13,2255,2256],{},"RawDescriptionHelpFormatter"," preserves the newlines in your epilog, which is the only way to get\nreadable examples. ",[13,2259,2260],{},"ArgumentDefaultsHelpFormatter"," appends the default to each help string\nautomatically — useful, though it fights with ",[13,2263,2048],{},", so pick one approach per parser.",[10,2266,2267,2268,2270],{},"For custom validation, ",[13,2269,1186],{}," is the function that produces the conventional behaviour:\nusage line, message, exit code 2.",[147,2272,2274],{"className":149,"code":2273,"language":151,"meta":152,"style":152},"def positive_int(raw: str) -> int:\n    value = int(raw)                     # ValueError here becomes a usage error automatically\n    if value \u003C 1:\n        raise argparse.ArgumentTypeError(\"must be 1 or greater\")\n    return value\n",[13,2275,2276,2292,2306,2320,2331],{"__ignoreMap":152},[156,2277,2278,2280,2282,2284,2286,2288,2290],{"class":158,"line":159},[156,2279,201],{"class":169},[156,2281,1001],{"class":204},[156,2283,1004],{"class":173},[156,2285,681],{"class":211},[156,2287,1009],{"class":173},[156,2289,961],{"class":211},[156,2291,215],{"class":173},[156,2293,2294,2296,2298,2300,2303],{"class":158,"line":166},[156,2295,1018],{"class":173},[156,2297,224],{"class":169},[156,2299,1023],{"class":211},[156,2301,2302],{"class":173},"(raw)                     ",[156,2304,2305],{"class":162},"# ValueError here becomes a usage error automatically\n",[156,2307,2308,2310,2312,2315,2318],{"class":158,"line":177},[156,2309,429],{"class":169},[156,2311,1036],{"class":173},[156,2313,2314],{"class":169},"\u003C",[156,2316,2317],{"class":211}," 1",[156,2319,215],{"class":173},[156,2321,2322,2324,2326,2329],{"class":158,"line":191},[156,2323,1049],{"class":169},[156,2325,1052],{"class":173},[156,2327,2328],{"class":239},"\"must be 1 or greater\"",[156,2330,293],{"class":173},[156,2332,2333,2335],{"class":158,"line":198},[156,2334,1076],{"class":169},[156,2336,1079],{"class":173},[10,2338,2339,2340,2343,2344,2346,2347,2349,2350,2352],{},"Raising ",[13,2341,2342],{},"ArgumentTypeError"," from a ",[13,2345,63],{}," callable is the idiomatic form — ",[13,2348,15],{}," catches it,\nprefixes the option name, and exits 2. Raising a bare ",[13,2351,972],{}," works too but produces a\nslightly less specific message.",[27,2354,2356],{"id":2355},"what-you-end-up-writing-yourself","What you end up writing yourself",[10,2358,2359],{},"Knowing the gaps is what makes the argparse-or-framework decision concrete. Four things are\nmissing, and each is a known quantity rather than a mystery.",[10,2361,2362,2365,2366,2369,2370,2372],{},[18,2363,2364],{},"Shell completion."," There is no built-in support. ",[13,2367,2368],{},"argcomplete"," covers it with one decorator and\na registration step, but it is a third-party dependency — which, if the reason you chose\n",[13,2371,15],{}," was to avoid dependencies, is worth noticing.",[10,2374,2375,1763,2378,2381],{},[18,2376,2377],{},"Dispatch.",[13,2379,2380],{},"set_defaults(func=handler)"," is the standard pattern and it works well, but you write\nit, and you write the \"no subcommand given\" branch yourself:",[147,2383,2385],{"className":149,"code":2384,"language":151,"meta":152,"style":152},"args = parser.parse_args()\nif not hasattr(args, \"func\"):\n    parser.print_help()\n    raise SystemExit(2)\nraise SystemExit(args.func(args))\n",[13,2386,2387,2395,2413,2418,2433],{"__ignoreMap":152},[156,2388,2389,2391,2393],{"class":158,"line":159},[156,2390,1197],{"class":173},[156,2392,224],{"class":169},[156,2394,406],{"class":173},[156,2396,2397,2399,2401,2404,2407,2410],{"class":158,"line":166},[156,2398,512],{"class":169},[156,2400,438],{"class":169},[156,2402,2403],{"class":211}," hasattr",[156,2405,2406],{"class":173},"(args, ",[156,2408,2409],{"class":239},"\"func\"",[156,2411,2412],{"class":173},"):\n",[156,2414,2415],{"class":158,"line":177},[156,2416,2417],{"class":173},"    parser.print_help()\n",[156,2419,2420,2423,2426,2428,2431],{"class":158,"line":191},[156,2421,2422],{"class":169},"    raise",[156,2424,2425],{"class":211}," SystemExit",[156,2427,476],{"class":173},[156,2429,2430],{"class":211},"2",[156,2432,293],{"class":173},[156,2434,2435,2438,2440],{"class":158,"line":198},[156,2436,2437],{"class":169},"raise",[156,2439,2425],{"class":211},[156,2441,2442],{"class":173},"(args.func(args))\n",[10,2444,2445,2448,2449,2452],{},[18,2446,2447],{},"Shared setup."," There is no equivalent of a group callback that runs before every subcommand, so\nconfiguration loading and logging setup go in ",[13,2450,2451],{},"main()"," before dispatch — which is fine, and is one\nmore thing to remember when adding a command.",[10,2454,2455,2458,2459,2461],{},[18,2456,2457],{},"Context."," Nothing threads shared state through the tree, so it travels in the ",[13,2460,88],{}," or in\na variable you pass to the handler. Explicit, and slightly more code at every level.",[10,2463,2464],{},"None of these is hard. Together they are perhaps two hundred lines you own and maintain, which is\na reasonable trade for zero dependencies and a poor one if you never needed that constraint.",[27,2466,2468],{"id":2467},"structuring-an-argparse-cli-that-grows","Structuring an argparse CLI that grows",[10,2470,2471,2472,2474],{},"The stdlib gives you no opinion about layout, which means the discipline has to come from you.\nThree conventions keep an ",[13,2473,15],{}," tool from turning into one long module.",[10,2476,2477,2480,2481,2484,2485,2488],{},[18,2478,2479],{},"Build the parser in a function."," A module-level ",[13,2482,2483],{},"parser = ArgumentParser(...)"," runs at import\ntime and cannot be tested without side effects. A ",[13,2486,2487],{},"build_parser()"," that returns the parser is\nimportable, testable and reusable:",[147,2490,2492],{"className":149,"code":2491,"language":151,"meta":152,"style":152},"def build_parser() -> argparse.ArgumentParser:\n    parser = argparse.ArgumentParser(prog=\"mytool\", allow_abbrev=False)\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n    register_sync(sub)\n    register_status(sub)\n    return parser\n",[13,2493,2494,2504,2528,2553,2558,2563],{"__ignoreMap":152},[156,2495,2496,2498,2501],{"class":158,"line":159},[156,2497,201],{"class":169},[156,2499,2500],{"class":204}," build_parser",[156,2502,2503],{"class":173},"() -> argparse.ArgumentParser:\n",[156,2505,2506,2508,2510,2512,2514,2516,2518,2520,2522,2524,2526],{"class":158,"line":166},[156,2507,221],{"class":173},[156,2509,224],{"class":169},[156,2511,1275],{"class":173},[156,2513,1278],{"class":233},[156,2515,224],{"class":169},[156,2517,2168],{"class":239},[156,2519,274],{"class":173},[156,2521,2117],{"class":233},[156,2523,224],{"class":169},[156,2525,656],{"class":211},[156,2527,293],{"class":173},[156,2529,2530,2533,2535,2537,2539,2541,2543,2545,2547,2549,2551],{"class":158,"line":177},[156,2531,2532],{"class":173},"    sub ",[156,2534,224],{"class":169},[156,2536,1295],{"class":173},[156,2538,1298],{"class":233},[156,2540,224],{"class":169},[156,2542,1303],{"class":239},[156,2544,274],{"class":173},[156,2546,1308],{"class":233},[156,2548,224],{"class":169},[156,2550,660],{"class":211},[156,2552,293],{"class":173},[156,2554,2555],{"class":158,"line":191},[156,2556,2557],{"class":173},"    register_sync(sub)\n",[156,2559,2560],{"class":158,"line":198},[156,2561,2562],{"class":173},"    register_status(sub)\n",[156,2564,2565,2567],{"class":158,"line":218},[156,2566,1076],{"class":169},[156,2568,2569],{"class":173}," parser\n",[10,2571,2572,2575],{},[18,2573,2574],{},"One registration function per command",", living beside its handler:",[147,2577,2579],{"className":149,"code":2578,"language":151,"meta":152,"style":152},"# mytool\u002Fcommands\u002Fsync.py\ndef register_sync(sub: argparse._SubParsersAction) -> None:\n    parser = sub.add_parser(\"sync\", help=\"Sync a directory to the bucket.\")\n    parser.add_argument(\"source\", type=Path)\n    parser.add_argument(\"--retries\", type=positive_int, default=argparse.SUPPRESS)\n    parser.set_defaults(func=run_sync)\n\ndef run_sync(args: argparse.Namespace) -> int:\n    result = core.sync_directory(args.source, retries=getattr(args, \"retries\", 3))\n    print(f\"{result.uploaded} uploaded\")\n    return 0\n",[13,2580,2581,2586,2600,2622,2637,2661,2674,2678,2692,2722,2744],{"__ignoreMap":152},[156,2582,2583],{"class":158,"line":159},[156,2584,2585],{"class":162},"# mytool\u002Fcommands\u002Fsync.py\n",[156,2587,2588,2590,2593,2596,2598],{"class":158,"line":166},[156,2589,201],{"class":169},[156,2591,2592],{"class":204}," register_sync",[156,2594,2595],{"class":173},"(sub: argparse._SubParsersAction) -> ",[156,2597,212],{"class":211},[156,2599,215],{"class":173},[156,2601,2602,2604,2606,2608,2611,2613,2615,2617,2620],{"class":158,"line":177},[156,2603,221],{"class":173},[156,2605,224],{"class":169},[156,2607,1328],{"class":173},[156,2609,2610],{"class":239},"\"sync\"",[156,2612,274],{"class":173},[156,2614,285],{"class":233},[156,2616,224],{"class":169},[156,2618,2619],{"class":239},"\"Sync a directory to the bucket.\"",[156,2621,293],{"class":173},[156,2623,2624,2626,2628,2630,2632,2634],{"class":158,"line":191},[156,2625,268],{"class":173},[156,2627,271],{"class":239},[156,2629,274],{"class":173},[156,2631,277],{"class":233},[156,2633,224],{"class":169},[156,2635,2636],{"class":173},"Path)\n",[156,2638,2639,2641,2643,2645,2647,2649,2651,2653,2655,2657,2659],{"class":158,"line":198},[156,2640,268],{"class":173},[156,2642,2028],{"class":239},[156,2644,274],{"class":173},[156,2646,277],{"class":233},[156,2648,224],{"class":169},[156,2650,1109],{"class":173},[156,2652,913],{"class":233},[156,2654,224],{"class":169},[156,2656,2045],{"class":173},[156,2658,2048],{"class":211},[156,2660,293],{"class":173},[156,2662,2663,2666,2669,2671],{"class":158,"line":218},[156,2664,2665],{"class":173},"    parser.set_defaults(",[156,2667,2668],{"class":233},"func",[156,2670,224],{"class":169},[156,2672,2673],{"class":173},"run_sync)\n",[156,2675,2676],{"class":158,"line":230},[156,2677,195],{"emptyLinePlaceholder":194},[156,2679,2680,2682,2685,2688,2690],{"class":158,"line":246},[156,2681,201],{"class":169},[156,2683,2684],{"class":204}," run_sync",[156,2686,2687],{"class":173},"(args: argparse.Namespace) -> ",[156,2689,961],{"class":211},[156,2691,215],{"class":173},[156,2693,2694,2697,2699,2702,2705,2707,2710,2712,2715,2717,2719],{"class":158,"line":259},[156,2695,2696],{"class":173},"    result ",[156,2698,224],{"class":169},[156,2700,2701],{"class":173}," core.sync_directory(args.source, ",[156,2703,2704],{"class":233},"retries",[156,2706,224],{"class":169},[156,2708,2709],{"class":211},"getattr",[156,2711,2406],{"class":173},[156,2713,2714],{"class":239},"\"retries\"",[156,2716,274],{"class":173},[156,2718,2103],{"class":211},[156,2720,2721],{"class":173},"))\n",[156,2723,2724,2726,2728,2730,2732,2734,2737,2739,2742],{"class":158,"line":265},[156,2725,473],{"class":211},[156,2727,476],{"class":173},[156,2729,450],{"class":169},[156,2731,453],{"class":239},[156,2733,456],{"class":211},[156,2735,2736],{"class":173},"result.uploaded",[156,2738,462],{"class":211},[156,2740,2741],{"class":239}," uploaded\"",[156,2743,293],{"class":173},[156,2745,2746,2748],{"class":158,"line":296},[156,2747,1076],{"class":169},[156,2749,1138],{"class":211},[10,2751,2752,2753,2756,2757,2760],{},"Adding a command becomes one file and one line in ",[13,2754,2755],{},"build_parser",", and each handler returns an exit\ncode rather than calling ",[13,2758,2759],{},"sys.exit",", so the dispatcher owns termination.",[10,2762,2763,2769],{},[18,2764,2765,2766,2768],{},"Keep ",[13,2767,2451],{}," tiny."," Parse, dispatch, translate exceptions:",[147,2771,2773],{"className":149,"code":2772,"language":151,"meta":152,"style":152},"def main(argv: list[str] | None = None) -> int:\n    args = build_parser().parse_args(argv)\n    try:\n        return args.func(args)\n    except MytoolError as exc:\n        print(f\"error: {exc}\", file=sys.stderr)\n        return 1\n",[13,2774,2775,2806,2815,2822,2830,2844,2875],{"__ignoreMap":152},[156,2776,2777,2779,2781,2784,2786,2789,2792,2795,2798,2800,2802,2804],{"class":158,"line":159},[156,2778,201],{"class":169},[156,2780,205],{"class":204},[156,2782,2783],{"class":173},"(argv: list[",[156,2785,681],{"class":211},[156,2787,2788],{"class":173},"] ",[156,2790,2791],{"class":169},"|",[156,2793,2794],{"class":211}," None",[156,2796,2797],{"class":169}," =",[156,2799,2794],{"class":211},[156,2801,1009],{"class":173},[156,2803,961],{"class":211},[156,2805,215],{"class":173},[156,2807,2808,2810,2812],{"class":158,"line":166},[156,2809,401],{"class":173},[156,2811,224],{"class":169},[156,2813,2814],{"class":173}," build_parser().parse_args(argv)\n",[156,2816,2817,2820],{"class":158,"line":177},[156,2818,2819],{"class":169},"    try",[156,2821,215],{"class":173},[156,2823,2824,2827],{"class":158,"line":191},[156,2825,2826],{"class":169},"        return",[156,2828,2829],{"class":173}," args.func(args)\n",[156,2831,2832,2835,2838,2841],{"class":158,"line":198},[156,2833,2834],{"class":169},"    except",[156,2836,2837],{"class":173}," MytoolError ",[156,2839,2840],{"class":169},"as",[156,2842,2843],{"class":173}," exc:\n",[156,2845,2846,2849,2851,2853,2856,2858,2861,2863,2865,2867,2870,2872],{"class":158,"line":218},[156,2847,2848],{"class":211},"        print",[156,2850,476],{"class":173},[156,2852,450],{"class":169},[156,2854,2855],{"class":239},"\"error: ",[156,2857,456],{"class":211},[156,2859,2860],{"class":173},"exc",[156,2862,462],{"class":211},[156,2864,453],{"class":239},[156,2866,274],{"class":173},[156,2868,2869],{"class":233},"file",[156,2871,224],{"class":169},[156,2873,2874],{"class":173},"sys.stderr)\n",[156,2876,2877,2879],{"class":158,"line":230},[156,2878,2826],{"class":169},[156,2880,2881],{"class":211}," 1\n",[10,2883,2884,2885,2888,2889,2892],{},"Accepting ",[13,2886,2887],{},"argv"," as a parameter is what makes the whole program testable in-process:\n",[13,2890,2891],{},"main([\"sync\", \".\u002Fdata\"])"," returns an exit code with no subprocess involved.",[27,2894,2896],{"id":2895},"frequently-asked-questions","Frequently asked questions",[2898,2899,2901],"h3",{"id":2900},"is-argparse-still-a-reasonable-choice-in-2026","Is argparse still a reasonable choice in 2026?",[10,2903,2904],{},"Yes, for the specific case of a tool that cannot take dependencies: an installer, a bootstrap\nscript, something shipped into a locked image, or a utility that must run on a bare interpreter.\nIt is stable, well documented and in every Python. For anything with a growing command tree and no\ndependency constraint, the machinery Click and Typer provide is exactly the code you would\notherwise be writing.",[2898,2906,2908],{"id":2907},"how-do-i-test-an-argparse-cli","How do I test an argparse CLI?",[10,2910,2911],{},"Build the parser in a function so tests can call it, then assert on the parsed namespace rather\nthan by running a subprocess:",[147,2913,2915],{"className":149,"code":2914,"language":151,"meta":152,"style":152},"def test_defaults():\n    args = build_parser().parse_args([\"sync\", \".\u002Fdata\"])\n    assert args.retries == 3\n",[13,2916,2917,2927,2946],{"__ignoreMap":152},[156,2918,2919,2921,2924],{"class":158,"line":159},[156,2920,201],{"class":169},[156,2922,2923],{"class":204}," test_defaults",[156,2925,2926],{"class":173},"():\n",[156,2928,2929,2931,2933,2936,2938,2940,2943],{"class":158,"line":166},[156,2930,401],{"class":173},[156,2932,224],{"class":169},[156,2934,2935],{"class":173}," build_parser().parse_args([",[156,2937,2610],{"class":239},[156,2939,274],{"class":173},[156,2941,2942],{"class":239},"\".\u002Fdata\"",[156,2944,2945],{"class":173},"])\n",[156,2947,2948,2951,2954,2957],{"class":158,"line":177},[156,2949,2950],{"class":169},"    assert",[156,2952,2953],{"class":173}," args.retries ",[156,2955,2956],{"class":169},"==",[156,2958,2959],{"class":211}," 3\n",[10,2961,2962,2963,2966,2967,2970],{},"For error paths, ",[13,2964,2965],{},"pytest.raises(SystemExit)"," captures the exit and ",[13,2968,2969],{},"capsys"," captures the usage\nmessage. That keeps the tests fast and gives you real tracebacks when something is wrong.",[2898,2972,2974,2975,2977],{"id":2973},"why-does-my-subcommands-dest-collide","Why does my subcommand's ",[13,2976,1298],{}," collide?",[10,2979,2980,2981,2983,2984,2986,2987,2990,2991,2994],{},"Because each ",[13,2982,1260],{}," call needs its own ",[13,2985,1298],{},". With two levels, the inner one\noverwrites the outer if both default to the same name — pass ",[13,2988,2989],{},"dest=\"command\""," at the top level and\n",[13,2992,2993],{},"dest=\"remote_command\""," on the nested one, and both end up in the namespace.",[2898,2996,2998],{"id":2997},"can-argparse-read-defaults-from-a-config-file","Can argparse read defaults from a config file?",[10,3000,3001,3002,3005,3006,3008],{},"Not directly, but ",[13,3003,3004],{},"parser.set_defaults(**config)"," applies a mapping before parsing, so values from\na file become the defaults and any flag the user passes still wins. Combine that with\n",[13,3007,2015],{}," on the options themselves and you have a working precedence chain in\nabout five lines.",[2898,3010,3012],{"id":3011},"how-do-i-show-a-version-flag","How do I show a version flag?",[10,3014,3015,3018,3019,3022],{},[13,3016,3017],{},"parser.add_argument(\"--version\", action=\"version\", version=f\"mytool {version('mytool')}\")",". The\n",[13,3020,3021],{},"version"," action prints and exits before other arguments are validated, which is the behaviour you\nwant — the flag works even when the rest of the command line is incomplete.",[2898,3024,3026],{"id":3025},"does-argparse-handle-unicode-and-windows-paths-correctly","Does argparse handle Unicode and Windows paths correctly?",[10,3028,3029,3030,3032,3033,3035,3036,3038,3039,3041,3042,3045],{},"Yes — arguments arrive as ",[13,3031,681],{}," already decoded by Python, and ",[13,3034,667],{}," produces a ",[13,3037,964],{}," that\nbehaves correctly on every platform. The cross-platform problems people attribute to ",[13,3040,15],{},"\nare almost always elsewhere: a hard-coded forward slash in a default, or a shell that expanded a\nglob differently. Use ",[13,3043,3044],{},"pathlib"," throughout, let the shell do its own quoting, and run the test suite on Windows in CI rather than special-casing anything in the parser.",[27,3047,3049],{"id":3048},"related","Related",[32,3051,3052,3059,3064,3069,3074],{},[35,3053,3054,3055],{},"Up: ",[94,3056,3058],{"href":3057},"\u002Fmodern-python-cli-frameworks-architecture\u002F","Modern Python CLI Frameworks & Architecture",[35,3060,3061,3062],{},"Down: ",[94,3063,1426],{"href":96},[35,3065,3061,3066],{},[94,3067,3068],{"href":109},"Migrating from argparse to Typer",[35,3070,3071,3072],{},"Sideways: ",[94,3073,1742],{"href":104},[35,3075,3071,3076],{},[94,3077,3078],{"href":1819},"Structuring multi-command Python CLIs",[3080,3081,3082],"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":152,"searchDepth":166,"depth":166,"links":3084},[3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3107],{"id":29,"depth":166,"text":30},{"id":117,"depth":166,"text":118},{"id":137,"depth":166,"text":138},{"id":685,"depth":166,"text":686},{"id":952,"depth":166,"text":953},{"id":1240,"depth":166,"text":1241},{"id":1429,"depth":166,"text":1430},{"id":1633,"depth":166,"text":1634},{"id":1754,"depth":166,"text":1755},{"id":1836,"depth":166,"text":1837},{"id":2139,"depth":166,"text":2140},{"id":2355,"depth":166,"text":2356},{"id":2467,"depth":166,"text":2468},{"id":2895,"depth":166,"text":2896,"children":3099},[3100,3101,3102,3104,3105,3106],{"id":2900,"depth":177,"text":2901},{"id":2907,"depth":177,"text":2908},{"id":2973,"depth":177,"text":3103},"Why does my subcommand's dest collide?",{"id":2997,"depth":177,"text":2998},{"id":3011,"depth":177,"text":3012},{"id":3025,"depth":177,"text":3026},{"id":3048,"depth":166,"text":3049},"2026-07-05","Build Python CLIs with the standard-library argparse: arguments, options, types, and subcommands, and know when to move up to Click or Typer.","beginner",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse",{"title":5,"description":3109},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Findex",[15,3118,3119,3120,3121],"cli","structure","errors","click","2026-08-01","NI4impFXhn9OIhx3ZEiwQxZD-x-5CTpROTV7ZWvsIIc",[3125,3128,3131,3134,3137,3140,3143,3146,3149,3152,3155,3158,3161,3164,3167,3170,3173,3176,3179,3182,3185,3188,3191,3194,3197,3200,3203,3206,3209,3212,3215,3218,3220,3223,3226,3229,3232,3235,3238,3239,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3313,3316,3319,3322,3325,3328,3331,3334,3337,3340,3343,3346,3349,3352,3355,3358,3361,3364,3367],{"path":3126,"title":3127},"\u002Fabout","About Python CLI Toolcraft",{"path":3129,"title":3130},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":3132,"title":3133},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":3135,"title":3136},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":3138,"title":3139},"\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":3141,"title":3142},"\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":3144,"title":3145},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":3147,"title":3148},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":3150,"title":3151},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":3153,"title":3154},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":3156,"title":3157},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":3159,"title":3160},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":3162,"title":3163},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":3165,"title":3166},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":3168,"title":3169},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":3171,"title":3172},"\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":3174,"title":3175},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3177,"title":3178},"\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":3180,"title":3181},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3183,"title":3184},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3186,"title":3187},"\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":3189,"title":3190},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3192,"title":3193},"\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":3195,"title":3196},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3198,"title":3199},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3201,"title":3202},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3204,"title":3205},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3207,"title":3208},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3210,"title":3211},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3213,"title":3214},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3216,"title":3217},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":420,"title":3219},"Python CLI Toolcraft",{"path":3221,"title":3222},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3224,"title":3225},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3227,"title":3228},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3230,"title":3231},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3233,"title":3234},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3236,"title":3237},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3114,"title":5},{"path":3240,"title":3068},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer",{"path":3242,"title":3243},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3245,"title":3246},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3248,"title":3249},"\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":3251,"title":3252},"\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":3254,"title":3255},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3257,"title":3258},"\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":3260,"title":3261},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3263,"title":3264},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3266,"title":3267},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3269,"title":3270},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3272,"title":3273},"\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":3275,"title":3276},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3278,"title":3279},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3281,"title":3282},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3284,"title":3285},"\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":3287,"title":3288},"\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":3290,"title":3291},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3293,"title":3294},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3296,"title":3297},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3299,"title":3300},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3302,"title":3303},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3305,"title":3306},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3308,"title":3309},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3311,"title":3312},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3314,"title":3315},"\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":3317,"title":3318},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3320,"title":3321},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3323,"title":3324},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3326,"title":3327},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3329,"title":3330},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3332,"title":3333},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3335,"title":3336},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3338,"title":3339},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3341,"title":3342},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3344,"title":3345},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3347,"title":3348},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3350,"title":3351},"\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":3353,"title":3354},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3356,"title":3357},"\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":3359,"title":3360},"\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":3362,"title":3363},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3365,"title":3366},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3368,"title":3369},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690032]