[{"data":1,"prerenderedAt":2039},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F":3,"content-directory":1889},{"id":4,"title":5,"body":6,"date":1874,"description":1875,"difficulty":1876,"draft":1877,"extension":1878,"meta":1879,"navigation":194,"path":1880,"seo":1881,"stem":1882,"tags":1883,"updated":1874,"__hash__":1888},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Findex.md","Command-Line Parsing with argparse",{"type":7,"value":8,"toc":1862},"minimark",[9,26,31,111,115,119,125,135,139,146,530,626,662,683,687,690,886,947,951,974,1113,1163,1182,1218,1232,1236,1256,1404,1421,1425,1449,1614,1625,1629,1634,1725,1743,1747,1825,1829,1858],[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.",[147,691,693],{"className":149,"code":692,"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,694,695,700,707,738,749,761,765,769,776,792,801,812,816,820,827,843,852,863,867],{"__ignoreMap":152},[156,696,697],{"class":158,"line":159},[156,698,699],{"class":173},"parser.add_argument(\n",[156,701,702,705],{"class":158,"line":166},[156,703,704],{"class":239},"    \"--log-level\"",[156,706,243],{"class":173},[156,708,709,712,714,717,720,722,725,727,730,732,735],{"class":158,"line":177},[156,710,711],{"class":233},"    choices",[156,713,224],{"class":169},[156,715,716],{"class":173},"[",[156,718,719],{"class":239},"\"debug\"",[156,721,274],{"class":173},[156,723,724],{"class":239},"\"info\"",[156,726,274],{"class":173},[156,728,729],{"class":239},"\"warning\"",[156,731,274],{"class":173},[156,733,734],{"class":239},"\"error\"",[156,736,737],{"class":173},"],\n",[156,739,740,743,745,747],{"class":158,"line":191},[156,741,742],{"class":233},"    default",[156,744,224],{"class":169},[156,746,724],{"class":239},[156,748,243],{"class":173},[156,750,751,754,756,759],{"class":158,"line":198},[156,752,753],{"class":233},"    help",[156,755,224],{"class":169},[156,757,758],{"class":239},"\"Verbosity (default: info).\"",[156,760,243],{"class":173},[156,762,763],{"class":158,"line":218},[156,764,293],{"class":173},[156,766,767],{"class":158,"line":230},[156,768,699],{"class":173},[156,770,771,774],{"class":158,"line":246},[156,772,773],{"class":239},"    \"paths\"",[156,775,243],{"class":173},[156,777,778,781,783,786,789],{"class":158,"line":259},[156,779,780],{"class":233},"    nargs",[156,782,224],{"class":169},[156,784,785],{"class":239},"\"+\"",[156,787,788],{"class":173},",              ",[156,790,791],{"class":162},"# one or more, collected into a list\n",[156,793,794,797,799],{"class":158,"line":265},[156,795,796],{"class":233},"    type",[156,798,224],{"class":169},[156,800,318],{"class":173},[156,802,803,805,807,810],{"class":158,"line":296},[156,804,753],{"class":233},[156,806,224],{"class":169},[156,808,809],{"class":239},"\"One or more files to process.\"",[156,811,243],{"class":173},[156,813,814],{"class":158,"line":302},[156,815,293],{"class":173},[156,817,818],{"class":158,"line":310},[156,819,699],{"class":173},[156,821,822,825],{"class":158,"line":321},[156,823,824],{"class":239},"    \"--tag\"",[156,826,243],{"class":173},[156,828,829,832,834,837,840],{"class":158,"line":332},[156,830,831],{"class":233},"    action",[156,833,224],{"class":169},[156,835,836],{"class":239},"\"append\"",[156,838,839],{"class":173},",        ",[156,841,842],{"class":162},"# repeatable: --tag a --tag b -> [\"a\", \"b\"]\n",[156,844,845,847,849],{"class":158,"line":345},[156,846,742],{"class":233},[156,848,224],{"class":169},[156,850,851],{"class":173},"[],\n",[156,853,854,856,858,861],{"class":158,"line":350},[156,855,753],{"class":233},[156,857,224],{"class":169},[156,859,860],{"class":239},"\"Attach a tag; repeat for several.\"",[156,862,243],{"class":173},[156,864,865],{"class":158,"line":355},[156,866,293],{"class":173},[156,868,869,872,875,877,880,882,884],{"class":158,"line":363},[156,870,871],{"class":173},"parser.add_argument(",[156,873,874],{"class":239},"\"--dry-run\"",[156,876,274],{"class":173},[156,878,879],{"class":233},"action",[156,881,224],{"class":169},[156,883,371],{"class":239},[156,885,293],{"class":173},[32,887,888,904,914,936],{},[35,889,890,895,896,898,899,903],{},[18,891,892],{},[13,893,894],{},"choices"," restricts a value to a fixed set. ",[13,897,15],{}," rejects anything else ",[900,901,902],"em",{},"before","\nyour code runs and lists the valid options in the error, so you never validate the enum by\nhand.",[35,905,906,911,912,57],{},[18,907,908],{},[13,909,910],{},"default"," supplies a value when the flag is absent. Optionals without a default get\n",[13,913,212],{},[35,915,916,920,921,923,924,927,928,931,932,935],{},[18,917,918],{},[13,919,130],{}," controls how many values an argument consumes: ",[13,922,785],{}," (one or more), ",[13,925,926],{},"\"*\"","\n(zero or more), ",[13,929,930],{},"\"?\""," (optional single), or an integer for an exact count. ",[13,933,934],{},"nargs=\"+\""," on\na positional is how you accept a list of files.",[35,937,938,942,943,946],{},[18,939,940],{},[13,941,79],{}," for boolean switches, ",[13,944,945],{},"action=\"append\""," to collect a repeatable\noption into a list.",[27,948,950],{"id":949},"validation-with-type-callables-and-parsererror","Validation with type= callables and parser.error()",[10,952,953,955,956,49,959,962,963,966,967,970,971,973],{},[13,954,63],{}," is not just for ",[13,957,958],{},"int",[13,960,961],{},"Path"," — any callable that takes a string and either\nreturns a value or raises is fair game. Raise ",[13,964,965],{},"argparse.ArgumentTypeError"," (or ",[13,968,969],{},"ValueError",")\nand ",[13,972,15],{}," turns it into a clean, non-zero-exit error message instead of a traceback.",[147,975,977],{"className":149,"code":976,"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,978,979,985,989,1008,1024,1039,1066,1074,1078,1088],{"__ignoreMap":152},[156,980,981,983],{"class":158,"line":159},[156,982,170],{"class":169},[156,984,174],{"class":173},[156,986,987],{"class":158,"line":166},[156,988,195],{"emptyLinePlaceholder":194},[156,990,991,993,996,999,1001,1004,1006],{"class":158,"line":177},[156,992,201],{"class":169},[156,994,995],{"class":204}," positive_int",[156,997,998],{"class":173},"(raw: ",[156,1000,681],{"class":211},[156,1002,1003],{"class":173},") -> ",[156,1005,958],{"class":211},[156,1007,215],{"class":173},[156,1009,1010,1013,1015,1018,1021],{"class":158,"line":191},[156,1011,1012],{"class":173},"    value ",[156,1014,224],{"class":169},[156,1016,1017],{"class":211}," int",[156,1019,1020],{"class":173},"(raw)              ",[156,1022,1023],{"class":162},"# ValueError here is caught by argparse too\n",[156,1025,1026,1028,1031,1034,1037],{"class":158,"line":198},[156,1027,429],{"class":169},[156,1029,1030],{"class":173}," value ",[156,1032,1033],{"class":169},"\u003C=",[156,1035,1036],{"class":211}," 0",[156,1038,215],{"class":173},[156,1040,1041,1044,1047,1049,1051,1053,1056,1059,1061,1064],{"class":158,"line":218},[156,1042,1043],{"class":169},"        raise",[156,1045,1046],{"class":173}," argparse.ArgumentTypeError(",[156,1048,450],{"class":169},[156,1050,453],{"class":239},[156,1052,456],{"class":211},[156,1054,1055],{"class":173},"raw",[156,1057,1058],{"class":169},"!r",[156,1060,462],{"class":211},[156,1062,1063],{"class":239}," is not a positive integer\"",[156,1065,293],{"class":173},[156,1067,1068,1071],{"class":158,"line":230},[156,1069,1070],{"class":169},"    return",[156,1072,1073],{"class":173}," value\n",[156,1075,1076],{"class":158,"line":246},[156,1077,195],{"emptyLinePlaceholder":194},[156,1079,1080,1083,1085],{"class":158,"line":259},[156,1081,1082],{"class":173},"parser ",[156,1084,224],{"class":169},[156,1086,1087],{"class":173}," argparse.ArgumentParser()\n",[156,1089,1090,1092,1095,1097,1099,1101,1104,1106,1108,1111],{"class":158,"line":265},[156,1091,871],{"class":173},[156,1093,1094],{"class":239},"\"--workers\"",[156,1096,274],{"class":173},[156,1098,277],{"class":233},[156,1100,224],{"class":169},[156,1102,1103],{"class":173},"positive_int, ",[156,1105,910],{"class":233},[156,1107,224],{"class":169},[156,1109,1110],{"class":211},"4",[156,1112,293],{"class":173},[147,1114,1116],{"className":532,"code":1115,"language":534,"meta":152,"style":152},"$ python app.py --workers 0\napp.py: error: argument --workers: '0' is not a positive integer\n",[13,1117,1118,1133],{"__ignoreMap":152},[156,1119,1120,1122,1124,1127,1130],{"class":158,"line":159},[156,1121,541],{"class":204},[156,1123,544],{"class":239},[156,1125,1126],{"class":239}," app.py",[156,1128,1129],{"class":211}," --workers",[156,1131,1132],{"class":211}," 0\n",[156,1134,1135,1138,1140,1143,1146,1149,1152,1154,1157,1160],{"class":158,"line":166},[156,1136,1137],{"class":204},"app.py:",[156,1139,601],{"class":239},[156,1141,1142],{"class":239}," argument",[156,1144,1145],{"class":211}," --workers:",[156,1147,1148],{"class":239}," '0'",[156,1150,1151],{"class":239}," is",[156,1153,438],{"class":239},[156,1155,1156],{"class":239}," a",[156,1158,1159],{"class":239}," positive",[156,1161,1162],{"class":239}," integer\n",[10,1164,1165,1166,1169,1170,1173,1174,1177,1178,1181],{},"For validation that spans ",[900,1167,1168],{},"several"," arguments (say, \"",[13,1171,1172],{},"--end"," must be after ",[13,1175,1176],{},"--start","\"), do\nit after parsing and report failures through ",[13,1179,1180],{},"parser.error()",", which prints to stderr and\nexits with status 2 — the conventional argparse usage-error code:",[147,1183,1185],{"className":149,"code":1184,"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,1186,1187,1196,1208],{"__ignoreMap":152},[156,1188,1189,1192,1194],{"class":158,"line":159},[156,1190,1191],{"class":173},"args ",[156,1193,224],{"class":169},[156,1195,406],{"class":173},[156,1197,1198,1200,1203,1205],{"class":158,"line":166},[156,1199,512],{"class":169},[156,1201,1202],{"class":173}," args.end ",[156,1204,1033],{"class":169},[156,1206,1207],{"class":173}," args.start:\n",[156,1209,1210,1213,1216],{"class":158,"line":177},[156,1211,1212],{"class":173},"    parser.error(",[156,1214,1215],{"class":239},"\"--end must be later than --start\"",[156,1217,293],{"class":173},[10,1219,1220,1221,1223,1224,1227,1228,57],{},"Using ",[13,1222,1180],{}," rather than ",[13,1225,1226],{},"print(); sys.exit()"," keeps your error output consistent\nwith the parser's own messages. For the broader picture of exit statuses, see\n",[94,1229,1231],{"href":1230},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","choosing exit codes for CLI tools",[27,1233,1235],{"id":1234},"subcommands-a-first-look","Subcommands: a first look",[10,1237,1238,1239,274,1242,274,1245,1248,1249,1251,1252,1255],{},"Once a tool does more than one job — ",[13,1240,1241],{},"tool build",[13,1243,1244],{},"tool deploy",[13,1246,1247],{},"tool clean"," — you want\nsubcommands, each with its own arguments and help. ",[13,1250,15],{}," provides these through\n",[13,1253,1254],{},"add_subparsers()",":",[147,1257,1259],{"className":149,"code":1258,"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,1260,1261,1280,1309,1313,1337,1355,1359,1382,1392,1396],{"__ignoreMap":152},[156,1262,1263,1265,1267,1270,1273,1275,1278],{"class":158,"line":159},[156,1264,1082],{"class":173},[156,1266,224],{"class":169},[156,1268,1269],{"class":173}," argparse.ArgumentParser(",[156,1271,1272],{"class":233},"prog",[156,1274,224],{"class":169},[156,1276,1277],{"class":239},"\"tool\"",[156,1279,293],{"class":173},[156,1281,1282,1285,1287,1290,1293,1295,1298,1300,1303,1305,1307],{"class":158,"line":166},[156,1283,1284],{"class":173},"sub ",[156,1286,224],{"class":169},[156,1288,1289],{"class":173}," parser.add_subparsers(",[156,1291,1292],{"class":233},"dest",[156,1294,224],{"class":169},[156,1296,1297],{"class":239},"\"command\"",[156,1299,274],{"class":173},[156,1301,1302],{"class":233},"required",[156,1304,224],{"class":169},[156,1306,660],{"class":211},[156,1308,293],{"class":173},[156,1310,1311],{"class":158,"line":177},[156,1312,195],{"emptyLinePlaceholder":194},[156,1314,1315,1318,1320,1323,1326,1328,1330,1332,1335],{"class":158,"line":191},[156,1316,1317],{"class":173},"build ",[156,1319,224],{"class":169},[156,1321,1322],{"class":173}," sub.add_parser(",[156,1324,1325],{"class":239},"\"build\"",[156,1327,274],{"class":173},[156,1329,285],{"class":233},[156,1331,224],{"class":169},[156,1333,1334],{"class":239},"\"Build the project.\"",[156,1336,293],{"class":173},[156,1338,1339,1342,1345,1347,1349,1351,1353],{"class":158,"line":198},[156,1340,1341],{"class":173},"build.add_argument(",[156,1343,1344],{"class":239},"\"--release\"",[156,1346,274],{"class":173},[156,1348,879],{"class":233},[156,1350,224],{"class":169},[156,1352,371],{"class":239},[156,1354,293],{"class":173},[156,1356,1357],{"class":158,"line":218},[156,1358,195],{"emptyLinePlaceholder":194},[156,1360,1361,1364,1366,1368,1371,1373,1375,1377,1380],{"class":158,"line":230},[156,1362,1363],{"class":173},"deploy ",[156,1365,224],{"class":169},[156,1367,1322],{"class":173},[156,1369,1370],{"class":239},"\"deploy\"",[156,1372,274],{"class":173},[156,1374,285],{"class":233},[156,1376,224],{"class":169},[156,1378,1379],{"class":239},"\"Deploy the project.\"",[156,1381,293],{"class":173},[156,1383,1384,1387,1390],{"class":158,"line":246},[156,1385,1386],{"class":173},"deploy.add_argument(",[156,1388,1389],{"class":239},"\"target\"",[156,1391,293],{"class":173},[156,1393,1394],{"class":158,"line":259},[156,1395,195],{"emptyLinePlaceholder":194},[156,1397,1398,1400,1402],{"class":158,"line":265},[156,1399,1191],{"class":173},[156,1401,224],{"class":169},[156,1403,406],{"class":173},[10,1405,1406,1407,49,1410,1413,1414,1417,1418,57],{},"That is enough to give ",[13,1408,1409],{},"tool build --release",[13,1411,1412],{},"tool deploy prod"," their own parsers. The\nclean way to route each subcommand to a handler function — with ",[13,1415,1416],{},"set_defaults(func=...)",",\nshared parent parsers, and nesting — is a topic of its own:\n",[94,1419,1420],{"href":96},"argparse subparsers for subcommands",[27,1422,1424],{"id":1423},"help-output-for-free","Help output for free",[10,1426,1427,1428,668,1431,1433,1434,274,1436,1439,1440,1442,1443,420,1446,1448],{},"You never write ",[13,1429,1430],{},"--help",[13,1432,15],{}," builds usage text from your ",[13,1435,1272],{},[13,1437,1438],{},"description",", and\nevery argument's ",[13,1441,285],{}," string, and wires up ",[13,1444,1445],{},"-h",[13,1447,1430],{}," automatically:",[147,1450,1452],{"className":532,"code":1451,"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,1453,1454,1465,1476,1480,1499,1503,1510,1523,1527,1532,1558,1588],{"__ignoreMap":152},[156,1455,1456,1458,1460,1462],{"class":158,"line":159},[156,1457,541],{"class":204},[156,1459,544],{"class":239},[156,1461,547],{"class":239},[156,1463,1464],{"class":211}," --help\n",[156,1466,1467,1470,1473],{"class":158,"line":166},[156,1468,1469],{"class":204},"usage:",[156,1471,1472],{"class":239}," mvcp",[156,1474,1475],{"class":173}," [-h] [--dest-dir DEST_DIR] [--overwrite] source\n",[156,1477,1478],{"class":158,"line":177},[156,1479,195],{"emptyLinePlaceholder":194},[156,1481,1482,1485,1487,1490,1493,1496],{"class":158,"line":191},[156,1483,1484],{"class":204},"Copy",[156,1486,1156],{"class":239},[156,1488,1489],{"class":239}," file,",[156,1491,1492],{"class":239}," optionally",[156,1494,1495],{"class":239}," renaming",[156,1497,1498],{"class":239}," it.\n",[156,1500,1501],{"class":158,"line":198},[156,1502,195],{"emptyLinePlaceholder":194},[156,1504,1505,1507],{"class":158,"line":218},[156,1506,44],{"class":204},[156,1508,1509],{"class":239}," arguments:\n",[156,1511,1512,1515,1518,1520],{"class":158,"line":230},[156,1513,1514],{"class":211},"  source",[156,1516,1517],{"class":239},"               File",[156,1519,619],{"class":239},[156,1521,1522],{"class":239}," copy.\n",[156,1524,1525],{"class":158,"line":246},[156,1526,195],{"emptyLinePlaceholder":194},[156,1528,1529],{"class":158,"line":259},[156,1530,1531],{"class":204},"options:\n",[156,1533,1534,1537,1540,1543,1546,1549,1552,1555],{"class":158,"line":265},[156,1535,1536],{"class":204},"  -h,",[156,1538,1539],{"class":211}," --help",[156,1541,1542],{"class":239},"           show",[156,1544,1545],{"class":239}," this",[156,1547,1548],{"class":239}," help",[156,1550,1551],{"class":239}," message",[156,1553,1554],{"class":239}," and",[156,1556,1557],{"class":239}," exit\n",[156,1559,1560,1563,1566,1569,1571,1573,1576,1579,1582,1585],{"class":158,"line":296},[156,1561,1562],{"class":204},"  --dest-dir",[156,1564,1565],{"class":239}," DEST_DIR",[156,1567,1568],{"class":239},"  Directory",[156,1570,619],{"class":239},[156,1572,564],{"class":239},[156,1574,1575],{"class":239}," into",[156,1577,1578],{"class":173}," (default: ",[156,1580,1581],{"class":239},"current",[156,1583,1584],{"class":239}," directory",[156,1586,1587],{"class":173},").\n",[156,1589,1590,1593,1596,1599,1602,1605,1608,1611],{"class":158,"line":302},[156,1591,1592],{"class":204},"  --overwrite",[156,1594,1595],{"class":239},"          Replace",[156,1597,1598],{"class":239}," the",[156,1600,1601],{"class":239}," destination",[156,1603,1604],{"class":239}," if",[156,1606,1607],{"class":239}," it",[156,1609,1610],{"class":239}," already",[156,1612,1613],{"class":239}," exists.\n",[10,1615,1616,1617,1620,1621,1624],{},"Add an ",[13,1618,1619],{},"epilog="," for examples, and set ",[13,1622,1623],{},"formatter_class=argparse.RawDescriptionHelpFormatter","\nif you want to control the wrapping of your description yourself.",[27,1626,1628],{"id":1627},"when-to-graduate-to-click-or-typer","When to graduate to Click or Typer",[10,1630,1631,1633],{},[13,1632,15],{}," starts to fight you at a predictable point. The trade-offs:",[1635,1636,1637,1652],"table",{},[1638,1639,1640],"thead",{},[1641,1642,1643,1647,1649],"tr",{},[1644,1645,1646],"th",{},"Concern",[1644,1648,15],{},[1644,1650,1651],{},"Click \u002F Typer",[1653,1654,1655,1667,1678,1689,1703,1714],"tbody",{},[1641,1656,1657,1661,1664],{},[1658,1659,1660],"td",{},"Dependencies",[1658,1662,1663],{},"None (stdlib)",[1658,1665,1666],{},"One framework",[1641,1668,1669,1672,1675],{},[1658,1670,1671],{},"Boilerplate",[1658,1673,1674],{},"High — every arg spelled out",[1658,1676,1677],{},"Low, especially with Typer's type hints",[1641,1679,1680,1683,1686],{},[1658,1681,1682],{},"Nested subcommands",[1658,1684,1685],{},"Manual and verbose",[1658,1687,1688],{},"First-class groups",[1641,1690,1691,1694,1697],{},[1658,1692,1693],{},"Shared context between commands",[1658,1695,1696],{},"Roll your own",[1658,1698,1699,1702],{},[13,1700,1701],{},"ctx.obj"," \u002F dependency injection",[1641,1704,1705,1708,1711],{},[1658,1706,1707],{},"Shell completion",[1658,1709,1710],{},"Not built in",[1658,1712,1713],{},"Built in",[1641,1715,1716,1719,1722],{},[1658,1717,1718],{},"Rich help, colors, prompts",[1658,1720,1721],{},"Manual",[1658,1723,1724],{},"Included",[10,1726,1727,1728,1730,1731,1734,1735,1739,1740,57],{},"If your tool is a handful of commands with simple options, ",[13,1729,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,1732,1733],{"href":104},"Typer vs Click: when to use each","\nto pick one, then either\n",[94,1736,1738],{"href":1737},"\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,1741,1742],{"href":109},"migration path to Typer",[27,1744,1746],{"id":1745},"production-notes","Production notes",[32,1748,1749,1766,1783,1800,1813],{},[35,1750,1751,1754,1755,1757,1758,1761,1762,1765],{},[18,1752,1753],{},"Namespace is intentionally dumb."," ",[13,1756,84],{}," returns an ",[13,1759,1760],{},"argparse.Namespace"," with\nno validation of its own. For a typed object, feed it into a dataclass:\n",[13,1763,1764],{},"Config(**vars(args))",". That gives you editor autocompletion and mypy coverage downstream.",[35,1767,1768,1771,1772,1775,1776,1779,1780,1782],{},[18,1769,1770],{},"Test without a subprocess."," Call ",[13,1773,1774],{},"parser.parse_args([\"notes.txt\", \"--overwrite\"])"," with\nan explicit list in unit tests — it reads ",[13,1777,1778],{},"sys.argv"," only when you pass ",[13,1781,212],{},". Assert on\nthe returned namespace directly.",[35,1784,1785,1754,1788,1790,1791,668,1794,1796,1797,57],{},[18,1786,1787],{},"Hyphens become underscores.",[13,1789,638],{}," is available as ",[13,1792,1793],{},"args.dest_dir",[13,1795,15],{},"\ntranslates automatically; do not look for ",[13,1798,1799],{},"args[\"dest-dir\"]",[35,1801,1802,1754,1805,1807,1808,1812],{},[18,1803,1804],{},"Exit code 2 for usage errors.",[13,1806,1180],{}," and unknown-argument failures exit with\nstatus 2, a convention worth preserving if you later migrate. See\n",[94,1809,1811],{"href":1810},"\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,1814,1815,1820,1821,1824],{},[18,1816,1817],{},[13,1818,1819],{},"parse_known_args()"," returns a ",[13,1822,1823],{},"(namespace, leftovers)"," tuple when you need to forward\nunrecognized flags to a wrapped tool, rather than erroring on them.",[27,1826,1828],{"id":1827},"related","Related",[32,1830,1831,1838,1843,1848,1853],{},[35,1832,1833,1834],{},"Up: ",[94,1835,1837],{"href":1836},"\u002Fmodern-python-cli-frameworks-architecture\u002F","Modern Python CLI Frameworks & Architecture",[35,1839,1840,1841],{},"Down: ",[94,1842,1420],{"href":96},[35,1844,1840,1845],{},[94,1846,1847],{"href":109},"Migrating from argparse to Typer",[35,1849,1850,1851],{},"Sideways: ",[94,1852,1733],{"href":104},[35,1854,1850,1855],{},[94,1856,1857],{"href":1810},"Structuring multi-command Python CLIs",[1859,1860,1861],"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":1863},[1864,1865,1866,1867,1868,1869,1870,1871,1872,1873],{"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":949,"depth":166,"text":950},{"id":1234,"depth":166,"text":1235},{"id":1423,"depth":166,"text":1424},{"id":1627,"depth":166,"text":1628},{"id":1745,"depth":166,"text":1746},{"id":1827,"depth":166,"text":1828},"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":1875},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Findex",[15,1884,1885,1886,1887],"cli","structure","errors","click","5HP86bsOyJ9yCemgo95W3UgGzm3twFhTPFobLcBJFqw",[1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1946,1949,1952,1955,1958,1959,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036],{"path":1891,"title":1892},"\u002Fabout","About Python CLI Toolcraft",{"path":1894,"title":1895},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1897,"title":1898},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1900,"title":1901},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1903,"title":1904},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1906,"title":1907},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1909,"title":1910},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1912,"title":1913},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1915,"title":1916},"\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":1918,"title":1919},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1921,"title":1922},"\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":1924,"title":1925},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1927,"title":1928},"\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":1930,"title":1931},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1933,"title":1934},"\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":1936,"title":1937},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1939,"title":1940},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1942,"title":1943},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":420,"title":1945},"Python CLI Toolcraft",{"path":1947,"title":1948},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1950,"title":1951},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1953,"title":1954},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1956,"title":1957},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1880,"title":5},{"path":1960,"title":1847},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer",{"path":1962,"title":1963},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1965,"title":1966},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1968,"title":1969},"\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":1971,"title":1972},"\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":1974,"title":1975},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1977,"title":1978},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1980,"title":1981},"\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":1983,"title":1984},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1986,"title":1987},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1989,"title":1990},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1992,"title":1993},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1995,"title":1996},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1998,"title":1999},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2001,"title":2002},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2004,"title":2005},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2007,"title":2008},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2010,"title":2011},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2013,"title":2014},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2016,"title":2017},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2019,"title":2020},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2022,"title":2023},"\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":2025,"title":2026},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2028,"title":2029},"\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":2031,"title":2032},"\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":2034,"title":2035},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2037,"title":2038},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867197]