[{"data":1,"prerenderedAt":2401},["ShallowReactive",2],{"page-\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse\u002F":3,"content-directory":1853},{"id":4,"title":5,"body":6,"date":1839,"description":1840,"difficulty":1841,"draft":1842,"extension":1843,"meta":1844,"navigation":154,"path":1845,"seo":1846,"stem":1847,"tags":1848,"updated":1839,"__hash__":1852},"content\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse\u002Findex.md","Mutually Exclusive Options in argparse",{"type":7,"value":8,"toc":1818},"minimark",[9,56,61,75,79,83,109,113,962,967,1000,1011,1041,1058,1061,1065,1079,1103,1107,1122,1126,1129,1173,1177,1191,1671,1688,1692,1704,1708,1712,1726,1730,1736,1740,1747,1751,1760,1767,1779,1783,1814],[10,11,12,13,17,18,21,22,17,25,28,29,32,33,36,37,40,41,44,45,49,50,55],"p",{},"Some options cannot be combined. A report can be ",[14,15,16],"code",{},"--json"," or ",[14,19,20],{},"--csv",", not both. A sync can be ",[14,23,24],{},"--dry-run",[14,26,27],{},"--force",", but combining them is almost certainly a mistake. Other options only make sense together: ",[14,30,31],{},"--cert"," requires ",[14,34,35],{},"--key",", ",[14,38,39],{},"--output-dir"," is meaningless without ",[14,42,43],{},"--save",". Leaving these rules unchecked produces the worst kind of CLI behaviour — silently ignoring one of the flags the user typed. argparse enforces simple exclusivity for you with ",[46,47,48],"strong",{},"mutually exclusive groups",", including \"exactly one of these is required\". Anything more nuanced — dependencies between options, rules involving values — needs a small validation step after parsing that reports errors in argparse's own format. This guide covers both, and how to test every rule. It belongs to the ",[51,52,54],"a",{"href":53},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002F","command-line parsing with argparse topic",".",[57,58,60],"h2",{"id":59},"prerequisites","Prerequisites",[62,63,64,68],"ul",{},[65,66,67],"li",{},"Python 3.10+; standard library only.",[65,69,70,71,55],{},"A CLI built with argparse. For Click and Typer, the equivalent techniques are in ",[51,72,74],{"href":73},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options\u002F","validating dependent and conflicting options",[57,76,78],{"id":77},"exclusive-groups","Exclusive groups",[80,81],"inline-diagram",{"name":82},"mx-group",[10,84,85,88,89,92,93,96,97,100,101,104,105,108],{},[14,86,87],{},"parser.add_mutually_exclusive_group()"," returns a group object with the same ",[14,90,91],{},"add_argument"," method as the parser. argparse then rejects any command line that uses more than one option from the group. With ",[14,94,95],{},"required=True",", it also rejects a command line that uses ",[46,98,99],{},"none"," of them — \"exactly one\" semantics. The usage line reflects the rule automatically: ",[14,102,103],{},"[--json | --csv]"," for an optional group, ",[14,106,107],{},"(--json | --csv | --table)"," for a required one.",[57,110,112],{"id":111},"the-recipe","The recipe",[114,115,120],"pre",{"className":116,"code":117,"language":118,"meta":119,"style":119},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fcli.py\nfrom __future__ import annotations\n\nimport argparse\nfrom collections.abc import Sequence\nfrom pathlib import Path\n\n\ndef build_parser() -> argparse.ArgumentParser:\n    p = argparse.ArgumentParser(prog=\"report\", allow_abbrev=False)\n\n    fmt = p.add_mutually_exclusive_group()\n    fmt.add_argument(\"--json\", dest=\"format\", action=\"store_const\", const=\"json\")\n    fmt.add_argument(\"--csv\", dest=\"format\", action=\"store_const\", const=\"csv\")\n    fmt.add_argument(\"--table\", dest=\"format\", action=\"store_const\", const=\"table\")\n    p.set_defaults(format=\"table\")\n\n    mode = p.add_mutually_exclusive_group()\n    mode.add_argument(\"--dry-run\", \"-n\", action=\"store_true\", help=\"Show what would be written.\")\n    mode.add_argument(\"--force\", \"-f\", action=\"store_true\", help=\"Overwrite existing output.\")\n\n    p.add_argument(\"--output-dir\", type=Path, help=\"Write the report here (requires --save).\")\n    p.add_argument(\"--save\", action=\"store_true\", help=\"Save the report instead of printing it.\")\n    p.add_argument(\"--cert\", type=Path, help=\"Client certificate (requires --key).\")\n    p.add_argument(\"--key\", type=Path, help=\"Client key (requires --cert).\")\n    p.add_argument(\"--since\", help=\"Start date, YYYY-MM-DD.\")\n    p.add_argument(\"--until\", help=\"End date, YYYY-MM-DD; must not be before --since.\")\n    return p\n\n\ndef validate(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:\n    \"\"\"Cross-option rules that exclusive groups cannot express.\"\"\"\n    if args.output_dir is not None and not args.save:\n        parser.error(\"argument --output-dir: only allowed with --save\")\n    if (args.cert is None) != (args.key is None):\n        parser.error(\"arguments --cert and --key must be given together\")\n    if args.since and args.until and args.until \u003C args.since:\n        parser.error(f\"argument --until: {args.until} is before --since {args.since}\")\n\n\ndef main(argv: Sequence[str] | None = None) -> int:\n    parser = build_parser()\n    args = parser.parse_args(argv)\n    validate(parser, args)\n    print(f\"format={args.format} save={args.save} dry_run={args.dry_run}\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n","python","",[14,121,122,131,149,156,165,178,191,196,201,214,249,254,265,306,341,376,391,396,406,441,473,478,506,533,558,583,602,621,630,635,640,657,663,689,700,728,738,762,797,802,807,842,853,864,870,915,923,928,933,950],{"__ignoreMap":119},[123,124,127],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sJ8bj","# src\u002Fmytool\u002Fcli.py\n",[123,132,134,138,142,145],{"class":125,"line":133},2,[123,135,137],{"class":136},"szBVR","from",[123,139,141],{"class":140},"sj4cs"," __future__",[123,143,144],{"class":136}," import",[123,146,148],{"class":147},"sVt8B"," annotations\n",[123,150,152],{"class":125,"line":151},3,[123,153,155],{"emptyLinePlaceholder":154},true,"\n",[123,157,159,162],{"class":125,"line":158},4,[123,160,161],{"class":136},"import",[123,163,164],{"class":147}," argparse\n",[123,166,168,170,173,175],{"class":125,"line":167},5,[123,169,137],{"class":136},[123,171,172],{"class":147}," collections.abc ",[123,174,161],{"class":136},[123,176,177],{"class":147}," Sequence\n",[123,179,181,183,186,188],{"class":125,"line":180},6,[123,182,137],{"class":136},[123,184,185],{"class":147}," pathlib ",[123,187,161],{"class":136},[123,189,190],{"class":147}," Path\n",[123,192,194],{"class":125,"line":193},7,[123,195,155],{"emptyLinePlaceholder":154},[123,197,199],{"class":125,"line":198},8,[123,200,155],{"emptyLinePlaceholder":154},[123,202,204,207,211],{"class":125,"line":203},9,[123,205,206],{"class":136},"def",[123,208,210],{"class":209},"sScJk"," build_parser",[123,212,213],{"class":147},"() -> argparse.ArgumentParser:\n",[123,215,217,220,223,226,230,232,236,238,241,243,246],{"class":125,"line":216},10,[123,218,219],{"class":147},"    p ",[123,221,222],{"class":136},"=",[123,224,225],{"class":147}," argparse.ArgumentParser(",[123,227,229],{"class":228},"s4XuR","prog",[123,231,222],{"class":136},[123,233,235],{"class":234},"sZZnC","\"report\"",[123,237,36],{"class":147},[123,239,240],{"class":228},"allow_abbrev",[123,242,222],{"class":136},[123,244,245],{"class":140},"False",[123,247,248],{"class":147},")\n",[123,250,252],{"class":125,"line":251},11,[123,253,155],{"emptyLinePlaceholder":154},[123,255,257,260,262],{"class":125,"line":256},12,[123,258,259],{"class":147},"    fmt ",[123,261,222],{"class":136},[123,263,264],{"class":147}," p.add_mutually_exclusive_group()\n",[123,266,268,271,274,276,279,281,284,286,289,291,294,296,299,301,304],{"class":125,"line":267},13,[123,269,270],{"class":147},"    fmt.add_argument(",[123,272,273],{"class":234},"\"--json\"",[123,275,36],{"class":147},[123,277,278],{"class":228},"dest",[123,280,222],{"class":136},[123,282,283],{"class":234},"\"format\"",[123,285,36],{"class":147},[123,287,288],{"class":228},"action",[123,290,222],{"class":136},[123,292,293],{"class":234},"\"store_const\"",[123,295,36],{"class":147},[123,297,298],{"class":228},"const",[123,300,222],{"class":136},[123,302,303],{"class":234},"\"json\"",[123,305,248],{"class":147},[123,307,309,311,314,316,318,320,322,324,326,328,330,332,334,336,339],{"class":125,"line":308},14,[123,310,270],{"class":147},[123,312,313],{"class":234},"\"--csv\"",[123,315,36],{"class":147},[123,317,278],{"class":228},[123,319,222],{"class":136},[123,321,283],{"class":234},[123,323,36],{"class":147},[123,325,288],{"class":228},[123,327,222],{"class":136},[123,329,293],{"class":234},[123,331,36],{"class":147},[123,333,298],{"class":228},[123,335,222],{"class":136},[123,337,338],{"class":234},"\"csv\"",[123,340,248],{"class":147},[123,342,344,346,349,351,353,355,357,359,361,363,365,367,369,371,374],{"class":125,"line":343},15,[123,345,270],{"class":147},[123,347,348],{"class":234},"\"--table\"",[123,350,36],{"class":147},[123,352,278],{"class":228},[123,354,222],{"class":136},[123,356,283],{"class":234},[123,358,36],{"class":147},[123,360,288],{"class":228},[123,362,222],{"class":136},[123,364,293],{"class":234},[123,366,36],{"class":147},[123,368,298],{"class":228},[123,370,222],{"class":136},[123,372,373],{"class":234},"\"table\"",[123,375,248],{"class":147},[123,377,379,382,385,387,389],{"class":125,"line":378},16,[123,380,381],{"class":147},"    p.set_defaults(",[123,383,384],{"class":228},"format",[123,386,222],{"class":136},[123,388,373],{"class":234},[123,390,248],{"class":147},[123,392,394],{"class":125,"line":393},17,[123,395,155],{"emptyLinePlaceholder":154},[123,397,399,402,404],{"class":125,"line":398},18,[123,400,401],{"class":147},"    mode ",[123,403,222],{"class":136},[123,405,264],{"class":147},[123,407,409,412,415,417,420,422,424,426,429,431,434,436,439],{"class":125,"line":408},19,[123,410,411],{"class":147},"    mode.add_argument(",[123,413,414],{"class":234},"\"--dry-run\"",[123,416,36],{"class":147},[123,418,419],{"class":234},"\"-n\"",[123,421,36],{"class":147},[123,423,288],{"class":228},[123,425,222],{"class":136},[123,427,428],{"class":234},"\"store_true\"",[123,430,36],{"class":147},[123,432,433],{"class":228},"help",[123,435,222],{"class":136},[123,437,438],{"class":234},"\"Show what would be written.\"",[123,440,248],{"class":147},[123,442,444,446,449,451,454,456,458,460,462,464,466,468,471],{"class":125,"line":443},20,[123,445,411],{"class":147},[123,447,448],{"class":234},"\"--force\"",[123,450,36],{"class":147},[123,452,453],{"class":234},"\"-f\"",[123,455,36],{"class":147},[123,457,288],{"class":228},[123,459,222],{"class":136},[123,461,428],{"class":234},[123,463,36],{"class":147},[123,465,433],{"class":228},[123,467,222],{"class":136},[123,469,470],{"class":234},"\"Overwrite existing output.\"",[123,472,248],{"class":147},[123,474,476],{"class":125,"line":475},21,[123,477,155],{"emptyLinePlaceholder":154},[123,479,481,484,487,489,492,494,497,499,501,504],{"class":125,"line":480},22,[123,482,483],{"class":147},"    p.add_argument(",[123,485,486],{"class":234},"\"--output-dir\"",[123,488,36],{"class":147},[123,490,491],{"class":228},"type",[123,493,222],{"class":136},[123,495,496],{"class":147},"Path, ",[123,498,433],{"class":228},[123,500,222],{"class":136},[123,502,503],{"class":234},"\"Write the report here (requires --save).\"",[123,505,248],{"class":147},[123,507,509,511,514,516,518,520,522,524,526,528,531],{"class":125,"line":508},23,[123,510,483],{"class":147},[123,512,513],{"class":234},"\"--save\"",[123,515,36],{"class":147},[123,517,288],{"class":228},[123,519,222],{"class":136},[123,521,428],{"class":234},[123,523,36],{"class":147},[123,525,433],{"class":228},[123,527,222],{"class":136},[123,529,530],{"class":234},"\"Save the report instead of printing it.\"",[123,532,248],{"class":147},[123,534,536,538,541,543,545,547,549,551,553,556],{"class":125,"line":535},24,[123,537,483],{"class":147},[123,539,540],{"class":234},"\"--cert\"",[123,542,36],{"class":147},[123,544,491],{"class":228},[123,546,222],{"class":136},[123,548,496],{"class":147},[123,550,433],{"class":228},[123,552,222],{"class":136},[123,554,555],{"class":234},"\"Client certificate (requires --key).\"",[123,557,248],{"class":147},[123,559,561,563,566,568,570,572,574,576,578,581],{"class":125,"line":560},25,[123,562,483],{"class":147},[123,564,565],{"class":234},"\"--key\"",[123,567,36],{"class":147},[123,569,491],{"class":228},[123,571,222],{"class":136},[123,573,496],{"class":147},[123,575,433],{"class":228},[123,577,222],{"class":136},[123,579,580],{"class":234},"\"Client key (requires --cert).\"",[123,582,248],{"class":147},[123,584,586,588,591,593,595,597,600],{"class":125,"line":585},26,[123,587,483],{"class":147},[123,589,590],{"class":234},"\"--since\"",[123,592,36],{"class":147},[123,594,433],{"class":228},[123,596,222],{"class":136},[123,598,599],{"class":234},"\"Start date, YYYY-MM-DD.\"",[123,601,248],{"class":147},[123,603,605,607,610,612,614,616,619],{"class":125,"line":604},27,[123,606,483],{"class":147},[123,608,609],{"class":234},"\"--until\"",[123,611,36],{"class":147},[123,613,433],{"class":228},[123,615,222],{"class":136},[123,617,618],{"class":234},"\"End date, YYYY-MM-DD; must not be before --since.\"",[123,620,248],{"class":147},[123,622,624,627],{"class":125,"line":623},28,[123,625,626],{"class":136},"    return",[123,628,629],{"class":147}," p\n",[123,631,633],{"class":125,"line":632},29,[123,634,155],{"emptyLinePlaceholder":154},[123,636,638],{"class":125,"line":637},30,[123,639,155],{"emptyLinePlaceholder":154},[123,641,643,645,648,651,654],{"class":125,"line":642},31,[123,644,206],{"class":136},[123,646,647],{"class":209}," validate",[123,649,650],{"class":147},"(parser: argparse.ArgumentParser, args: argparse.Namespace) -> ",[123,652,653],{"class":140},"None",[123,655,656],{"class":147},":\n",[123,658,660],{"class":125,"line":659},32,[123,661,662],{"class":234},"    \"\"\"Cross-option rules that exclusive groups cannot express.\"\"\"\n",[123,664,666,669,672,675,678,681,684,686],{"class":125,"line":665},33,[123,667,668],{"class":136},"    if",[123,670,671],{"class":147}," args.output_dir ",[123,673,674],{"class":136},"is",[123,676,677],{"class":136}," not",[123,679,680],{"class":140}," None",[123,682,683],{"class":136}," and",[123,685,677],{"class":136},[123,687,688],{"class":147}," args.save:\n",[123,690,692,695,698],{"class":125,"line":691},34,[123,693,694],{"class":147},"        parser.error(",[123,696,697],{"class":234},"\"argument --output-dir: only allowed with --save\"",[123,699,248],{"class":147},[123,701,703,705,708,710,712,715,718,721,723,725],{"class":125,"line":702},35,[123,704,668],{"class":136},[123,706,707],{"class":147}," (args.cert ",[123,709,674],{"class":136},[123,711,680],{"class":140},[123,713,714],{"class":147},") ",[123,716,717],{"class":136},"!=",[123,719,720],{"class":147}," (args.key ",[123,722,674],{"class":136},[123,724,680],{"class":140},[123,726,727],{"class":147},"):\n",[123,729,731,733,736],{"class":125,"line":730},36,[123,732,694],{"class":147},[123,734,735],{"class":234},"\"arguments --cert and --key must be given together\"",[123,737,248],{"class":147},[123,739,741,743,746,749,752,754,756,759],{"class":125,"line":740},37,[123,742,668],{"class":136},[123,744,745],{"class":147}," args.since ",[123,747,748],{"class":136},"and",[123,750,751],{"class":147}," args.until ",[123,753,748],{"class":136},[123,755,751],{"class":147},[123,757,758],{"class":136},"\u003C",[123,760,761],{"class":147}," args.since:\n",[123,763,765,767,770,773,776,779,782,785,787,790,792,795],{"class":125,"line":764},38,[123,766,694],{"class":147},[123,768,769],{"class":136},"f",[123,771,772],{"class":234},"\"argument --until: ",[123,774,775],{"class":140},"{",[123,777,778],{"class":147},"args.until",[123,780,781],{"class":140},"}",[123,783,784],{"class":234}," is before --since ",[123,786,775],{"class":140},[123,788,789],{"class":147},"args.since",[123,791,781],{"class":140},[123,793,794],{"class":234},"\"",[123,796,248],{"class":147},[123,798,800],{"class":125,"line":799},39,[123,801,155],{"emptyLinePlaceholder":154},[123,803,805],{"class":125,"line":804},40,[123,806,155],{"emptyLinePlaceholder":154},[123,808,810,812,815,818,821,824,827,829,832,834,837,840],{"class":125,"line":809},41,[123,811,206],{"class":136},[123,813,814],{"class":209}," main",[123,816,817],{"class":147},"(argv: Sequence[",[123,819,820],{"class":140},"str",[123,822,823],{"class":147},"] ",[123,825,826],{"class":136},"|",[123,828,680],{"class":140},[123,830,831],{"class":136}," =",[123,833,680],{"class":140},[123,835,836],{"class":147},") -> ",[123,838,839],{"class":140},"int",[123,841,656],{"class":147},[123,843,845,848,850],{"class":125,"line":844},42,[123,846,847],{"class":147},"    parser ",[123,849,222],{"class":136},[123,851,852],{"class":147}," build_parser()\n",[123,854,856,859,861],{"class":125,"line":855},43,[123,857,858],{"class":147},"    args ",[123,860,222],{"class":136},[123,862,863],{"class":147}," parser.parse_args(argv)\n",[123,865,867],{"class":125,"line":866},44,[123,868,869],{"class":147},"    validate(parser, args)\n",[123,871,873,876,879,881,884,886,889,891,894,896,899,901,904,906,909,911,913],{"class":125,"line":872},45,[123,874,875],{"class":140},"    print",[123,877,878],{"class":147},"(",[123,880,769],{"class":136},[123,882,883],{"class":234},"\"format=",[123,885,775],{"class":140},[123,887,888],{"class":147},"args.format",[123,890,781],{"class":140},[123,892,893],{"class":234}," save=",[123,895,775],{"class":140},[123,897,898],{"class":147},"args.save",[123,900,781],{"class":140},[123,902,903],{"class":234}," dry_run=",[123,905,775],{"class":140},[123,907,908],{"class":147},"args.dry_run",[123,910,781],{"class":140},[123,912,794],{"class":234},[123,914,248],{"class":147},[123,916,918,920],{"class":125,"line":917},46,[123,919,626],{"class":136},[123,921,922],{"class":140}," 0\n",[123,924,926],{"class":125,"line":925},47,[123,927,155],{"emptyLinePlaceholder":154},[123,929,931],{"class":125,"line":930},48,[123,932,155],{"emptyLinePlaceholder":154},[123,934,936,939,942,945,948],{"class":125,"line":935},49,[123,937,938],{"class":136},"if",[123,940,941],{"class":140}," __name__",[123,943,944],{"class":136}," ==",[123,946,947],{"class":234}," \"__main__\"",[123,949,656],{"class":147},[123,951,953,956,959],{"class":125,"line":952},50,[123,954,955],{"class":136},"    raise",[123,957,958],{"class":140}," SystemExit",[123,960,961],{"class":147},"(main())\n",[963,964,966],"h3",{"id":965},"design-notes","Design notes",[10,968,969,975,976,36,978,980,981,984,985,988,989,991,992,995,996,999],{},[46,970,971,972,974],{},"Share one ",[14,973,278],{}," for format flags."," Pointing ",[14,977,16],{},[14,979,20],{}," and ",[14,982,983],{},"--table"," at the same destination with ",[14,986,987],{},"store_const"," gives the program one value to read (",[14,990,888],{},") instead of three booleans to reconcile. The group guarantees only one can set it; ",[14,993,994],{},"set_defaults"," supplies the default. This is often cleaner than a ",[14,997,998],{},"--format {json,csv,table}"," option when each format is common enough to deserve its own short flag — and you can offer both.",[10,1001,1002,1005,1006,980,1008,1010],{},[46,1003,1004],{},"Use exclusive groups for \"at most one\", and nothing else."," ",[14,1007,24],{},[14,1009,27],{}," are a natural group: combining \"change nothing\" with \"overwrite everything\" is contradictory, and an error is far better than silently preferring one.",[10,1012,1013,1019,1020,32,1022,1024,1025,980,1027,1029,1030,1033,1034,1037,1038,1040],{},[46,1014,1015,1016,55],{},"Validate dependencies after parsing, with ",[14,1017,1018],{},"parser.error()"," \"",[14,1021,39],{},[14,1023,43],{},"\" and \"",[14,1026,31],{},[14,1028,35],{}," go together\" cannot be expressed by groups. A ",[14,1031,1032],{},"validate"," function run right after ",[14,1035,1036],{},"parse_args"," handles them, and ",[14,1039,1018],{}," makes the errors look and exit exactly like argparse's own — usage line, message, status 2.",[10,1042,1043,1019,1046,1049,1050,1053,1054,1057],{},[46,1044,1045],{},"Validate value relationships there too.",[14,1047,1048],{},"--until"," must not be before ",[14,1051,1052],{},"--since","\" involves values, not presence. ISO dates compare correctly as strings, which keeps the example short; in real code, convert with ",[14,1055,1056],{},"type=date.fromisoformat"," first.",[80,1059],{"name":1060},"mx-decision",[963,1062,1064],{"id":1063},"conflicts-that-involve-config-files-and-environment-variables","Conflicts that involve config files and environment variables",[10,1066,1067,1068,1071,1072,1074,1075,1078],{},"Exclusive groups only see the command line. Real tools also read configuration files and environment variables, and conflicts can span sources: a config file sets ",[14,1069,1070],{},"format = \"csv\""," while the user types ",[14,1073,16],{},". That is not a conflict — the command line should simply win, following the usual precedence of flags over environment over files over defaults. The rules in this guide should therefore be checked on the ",[46,1076,1077],{},"merged"," settings, after precedence has been applied, not only on the parsed arguments.",[10,1080,1081,1082,1084,1085,1087,1088,1091,1092,17,1095,1098,1099,55],{},"The practical shape is: parse arguments, load configuration, merge with flags taking priority, then run ",[14,1083,1032],{}," on the merged result. Error messages should then say where each conflicting value came from — \"",[14,1086,31],{}," given on the command line but ",[14,1089,1090],{},"key"," is not set in the command line, ",[14,1093,1094],{},"MYTOOL_KEY",[14,1096,1097],{},"~\u002F.config\u002Fmytool\u002Fconfig.toml","\" — because a user who never typed an option cannot fix a conflict they do not know about. Keeping track of each value's source during merging makes this straightforward; see ",[51,1100,1102],{"href":1101},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence: flags, env, files and defaults",[57,1104,1106],{"id":1105},"groups-and-subparsers","Groups and subparsers",[10,1108,1109,1110,1113,1114,1117,1118,55],{},"Exclusive groups belong to one parser, so each subcommand has its own. Rules that apply to several subcommands can live in a parent parser passed with ",[14,1111,1112],{},"parents=[...]"," — the group is copied into each subparser. There is one limitation to know: argparse does not support nesting groups (an exclusive group inside an argument group was deprecated in Python 3.11), so if you want grouped help sections ",[1115,1116,748],"em",{}," exclusivity, keep the exclusive group at the top level and describe it in the option help. Subparser structure is covered in ",[51,1119,1121],{"href":1120},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands\u002F","argparse subparsers for subcommands",[57,1123,1125],{"id":1124},"ux-considerations","UX considerations",[80,1127],{"name":1128},"mx-terminal",[62,1130,1131,1137,1143,1152,1161],{},[65,1132,1133,1136],{},[46,1134,1135],{},"Error, never silently prefer."," When two options conflict, the user meant something by each; picking one quietly guarantees surprise.",[65,1138,1139,1142],{},[46,1140,1141],{},"Name both sides."," \"argument --csv: not allowed with argument --json\" (argparse's own wording) and \"--cert and --key must be given together\" tell the user exactly which options are involved.",[65,1144,1145,1148,1149,1151],{},[46,1146,1147],{},"Say it in help too."," \"(requires --save)\" in the help text for ",[14,1150,39],{}," teaches the rule before anyone breaks it.",[65,1153,1154,1005,1157,1160],{},[46,1155,1156],{},"Prefer a required positional or choice over a required group when natural.",[14,1158,1159],{},"report {json,csv,table}"," may read better than a required group of three flags; required options are often a sign an argument wants to be positional.",[65,1162,1163,1166,1167,1169,1170,1172],{},[46,1164,1165],{},"Keep validation next to parsing."," Running ",[14,1168,1032],{}," immediately after ",[14,1171,1036],{}," means the rest of the program can assume the rules hold, instead of re-checking them in every function.",[57,1174,1176],{"id":1175},"testing-the-behaviour","Testing the behaviour",[10,1178,1179,1180,1182,1183,1186,1187,1190],{},"Every rule deserves a test for the accepted combination and the rejected one. ",[14,1181,1036],{}," raises ",[14,1184,1185],{},"SystemExit(2)"," on errors, and ",[14,1188,1189],{},"capsys"," captures the message:",[114,1192,1194],{"className":116,"code":1193,"language":118,"meta":119,"style":119},"# tests\u002Ftest_rules.py\nimport pytest\n\nfrom mytool.cli import build_parser, main, validate\n\n\ndef run(argv):\n    parser = build_parser()\n    args = parser.parse_args(argv)\n    validate(parser, args)\n    return args\n\n\ndef assert_usage_error(argv, capsys, fragment):\n    with pytest.raises(SystemExit) as info:\n        run(argv)\n    assert info.value.code == 2\n    assert fragment in capsys.readouterr().err\n\n\ndef test_format_defaults_to_table():\n    assert run([]).format == \"table\"\n\n\ndef test_one_format_flag():\n    assert run([\"--csv\"]).format == \"csv\"\n\n\ndef test_two_format_flags_conflict(capsys):\n    assert_usage_error([\"--json\", \"--csv\"], capsys, \"not allowed with argument --json\")\n\n\ndef test_dry_run_and_force_conflict(capsys):\n    assert_usage_error([\"-n\", \"-f\"], capsys, \"not allowed with\")\n\n\ndef test_output_dir_requires_save(capsys):\n    assert_usage_error([\"--output-dir\", \"out\"], capsys, \"only allowed with --save\")\n    assert run([\"--output-dir\", \"out\", \"--save\"]).save\n\n\n@pytest.mark.parametrize(\"argv\", [[\"--cert\", \"c.pem\"], [\"--key\", \"k.pem\"]])\ndef test_cert_and_key_together(argv, capsys):\n    assert_usage_error(argv, capsys, \"must be given together\")\n\n\ndef test_until_before_since(capsys):\n    assert_usage_error([\"--since\", \"2026-09-10\", \"--until\", \"2026-09-01\"], capsys, \"is before\")\n\n\ndef test_main_runs():\n    assert main([\"--json\", \"--save\"]) == 0\n",[14,1195,1196,1201,1208,1212,1224,1228,1232,1242,1250,1258,1262,1269,1273,1277,1287,1306,1311,1325,1338,1342,1346,1356,1368,1372,1376,1385,1402,1406,1410,1420,1439,1443,1447,1456,1473,1477,1481,1490,1508,1527,1531,1535,1568,1578,1588,1592,1596,1605,1632,1636,1640,1650],{"__ignoreMap":119},[123,1197,1198],{"class":125,"line":126},[123,1199,1200],{"class":129},"# tests\u002Ftest_rules.py\n",[123,1202,1203,1205],{"class":125,"line":133},[123,1204,161],{"class":136},[123,1206,1207],{"class":147}," pytest\n",[123,1209,1210],{"class":125,"line":151},[123,1211,155],{"emptyLinePlaceholder":154},[123,1213,1214,1216,1219,1221],{"class":125,"line":158},[123,1215,137],{"class":136},[123,1217,1218],{"class":147}," mytool.cli ",[123,1220,161],{"class":136},[123,1222,1223],{"class":147}," build_parser, main, validate\n",[123,1225,1226],{"class":125,"line":167},[123,1227,155],{"emptyLinePlaceholder":154},[123,1229,1230],{"class":125,"line":180},[123,1231,155],{"emptyLinePlaceholder":154},[123,1233,1234,1236,1239],{"class":125,"line":193},[123,1235,206],{"class":136},[123,1237,1238],{"class":209}," run",[123,1240,1241],{"class":147},"(argv):\n",[123,1243,1244,1246,1248],{"class":125,"line":198},[123,1245,847],{"class":147},[123,1247,222],{"class":136},[123,1249,852],{"class":147},[123,1251,1252,1254,1256],{"class":125,"line":203},[123,1253,858],{"class":147},[123,1255,222],{"class":136},[123,1257,863],{"class":147},[123,1259,1260],{"class":125,"line":216},[123,1261,869],{"class":147},[123,1263,1264,1266],{"class":125,"line":251},[123,1265,626],{"class":136},[123,1267,1268],{"class":147}," args\n",[123,1270,1271],{"class":125,"line":256},[123,1272,155],{"emptyLinePlaceholder":154},[123,1274,1275],{"class":125,"line":267},[123,1276,155],{"emptyLinePlaceholder":154},[123,1278,1279,1281,1284],{"class":125,"line":308},[123,1280,206],{"class":136},[123,1282,1283],{"class":209}," assert_usage_error",[123,1285,1286],{"class":147},"(argv, capsys, fragment):\n",[123,1288,1289,1292,1295,1298,1300,1303],{"class":125,"line":343},[123,1290,1291],{"class":136},"    with",[123,1293,1294],{"class":147}," pytest.raises(",[123,1296,1297],{"class":140},"SystemExit",[123,1299,714],{"class":147},[123,1301,1302],{"class":136},"as",[123,1304,1305],{"class":147}," info:\n",[123,1307,1308],{"class":125,"line":378},[123,1309,1310],{"class":147},"        run(argv)\n",[123,1312,1313,1316,1319,1322],{"class":125,"line":393},[123,1314,1315],{"class":136},"    assert",[123,1317,1318],{"class":147}," info.value.code ",[123,1320,1321],{"class":136},"==",[123,1323,1324],{"class":140}," 2\n",[123,1326,1327,1329,1332,1335],{"class":125,"line":398},[123,1328,1315],{"class":136},[123,1330,1331],{"class":147}," fragment ",[123,1333,1334],{"class":136},"in",[123,1336,1337],{"class":147}," capsys.readouterr().err\n",[123,1339,1340],{"class":125,"line":408},[123,1341,155],{"emptyLinePlaceholder":154},[123,1343,1344],{"class":125,"line":443},[123,1345,155],{"emptyLinePlaceholder":154},[123,1347,1348,1350,1353],{"class":125,"line":475},[123,1349,206],{"class":136},[123,1351,1352],{"class":209}," test_format_defaults_to_table",[123,1354,1355],{"class":147},"():\n",[123,1357,1358,1360,1363,1365],{"class":125,"line":480},[123,1359,1315],{"class":136},[123,1361,1362],{"class":147}," run([]).format ",[123,1364,1321],{"class":136},[123,1366,1367],{"class":234}," \"table\"\n",[123,1369,1370],{"class":125,"line":508},[123,1371,155],{"emptyLinePlaceholder":154},[123,1373,1374],{"class":125,"line":535},[123,1375,155],{"emptyLinePlaceholder":154},[123,1377,1378,1380,1383],{"class":125,"line":560},[123,1379,206],{"class":136},[123,1381,1382],{"class":209}," test_one_format_flag",[123,1384,1355],{"class":147},[123,1386,1387,1389,1392,1394,1397,1399],{"class":125,"line":585},[123,1388,1315],{"class":136},[123,1390,1391],{"class":147}," run([",[123,1393,313],{"class":234},[123,1395,1396],{"class":147},"]).format ",[123,1398,1321],{"class":136},[123,1400,1401],{"class":234}," \"csv\"\n",[123,1403,1404],{"class":125,"line":604},[123,1405,155],{"emptyLinePlaceholder":154},[123,1407,1408],{"class":125,"line":623},[123,1409,155],{"emptyLinePlaceholder":154},[123,1411,1412,1414,1417],{"class":125,"line":632},[123,1413,206],{"class":136},[123,1415,1416],{"class":209}," test_two_format_flags_conflict",[123,1418,1419],{"class":147},"(capsys):\n",[123,1421,1422,1425,1427,1429,1431,1434,1437],{"class":125,"line":637},[123,1423,1424],{"class":147},"    assert_usage_error([",[123,1426,273],{"class":234},[123,1428,36],{"class":147},[123,1430,313],{"class":234},[123,1432,1433],{"class":147},"], capsys, ",[123,1435,1436],{"class":234},"\"not allowed with argument --json\"",[123,1438,248],{"class":147},[123,1440,1441],{"class":125,"line":642},[123,1442,155],{"emptyLinePlaceholder":154},[123,1444,1445],{"class":125,"line":659},[123,1446,155],{"emptyLinePlaceholder":154},[123,1448,1449,1451,1454],{"class":125,"line":665},[123,1450,206],{"class":136},[123,1452,1453],{"class":209}," test_dry_run_and_force_conflict",[123,1455,1419],{"class":147},[123,1457,1458,1460,1462,1464,1466,1468,1471],{"class":125,"line":691},[123,1459,1424],{"class":147},[123,1461,419],{"class":234},[123,1463,36],{"class":147},[123,1465,453],{"class":234},[123,1467,1433],{"class":147},[123,1469,1470],{"class":234},"\"not allowed with\"",[123,1472,248],{"class":147},[123,1474,1475],{"class":125,"line":702},[123,1476,155],{"emptyLinePlaceholder":154},[123,1478,1479],{"class":125,"line":730},[123,1480,155],{"emptyLinePlaceholder":154},[123,1482,1483,1485,1488],{"class":125,"line":740},[123,1484,206],{"class":136},[123,1486,1487],{"class":209}," test_output_dir_requires_save",[123,1489,1419],{"class":147},[123,1491,1492,1494,1496,1498,1501,1503,1506],{"class":125,"line":764},[123,1493,1424],{"class":147},[123,1495,486],{"class":234},[123,1497,36],{"class":147},[123,1499,1500],{"class":234},"\"out\"",[123,1502,1433],{"class":147},[123,1504,1505],{"class":234},"\"only allowed with --save\"",[123,1507,248],{"class":147},[123,1509,1510,1512,1514,1516,1518,1520,1522,1524],{"class":125,"line":799},[123,1511,1315],{"class":136},[123,1513,1391],{"class":147},[123,1515,486],{"class":234},[123,1517,36],{"class":147},[123,1519,1500],{"class":234},[123,1521,36],{"class":147},[123,1523,513],{"class":234},[123,1525,1526],{"class":147},"]).save\n",[123,1528,1529],{"class":125,"line":804},[123,1530,155],{"emptyLinePlaceholder":154},[123,1532,1533],{"class":125,"line":809},[123,1534,155],{"emptyLinePlaceholder":154},[123,1536,1537,1540,1542,1545,1548,1550,1552,1555,1558,1560,1562,1565],{"class":125,"line":844},[123,1538,1539],{"class":209},"@pytest.mark.parametrize",[123,1541,878],{"class":147},[123,1543,1544],{"class":234},"\"argv\"",[123,1546,1547],{"class":147},", [[",[123,1549,540],{"class":234},[123,1551,36],{"class":147},[123,1553,1554],{"class":234},"\"c.pem\"",[123,1556,1557],{"class":147},"], [",[123,1559,565],{"class":234},[123,1561,36],{"class":147},[123,1563,1564],{"class":234},"\"k.pem\"",[123,1566,1567],{"class":147},"]])\n",[123,1569,1570,1572,1575],{"class":125,"line":855},[123,1571,206],{"class":136},[123,1573,1574],{"class":209}," test_cert_and_key_together",[123,1576,1577],{"class":147},"(argv, capsys):\n",[123,1579,1580,1583,1586],{"class":125,"line":866},[123,1581,1582],{"class":147},"    assert_usage_error(argv, capsys, ",[123,1584,1585],{"class":234},"\"must be given together\"",[123,1587,248],{"class":147},[123,1589,1590],{"class":125,"line":872},[123,1591,155],{"emptyLinePlaceholder":154},[123,1593,1594],{"class":125,"line":917},[123,1595,155],{"emptyLinePlaceholder":154},[123,1597,1598,1600,1603],{"class":125,"line":925},[123,1599,206],{"class":136},[123,1601,1602],{"class":209}," test_until_before_since",[123,1604,1419],{"class":147},[123,1606,1607,1609,1611,1613,1616,1618,1620,1622,1625,1627,1630],{"class":125,"line":930},[123,1608,1424],{"class":147},[123,1610,590],{"class":234},[123,1612,36],{"class":147},[123,1614,1615],{"class":234},"\"2026-09-10\"",[123,1617,36],{"class":147},[123,1619,609],{"class":234},[123,1621,36],{"class":147},[123,1623,1624],{"class":234},"\"2026-09-01\"",[123,1626,1433],{"class":147},[123,1628,1629],{"class":234},"\"is before\"",[123,1631,248],{"class":147},[123,1633,1634],{"class":125,"line":935},[123,1635,155],{"emptyLinePlaceholder":154},[123,1637,1638],{"class":125,"line":952},[123,1639,155],{"emptyLinePlaceholder":154},[123,1641,1643,1645,1648],{"class":125,"line":1642},51,[123,1644,206],{"class":136},[123,1646,1647],{"class":209}," test_main_runs",[123,1649,1355],{"class":147},[123,1651,1653,1655,1658,1660,1662,1664,1667,1669],{"class":125,"line":1652},52,[123,1654,1315],{"class":136},[123,1656,1657],{"class":147}," main([",[123,1659,273],{"class":234},[123,1661,36],{"class":147},[123,1663,513],{"class":234},[123,1665,1666],{"class":147},"]) ",[123,1668,1321],{"class":136},[123,1670,922],{"class":140},[10,1672,1673,1674,1677,1678,1681,1682,1684,1685,1687],{},"Testing ",[14,1675,1676],{},"run()"," rather than only ",[14,1679,1680],{},"parse_args()"," ensures the post-parse rules are exercised exactly as the real entry point applies them. Parametrised tests keep symmetric rules — ",[14,1683,31],{}," without ",[14,1686,35],{}," and vice versa — to a single function.",[57,1689,1691],{"id":1690},"conclusion","Conclusion",[10,1693,1694,1695,1697,1698,1700,1701,1703],{},"Use argparse's mutually exclusive groups for \"at most one of these\" and ",[14,1696,95],{}," for \"exactly one\"; they give you correct errors and usage lines for free. For everything groups cannot express — options that require each other, options that only make sense with another, rules about values — add a ",[14,1699,1032],{}," step immediately after parsing that reports problems with ",[14,1702,1018],{},". Never resolve a conflict by silently preferring one option, mention the rules in help text, and give every rule an accepting and a rejecting test.",[57,1705,1707],{"id":1706},"frequently-asked-questions","Frequently asked questions",[963,1709,1711],{"id":1710},"can-an-exclusive-group-include-a-positional-argument","Can an exclusive group include a positional argument?",[10,1713,1714,1715,1718,1719,1005,1722,1725],{},"Yes, if the positional is optional (",[14,1716,1717],{},"nargs=\"?\"","), so that it can be absent when another member is used. For example, a source file ",[1115,1720,1721],{},"or",[14,1723,1724],{},"--stdin",". Required positionals cannot be in an exclusive group.",[963,1727,1729],{"id":1728},"how-do-i-make-none-of-these-or-exactly-one-the-default","How do I make \"none of these, or exactly one\" the default?",[10,1731,1732,1733,1735],{},"That is an ordinary (non-required) exclusive group: zero or one member may be given. ",[14,1734,95],{}," changes it to exactly one.",[963,1737,1739],{"id":1738},"can-groups-express-at-least-one-of-these","Can groups express \"at least one of these\"?",[10,1741,1742,1743,1746],{},"No. Check it after parsing: ",[14,1744,1745],{},"if not (args.a or args.b): parser.error(\"one of --a or --b is required\")",". If exactly one is acceptable, a required exclusive group expresses it more precisely.",[963,1748,1750],{"id":1749},"should-validation-report-every-problem-or-stop-at-the-first","Should validation report every problem or stop at the first?",[10,1752,1753,1754,1756,1757,1759],{},"argparse itself stops at the first error, and ",[14,1755,1018],{}," exits immediately, so the simple approach reports one problem per run. For commands with many interacting options, collecting all violations into a list and reporting them together — then calling ",[14,1758,1018],{}," once with the combined message — saves users a frustrating fix-one-rerun loop. Keep the first-error behaviour for small tools; switch when you see people hitting two rules in a row.",[963,1761,1763,1764,1766],{"id":1762},"does-set_defaults-interact-badly-with-exclusive-groups","Does ",[14,1765,994],{}," interact badly with exclusive groups?",[10,1768,1769,1770,1772,1773,1775,1776,1778],{},"No — defaults apply only when no member was given. Just avoid giving each member its own non-",[14,1771,653],{}," default for a shared ",[14,1774,278],{},", since the last one defined wins and the result is confusing; use ",[14,1777,994],{}," once instead.",[57,1780,1782],{"id":1781},"related","Related",[62,1784,1785,1791,1797,1803,1808],{},[65,1786,1787,1788],{},"Up: ",[51,1789,1790],{"href":53},"Command-line parsing with argparse",[65,1792,1793],{},[51,1794,1796],{"href":1795},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions\u002F","Writing custom argparse actions",[65,1798,1799],{},[51,1800,1802],{"href":1801},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison\u002F","argparse vs Click vs Typer comparison",[65,1804,1805],{},[51,1806,1807],{"href":73},"Validating dependent and conflicting options",[65,1809,1810],{},[51,1811,1813],{"href":1812},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands\u002F","Adding dry-run and confirmation to destructive commands",[1815,1816,1817],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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":119,"searchDepth":133,"depth":133,"links":1819},[1820,1821,1822,1826,1827,1828,1829,1830,1838],{"id":59,"depth":133,"text":60},{"id":77,"depth":133,"text":78},{"id":111,"depth":133,"text":112,"children":1823},[1824,1825],{"id":965,"depth":151,"text":966},{"id":1063,"depth":151,"text":1064},{"id":1105,"depth":133,"text":1106},{"id":1124,"depth":133,"text":1125},{"id":1175,"depth":133,"text":1176},{"id":1690,"depth":133,"text":1691},{"id":1706,"depth":133,"text":1707,"children":1831},[1832,1833,1834,1835,1836],{"id":1710,"depth":151,"text":1711},{"id":1728,"depth":151,"text":1729},{"id":1738,"depth":151,"text":1739},{"id":1749,"depth":151,"text":1750},{"id":1762,"depth":151,"text":1837},"Does set_defaults interact badly with exclusive groups?",{"id":1781,"depth":133,"text":1782},"2026-09-18","Enforce option rules in argparse: mutually exclusive groups, required groups, ‘A requires B’ dependencies, defaults that interact, clear errors and tests for each rule.","beginner",false,"md",{},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse",{"title":5,"description":1840},"modern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse\u002Findex",[1849,1850,1851,118],"argparse","validation","options","UZPujQIJEdmPYc_cOIjy9s7wHJqrrc_XZsbIcNf5dfw",[1854,1857,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887,1890,1893,1896,1899,1902,1905,1908,1911,1914,1917,1920,1923,1926,1929,1932,1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2127,2130,2133,2136,2139,2142,2145,2148,2151,2154,2157,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2194,2197,2200,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2233,2236,2239,2242,2245,2248,2251,2254,2257,2260,2263,2266,2269,2272,2275,2278,2281,2284,2287,2290,2293,2296,2299,2302,2305,2308,2311,2314,2317,2320,2323,2326,2329,2332,2335,2338,2341,2344,2347,2350,2353,2356,2359,2362,2365,2368,2371,2374,2377,2380,2383,2386,2389,2392,2395,2398],{"path":1855,"title":1856},"\u002Fabout","About Python CLI Toolcraft",{"path":1858,"title":1859},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1861,"title":1862},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1864,"title":1865},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":1867,"title":1868},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1870,"title":1871},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":1873,"title":1874},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":1876,"title":1877},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":1879,"title":1880},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":1882,"title":1883},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":1885,"title":1886},"\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":1888,"title":1889},"\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":1891,"title":1892},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1894,"title":1895},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1897,"title":1898},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1900,"title":1901},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":1903,"title":1904},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":1906,"title":1907},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":1909,"title":1910},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":1912,"title":1913},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":1915,"title":1916},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1918,"title":1919},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":1921,"title":1922},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1924,"title":1925},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1927,"title":1928},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1930,"title":1931},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":1933,"title":1934},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1936,"title":1937},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":1939,"title":1940},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1942,"title":1943},"\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":1945,"title":1946},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":1948,"title":1949},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":1951,"title":1952},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1954,"title":1955},"\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":1957,"title":1958},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":1960,"title":1961},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1963,"title":1964},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":1966,"title":1967},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1969,"title":1970},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":1972,"title":1973},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":1975,"title":1976},"\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":1978,"title":1979},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1981,"title":1982},"\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":1984,"title":1985},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":1987,"title":1988},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":1990,"title":1991},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1993,"title":1994},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1996,"title":1997},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1999,"title":2000},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2002,"title":2003},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2005,"title":2006},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2008,"title":2009},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2011,"title":2012},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2014,"title":2015},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2017,"title":2018},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2020,"title":2021},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2023,"title":2024},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2026,"title":2027},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2029,"title":2030},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2032,"title":2033},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2035,"title":2036},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2038,"title":2039},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2041,"title":2042},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2044,"title":2045},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2047,"title":2048},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2050,"title":2051},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2053,"title":2054},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2056,"title":2057},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2059,"title":2060},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2062,"title":2063},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2065,"title":2066},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2068,"title":2069},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2071,"title":2072},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2074,"title":2075},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2077,"title":2078},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2080,"title":2081},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2083,"title":2084},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2086,"title":2087},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2089,"title":2090},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2092,"title":2093},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2095,"title":2096},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2098,"title":2099},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2101,"title":2102},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2104,"title":2105},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2107,"title":2108},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2110,"title":2111},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2113,"title":2114},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2116,"title":2117},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2119,"title":2120},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2122,"title":2123},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2125,"title":2126},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2128,"title":2129},"\u002F","Python CLI Toolcraft",{"path":2131,"title":2132},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2134,"title":2135},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2137,"title":2138},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2140,"title":2141},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2143,"title":2144},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2146,"title":2147},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2149,"title":2150},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2152,"title":2153},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2155,"title":2156},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1845,"title":5},{"path":2159,"title":2160},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2162,"title":2163},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2165,"title":2166},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2168,"title":2169},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2171,"title":2172},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2174,"title":2175},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2177,"title":2178},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2180,"title":2181},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2183,"title":2184},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2186,"title":2187},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2189,"title":2190},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2192,"title":2193},"\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":2195,"title":2196},"\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":2198,"title":2199},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2201,"title":2202},"\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":2204,"title":2205},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2207,"title":2208},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2210,"title":2211},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2213,"title":2214},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2216,"title":2217},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2219,"title":2220},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2222,"title":2223},"\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":2225,"title":2226},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2228,"title":2229},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2231,"title":2232},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2234,"title":2235},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2237,"title":2238},"\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":2240,"title":2241},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2243,"title":2244},"\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":2246,"title":2247},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2249,"title":2250},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2252,"title":2253},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2255,"title":2256},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2258,"title":2259},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2261,"title":2262},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2264,"title":2265},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2267,"title":2268},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2270,"title":2271},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2273,"title":2274},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2276,"title":2277},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2279,"title":2280},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2282,"title":2283},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2285,"title":2286},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2288,"title":2289},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2291,"title":2292},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2294,"title":2295},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2297,"title":2298},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2300,"title":2301},"\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":2303,"title":2304},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2306,"title":2307},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2309,"title":2310},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2312,"title":2313},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2315,"title":2316},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2318,"title":2319},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2321,"title":2322},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2324,"title":2325},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2327,"title":2328},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2330,"title":2331},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2333,"title":2334},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2336,"title":2337},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2339,"title":2340},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2342,"title":2343},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2345,"title":2346},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2348,"title":2349},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2351,"title":2352},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2354,"title":2355},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2357,"title":2358},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2360,"title":2361},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2363,"title":2364},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2366,"title":2367},"\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":2369,"title":2370},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2372,"title":2373},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2375,"title":2376},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2378,"title":2379},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2381,"title":2382},"\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":2384,"title":2385},"\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":2387,"title":2388},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2390,"title":2391},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2393,"title":2394},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2396,"title":2397},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2399,"title":2400},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736907149]