[{"data":1,"prerenderedAt":2414},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse":2407},{"id":4,"title":5,"body":6,"dateModified":2377,"datePublished":2377,"description":2378,"extension":2379,"faq":2380,"meta":2389,"navigation":249,"path":2399,"seo":2400,"slug":2403,"stem":2404,"type":2405,"__hash__":2406},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002Findex.md","Build a Command-Line Tool for Excel Reports with argparse",{"type":7,"value":8,"toc":2364},"minimark",[9,22,35,135,140,168,182,186,193,748,781,785,792,1169,1184,1274,1278,1284,1653,1692,1696,1699,1784,1805,1809,1816,2072,2093,2097,2236,2240,2247,2251,2267,2271,2278,2297,2303,2309,2313,2316,2325,2328,2360],[10,11,12,13,17,18,21],"p",{},"The constants at the top of a report script — ",[14,15,16],"code",{},"SOURCE = r\"C:\\Reports\\orders.xlsx\"",", ",[14,19,20],{},"MONTH = \"2026-07\""," — are the reason the script belongs to one person. Every rerun for a different month is an edit, every edit is a chance to leave the wrong value in place, and nobody else can run it at all without reading the code first.",[10,23,24,25,28,29,34],{},"Fifteen lines of argparse fixes that, and it does more than save typing: a script with arguments is a script a scheduler can call with different parameters, a test can drive without a subprocess, and a colleague can discover with ",[14,26,27],{},"--help",". This guide is part of ",[30,31,33],"a",{"href":32},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",".",[36,37,45,46,45,50,45,54,45,61,45,71,45,78,45,86,45,91,45,95,45,99,45,102,45,107,45,112,45,117,45,120,45,123,45,126,45,129,45,132],"svg",{"viewBox":38,"role":39,"ariaLabelledBy":40,"xmlns":43,"style":44},"0 0 740 242","img",[41,42],"cli-before-t","cli-before-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[47,48,49],"title",{"id":41},"Editing constants versus passing arguments",[51,52,53],"desc",{"id":42},"With constants at the top of the file, every variation means editing the source, and the edit is easy to leave behind. With a command line, the same script serves the scheduled run, the ad-hoc rerun and the test without being modified at all.",[55,56],"rect",{"x":57,"y":57,"width":58,"height":59,"fill":60},"0","740","242","#ffffff",[55,62],{"x":63,"y":64,"width":65,"height":66,"rx":67,"fill":68,"stroke":69,"style":70},"16","34","336","180","14","#fce9e9","var(--accent-ink,#be185d)","stroke-width:2px",[72,73,77],"text",{"x":74,"y":75,"style":76},"184","62","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","constants in the file",[55,79],{"x":80,"y":81,"width":82,"height":83,"rx":84,"fill":60,"stroke":85},"44","78","280","30","6","var(--line,#cdd5e6)",[72,87,90],{"x":74,"y":88,"style":89},"98","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","SOURCE = \"orders_july.xlsx\"",[72,92,94],{"x":74,"y":93,"style":89},"132","every rerun is a code edit",[72,96,98],{"x":74,"y":97,"style":89},"156","the edit gets committed by accident",[72,100,101],{"x":74,"y":66,"style":89},"one schedule entry, one behaviour",[72,103,106],{"x":74,"y":104,"style":105},"202","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","tests need to monkey-patch module globals",[55,108],{"x":109,"y":64,"width":65,"height":66,"rx":67,"fill":110,"stroke":111,"style":70},"388","#d9f4f1","var(--teal,#0f9488)",[72,113,116],{"x":114,"y":75,"style":115},"556","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","arguments on the command line",[55,118],{"x":119,"y":81,"width":82,"height":83,"rx":84,"fill":60,"stroke":85},"416",[72,121,122],{"x":114,"y":88,"style":89},"report orders.xlsx --month 2026-07",[72,124,125],{"x":114,"y":93,"style":89},"the file never changes",[72,127,128],{"x":114,"y":97,"style":89},"one script, many schedule entries",[72,130,131],{"x":114,"y":66,"style":89},"--help documents itself",[72,133,134],{"x":114,"y":104,"style":105},"tests call parse_args([\"orders.xlsx\"])",[136,137,139],"h2",{"id":138},"prerequisites","Prerequisites",[141,142,147],"pre",{"className":143,"code":144,"language":145,"meta":146,"style":146},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,148,149],{"__ignoreMap":146},[150,151,154,158,162,165],"span",{"class":152,"line":153},"line",1,[150,155,157],{"class":156},"sMTad","pip",[150,159,161],{"class":160},"srMev"," install",[150,163,164],{"class":160}," pandas",[150,166,167],{"class":160}," openpyxl\n",[10,169,170,171,174,175,178,179,34],{},"argparse itself is in the standard library — nothing to install. The examples assume a ",[14,172,173],{},"report.py"," with a ",[14,176,177],{},"build_report(source, target, **options)"," function, as set up in the ",[30,180,181],{"href":32},"parent guide",[136,183,185],{"id":184},"step-1-a-parser-that-types-its-arguments","Step 1: A parser that types its arguments",[10,187,188,189,192],{},"argparse can convert values as it parses, which removes a layer of validation from your own code. ",[14,190,191],{},"type=Path"," gives you a real path object; a small function gives you a real date:",[141,194,198],{"className":195,"code":196,"language":197,"meta":146,"style":146},"language-python shiki shiki-themes github-light github-dark-high-contrast","# cli.py\nimport argparse\nfrom datetime import date, datetime\nfrom pathlib import Path\n\n\ndef month_arg(value):\n    \"\"\"Accept 2026-07 and return the first day of that month.\"\"\"\n    try:\n        return datetime.strptime(value, \"%Y-%m\").date()\n    except ValueError:\n        raise argparse.ArgumentTypeError(\n            f\"expected a month like 2026-07, got {value!r}\")\n\n\ndef build_parser():\n    parser = argparse.ArgumentParser(\n        prog=\"monthly-report\",\n        description=\"Build the monthly regional sales report from an Excel export.\",\n        epilog=\"Example: monthly-report orders.xlsx --month 2026-07 -o july.xlsx\",\n    )\n    parser.add_argument(\"source\", type=Path,\n                        help=\"input workbook exported from the order system\")\n    parser.add_argument(\"-o\", \"--output\", type=Path, default=Path(\"report.xlsx\"),\n                        help=\"where to write the report (default: %(default)s)\")\n    parser.add_argument(\"--sheet\", default=\"Orders\",\n                        help=\"sheet to read from the input (default: %(default)s)\")\n    parser.add_argument(\"--month\", type=month_arg, default=None,\n                        help=\"restrict to one month, e.g. 2026-07\")\n    parser.add_argument(\"--min-amount\", type=float, default=0.0,\n                        help=\"drop rows below this value\")\n    parser.add_argument(\"--dry-run\", action=\"store_true\",\n                        help=\"do everything except write the output file\")\n    parser.add_argument(\"-v\", \"--verbose\", action=\"count\", default=0,\n                        help=\"repeat for more detail: -v, -vv\")\n    return parser\n\n\ndef parse_args(argv=None):\n    return build_parser().parse_args(argv)\n","python",[14,199,200,206,217,231,244,251,256,269,275,284,299,311,320,348,353,358,369,381,396,409,422,428,447,460,496,514,533,549,575,587,615,627,647,659,691,703,712,717,722,740],{"__ignoreMap":146},[150,201,202],{"class":152,"line":153},[150,203,205],{"class":204},"s-wDw","# cli.py\n",[150,207,209,213],{"class":152,"line":208},2,[150,210,212],{"class":211},"s-kum","import",[150,214,216],{"class":215},"skGVy"," argparse\n",[150,218,220,223,226,228],{"class":152,"line":219},3,[150,221,222],{"class":211},"from",[150,224,225],{"class":215}," datetime ",[150,227,212],{"class":211},[150,229,230],{"class":215}," date, datetime\n",[150,232,234,236,239,241],{"class":152,"line":233},4,[150,235,222],{"class":211},[150,237,238],{"class":215}," pathlib ",[150,240,212],{"class":211},[150,242,243],{"class":215}," Path\n",[150,245,247],{"class":152,"line":246},5,[150,248,250],{"emptyLinePlaceholder":249},true,"\n",[150,252,254],{"class":152,"line":253},6,[150,255,250],{"emptyLinePlaceholder":249},[150,257,259,262,266],{"class":152,"line":258},7,[150,260,261],{"class":211},"def",[150,263,265],{"class":264},"s_Opv"," month_arg",[150,267,268],{"class":215},"(value):\n",[150,270,272],{"class":152,"line":271},8,[150,273,274],{"class":160},"    \"\"\"Accept 2026-07 and return the first day of that month.\"\"\"\n",[150,276,278,281],{"class":152,"line":277},9,[150,279,280],{"class":211},"    try",[150,282,283],{"class":215},":\n",[150,285,287,290,293,296],{"class":152,"line":286},10,[150,288,289],{"class":211},"        return",[150,291,292],{"class":215}," datetime.strptime(value, ",[150,294,295],{"class":160},"\"%Y-%m\"",[150,297,298],{"class":215},").date()\n",[150,300,302,305,309],{"class":152,"line":301},11,[150,303,304],{"class":211},"    except",[150,306,308],{"class":307},"sP0c6"," ValueError",[150,310,283],{"class":215},[150,312,314,317],{"class":152,"line":313},12,[150,315,316],{"class":211},"        raise",[150,318,319],{"class":215}," argparse.ArgumentTypeError(\n",[150,321,323,326,329,333,336,339,342,345],{"class":152,"line":322},13,[150,324,325],{"class":211},"            f",[150,327,328],{"class":160},"\"expected a month like 2026-07, got ",[150,330,332],{"class":331},"sSjpA","{",[150,334,335],{"class":215},"value",[150,337,338],{"class":211},"!r",[150,340,341],{"class":331},"}",[150,343,344],{"class":160},"\"",[150,346,347],{"class":215},")\n",[150,349,351],{"class":152,"line":350},14,[150,352,250],{"emptyLinePlaceholder":249},[150,354,356],{"class":152,"line":355},15,[150,357,250],{"emptyLinePlaceholder":249},[150,359,361,363,366],{"class":152,"line":360},16,[150,362,261],{"class":211},[150,364,365],{"class":264}," build_parser",[150,367,368],{"class":215},"():\n",[150,370,372,375,378],{"class":152,"line":371},17,[150,373,374],{"class":215},"    parser ",[150,376,377],{"class":211},"=",[150,379,380],{"class":215}," argparse.ArgumentParser(\n",[150,382,384,388,390,393],{"class":152,"line":383},18,[150,385,387],{"class":386},"sa561","        prog",[150,389,377],{"class":211},[150,391,392],{"class":160},"\"monthly-report\"",[150,394,395],{"class":215},",\n",[150,397,399,402,404,407],{"class":152,"line":398},19,[150,400,401],{"class":386},"        description",[150,403,377],{"class":211},[150,405,406],{"class":160},"\"Build the monthly regional sales report from an Excel export.\"",[150,408,395],{"class":215},[150,410,412,415,417,420],{"class":152,"line":411},20,[150,413,414],{"class":386},"        epilog",[150,416,377],{"class":211},[150,418,419],{"class":160},"\"Example: monthly-report orders.xlsx --month 2026-07 -o july.xlsx\"",[150,421,395],{"class":215},[150,423,425],{"class":152,"line":424},21,[150,426,427],{"class":215},"    )\n",[150,429,431,434,437,439,442,444],{"class":152,"line":430},22,[150,432,433],{"class":215},"    parser.add_argument(",[150,435,436],{"class":160},"\"source\"",[150,438,17],{"class":215},[150,440,441],{"class":386},"type",[150,443,377],{"class":211},[150,445,446],{"class":215},"Path,\n",[150,448,450,453,455,458],{"class":152,"line":449},23,[150,451,452],{"class":386},"                        help",[150,454,377],{"class":211},[150,456,457],{"class":160},"\"input workbook exported from the order system\"",[150,459,347],{"class":215},[150,461,463,465,468,470,473,475,477,479,482,485,487,490,493],{"class":152,"line":462},24,[150,464,433],{"class":215},[150,466,467],{"class":160},"\"-o\"",[150,469,17],{"class":215},[150,471,472],{"class":160},"\"--output\"",[150,474,17],{"class":215},[150,476,441],{"class":386},[150,478,377],{"class":211},[150,480,481],{"class":215},"Path, ",[150,483,484],{"class":386},"default",[150,486,377],{"class":211},[150,488,489],{"class":215},"Path(",[150,491,492],{"class":160},"\"report.xlsx\"",[150,494,495],{"class":215},"),\n",[150,497,499,501,503,506,509,512],{"class":152,"line":498},25,[150,500,452],{"class":386},[150,502,377],{"class":211},[150,504,505],{"class":160},"\"where to write the report (default: ",[150,507,508],{"class":331},"%(default)s",[150,510,511],{"class":160},")\"",[150,513,347],{"class":215},[150,515,517,519,522,524,526,528,531],{"class":152,"line":516},26,[150,518,433],{"class":215},[150,520,521],{"class":160},"\"--sheet\"",[150,523,17],{"class":215},[150,525,484],{"class":386},[150,527,377],{"class":211},[150,529,530],{"class":160},"\"Orders\"",[150,532,395],{"class":215},[150,534,536,538,540,543,545,547],{"class":152,"line":535},27,[150,537,452],{"class":386},[150,539,377],{"class":211},[150,541,542],{"class":160},"\"sheet to read from the input (default: ",[150,544,508],{"class":331},[150,546,511],{"class":160},[150,548,347],{"class":215},[150,550,552,554,557,559,561,563,566,568,570,573],{"class":152,"line":551},28,[150,553,433],{"class":215},[150,555,556],{"class":160},"\"--month\"",[150,558,17],{"class":215},[150,560,441],{"class":386},[150,562,377],{"class":211},[150,564,565],{"class":215},"month_arg, ",[150,567,484],{"class":386},[150,569,377],{"class":211},[150,571,572],{"class":307},"None",[150,574,395],{"class":215},[150,576,578,580,582,585],{"class":152,"line":577},29,[150,579,452],{"class":386},[150,581,377],{"class":211},[150,583,584],{"class":160},"\"restrict to one month, e.g. 2026-07\"",[150,586,347],{"class":215},[150,588,590,592,595,597,599,601,604,606,608,610,613],{"class":152,"line":589},30,[150,591,433],{"class":215},[150,593,594],{"class":160},"\"--min-amount\"",[150,596,17],{"class":215},[150,598,441],{"class":386},[150,600,377],{"class":211},[150,602,603],{"class":307},"float",[150,605,17],{"class":215},[150,607,484],{"class":386},[150,609,377],{"class":211},[150,611,612],{"class":307},"0.0",[150,614,395],{"class":215},[150,616,618,620,622,625],{"class":152,"line":617},31,[150,619,452],{"class":386},[150,621,377],{"class":211},[150,623,624],{"class":160},"\"drop rows below this value\"",[150,626,347],{"class":215},[150,628,630,632,635,637,640,642,645],{"class":152,"line":629},32,[150,631,433],{"class":215},[150,633,634],{"class":160},"\"--dry-run\"",[150,636,17],{"class":215},[150,638,639],{"class":386},"action",[150,641,377],{"class":211},[150,643,644],{"class":160},"\"store_true\"",[150,646,395],{"class":215},[150,648,650,652,654,657],{"class":152,"line":649},33,[150,651,452],{"class":386},[150,653,377],{"class":211},[150,655,656],{"class":160},"\"do everything except write the output file\"",[150,658,347],{"class":215},[150,660,662,664,667,669,672,674,676,678,681,683,685,687,689],{"class":152,"line":661},34,[150,663,433],{"class":215},[150,665,666],{"class":160},"\"-v\"",[150,668,17],{"class":215},[150,670,671],{"class":160},"\"--verbose\"",[150,673,17],{"class":215},[150,675,639],{"class":386},[150,677,377],{"class":211},[150,679,680],{"class":160},"\"count\"",[150,682,17],{"class":215},[150,684,484],{"class":386},[150,686,377],{"class":211},[150,688,57],{"class":307},[150,690,395],{"class":215},[150,692,694,696,698,701],{"class":152,"line":693},35,[150,695,452],{"class":386},[150,697,377],{"class":211},[150,699,700],{"class":160},"\"repeat for more detail: -v, -vv\"",[150,702,347],{"class":215},[150,704,706,709],{"class":152,"line":705},36,[150,707,708],{"class":211},"    return",[150,710,711],{"class":215}," parser\n",[150,713,715],{"class":152,"line":714},37,[150,716,250],{"emptyLinePlaceholder":249},[150,718,720],{"class":152,"line":719},38,[150,721,250],{"emptyLinePlaceholder":249},[150,723,725,727,730,733,735,737],{"class":152,"line":724},39,[150,726,261],{"class":211},[150,728,729],{"class":264}," parse_args",[150,731,732],{"class":215},"(argv",[150,734,377],{"class":211},[150,736,572],{"class":307},[150,738,739],{"class":215},"):\n",[150,741,743,745],{"class":152,"line":742},40,[150,744,708],{"class":211},[150,746,747],{"class":215}," build_parser().parse_args(argv)\n",[10,749,750,751,753,754,757,758,761,762,765,766,769,770,773,774,776,777,780],{},"Three things here earn their keep. ",[14,752,508],{}," in the help text means the defaults stay accurate when you change them. ",[14,755,756],{},"ArgumentTypeError"," from ",[14,759,760],{},"month_arg"," produces argparse's own error format — a usage line and a clear message — instead of a traceback. And ",[14,763,764],{},"parse_args(argv=None)"," is what makes the parser testable: argparse falls back to ",[14,767,768],{},"sys.argv"," only when ",[14,771,772],{},"argv"," is ",[14,775,572],{},", so a test can call ",[14,778,779],{},"parse_args([\"orders.xlsx\", \"--month\", \"2026-07\"])"," directly.",[136,782,784],{"id":783},"step-2-turn-the-arguments-into-a-run","Step 2: Turn the arguments into a run",[10,786,787,788,791],{},"Keep ",[14,789,790],{},"main"," short. Its job is to validate what argparse cannot, wire logging, call the real function, and return an exit code:",[141,793,795],{"className":195,"code":794,"language":197,"meta":146,"style":146},"import logging\nimport sys\n\n\ndef main(argv=None):\n    args = parse_args(argv)\n\n    level = [logging.WARNING, logging.INFO, logging.DEBUG][min(args.verbose, 2)]\n    logging.basicConfig(level=level, format=\"%(levelname)s %(message)s\")\n    log = logging.getLogger(\"report\")\n\n    if not args.source.is_file():\n        log.error(\"input workbook not found: %s\", args.source)\n        return 2\n\n    try:\n        summary = build_summary(args.source, sheet=args.sheet,\n                                month=args.month, min_amount=args.min_amount)\n    except ValueError as exc:                      # a data problem, not a crash\n        log.error(\"cannot build the report: %s\", exc)\n        return 1\n\n    if args.dry_run:\n        log.warning(\"dry run — %d row(s) would be written to %s\",\n                    len(summary), args.output)\n        return 0\n\n    write_workbook(summary, args.output)\n    log.info(\"wrote %s (%d rows)\", args.output, len(summary))\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",[14,796,797,804,811,815,819,834,844,848,887,917,932,936,947,963,970,974,980,998,1016,1031,1045,1052,1056,1063,1083,1091,1098,1102,1107,1134,1140,1144,1148,1164],{"__ignoreMap":146},[150,798,799,801],{"class":152,"line":153},[150,800,212],{"class":211},[150,802,803],{"class":215}," logging\n",[150,805,806,808],{"class":152,"line":208},[150,807,212],{"class":211},[150,809,810],{"class":215}," sys\n",[150,812,813],{"class":152,"line":219},[150,814,250],{"emptyLinePlaceholder":249},[150,816,817],{"class":152,"line":233},[150,818,250],{"emptyLinePlaceholder":249},[150,820,821,823,826,828,830,832],{"class":152,"line":246},[150,822,261],{"class":211},[150,824,825],{"class":264}," main",[150,827,732],{"class":215},[150,829,377],{"class":211},[150,831,572],{"class":307},[150,833,739],{"class":215},[150,835,836,839,841],{"class":152,"line":253},[150,837,838],{"class":215},"    args ",[150,840,377],{"class":211},[150,842,843],{"class":215}," parse_args(argv)\n",[150,845,846],{"class":152,"line":258},[150,847,250],{"emptyLinePlaceholder":249},[150,849,850,853,855,858,861,864,867,869,872,875,878,881,884],{"class":152,"line":271},[150,851,852],{"class":215},"    level ",[150,854,377],{"class":211},[150,856,857],{"class":215}," [logging.",[150,859,860],{"class":307},"WARNING",[150,862,863],{"class":215},", logging.",[150,865,866],{"class":307},"INFO",[150,868,863],{"class":215},[150,870,871],{"class":307},"DEBUG",[150,873,874],{"class":215},"][",[150,876,877],{"class":307},"min",[150,879,880],{"class":215},"(args.verbose, ",[150,882,883],{"class":307},"2",[150,885,886],{"class":215},")]\n",[150,888,889,892,895,897,900,903,905,907,910,913,915],{"class":152,"line":277},[150,890,891],{"class":215},"    logging.basicConfig(",[150,893,894],{"class":386},"level",[150,896,377],{"class":211},[150,898,899],{"class":215},"level, ",[150,901,902],{"class":386},"format",[150,904,377],{"class":211},[150,906,344],{"class":160},[150,908,909],{"class":331},"%(levelname)s",[150,911,912],{"class":331}," %(message)s",[150,914,344],{"class":160},[150,916,347],{"class":215},[150,918,919,922,924,927,930],{"class":152,"line":286},[150,920,921],{"class":215},"    log ",[150,923,377],{"class":211},[150,925,926],{"class":215}," logging.getLogger(",[150,928,929],{"class":160},"\"report\"",[150,931,347],{"class":215},[150,933,934],{"class":152,"line":301},[150,935,250],{"emptyLinePlaceholder":249},[150,937,938,941,944],{"class":152,"line":313},[150,939,940],{"class":211},"    if",[150,942,943],{"class":211}," not",[150,945,946],{"class":215}," args.source.is_file():\n",[150,948,949,952,955,958,960],{"class":152,"line":322},[150,950,951],{"class":215},"        log.error(",[150,953,954],{"class":160},"\"input workbook not found: ",[150,956,957],{"class":331},"%s",[150,959,344],{"class":160},[150,961,962],{"class":215},", args.source)\n",[150,964,965,967],{"class":152,"line":350},[150,966,289],{"class":211},[150,968,969],{"class":307}," 2\n",[150,971,972],{"class":152,"line":355},[150,973,250],{"emptyLinePlaceholder":249},[150,975,976,978],{"class":152,"line":360},[150,977,280],{"class":211},[150,979,283],{"class":215},[150,981,982,985,987,990,993,995],{"class":152,"line":371},[150,983,984],{"class":215},"        summary ",[150,986,377],{"class":211},[150,988,989],{"class":215}," build_summary(args.source, ",[150,991,992],{"class":386},"sheet",[150,994,377],{"class":211},[150,996,997],{"class":215},"args.sheet,\n",[150,999,1000,1003,1005,1008,1011,1013],{"class":152,"line":383},[150,1001,1002],{"class":386},"                                month",[150,1004,377],{"class":211},[150,1006,1007],{"class":215},"args.month, ",[150,1009,1010],{"class":386},"min_amount",[150,1012,377],{"class":211},[150,1014,1015],{"class":215},"args.min_amount)\n",[150,1017,1018,1020,1022,1025,1028],{"class":152,"line":398},[150,1019,304],{"class":211},[150,1021,308],{"class":307},[150,1023,1024],{"class":211}," as",[150,1026,1027],{"class":215}," exc:                      ",[150,1029,1030],{"class":204},"# a data problem, not a crash\n",[150,1032,1033,1035,1038,1040,1042],{"class":152,"line":411},[150,1034,951],{"class":215},[150,1036,1037],{"class":160},"\"cannot build the report: ",[150,1039,957],{"class":331},[150,1041,344],{"class":160},[150,1043,1044],{"class":215},", exc)\n",[150,1046,1047,1049],{"class":152,"line":424},[150,1048,289],{"class":211},[150,1050,1051],{"class":307}," 1\n",[150,1053,1054],{"class":152,"line":430},[150,1055,250],{"emptyLinePlaceholder":249},[150,1057,1058,1060],{"class":152,"line":449},[150,1059,940],{"class":211},[150,1061,1062],{"class":215}," args.dry_run:\n",[150,1064,1065,1068,1071,1074,1077,1079,1081],{"class":152,"line":462},[150,1066,1067],{"class":215},"        log.warning(",[150,1069,1070],{"class":160},"\"dry run — ",[150,1072,1073],{"class":331},"%d",[150,1075,1076],{"class":160}," row(s) would be written to ",[150,1078,957],{"class":331},[150,1080,344],{"class":160},[150,1082,395],{"class":215},[150,1084,1085,1088],{"class":152,"line":498},[150,1086,1087],{"class":307},"                    len",[150,1089,1090],{"class":215},"(summary), args.output)\n",[150,1092,1093,1095],{"class":152,"line":516},[150,1094,289],{"class":211},[150,1096,1097],{"class":307}," 0\n",[150,1099,1100],{"class":152,"line":535},[150,1101,250],{"emptyLinePlaceholder":249},[150,1103,1104],{"class":152,"line":551},[150,1105,1106],{"class":215},"    write_workbook(summary, args.output)\n",[150,1108,1109,1112,1115,1117,1120,1122,1125,1128,1131],{"class":152,"line":577},[150,1110,1111],{"class":215},"    log.info(",[150,1113,1114],{"class":160},"\"wrote ",[150,1116,957],{"class":331},[150,1118,1119],{"class":160}," (",[150,1121,1073],{"class":331},[150,1123,1124],{"class":160}," rows)\"",[150,1126,1127],{"class":215},", args.output, ",[150,1129,1130],{"class":307},"len",[150,1132,1133],{"class":215},"(summary))\n",[150,1135,1136,1138],{"class":152,"line":589},[150,1137,708],{"class":211},[150,1139,1097],{"class":307},[150,1141,1142],{"class":152,"line":617},[150,1143,250],{"emptyLinePlaceholder":249},[150,1145,1146],{"class":152,"line":629},[150,1147,250],{"emptyLinePlaceholder":249},[150,1149,1150,1153,1156,1159,1162],{"class":152,"line":649},[150,1151,1152],{"class":211},"if",[150,1154,1155],{"class":307}," __name__",[150,1157,1158],{"class":211}," ==",[150,1160,1161],{"class":160}," \"__main__\"",[150,1163,283],{"class":215},[150,1165,1166],{"class":152,"line":661},[150,1167,1168],{"class":215},"    sys.exit(main())\n",[10,1170,1171,1174,1175,1179,1180,34],{},[14,1172,1173],{},"sys.exit(main())"," is the line people leave out, and it is the one the scheduler cares about. Without it every run exits zero, including the ones that logged an error, and no monitoring can tell the difference. Returning distinct codes — 1 for a data problem, 2 for a missing input — lets an alert say ",[1176,1177,1178],"em",{},"which"," kind of failure happened before anyone opens the log. This is the same split described in ",[30,1181,1183],{"href":1182},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",[36,1185,45,1190,45,1193,45,1196,45,1199,45,1204,45,1209,45,1214,45,1218,45,1222,45,1226,45,1230,45,1235,45,1239,45,1243,45,1246,45,1249,45,1252,45,1257,45,1261,45,1265,45,1268,45,1271],{"viewBox":1186,"role":39,"ariaLabelledBy":1187,"xmlns":43,"style":44},"0 0 740 236",[1188,1189],"cli-exit-t","cli-exit-d",[47,1191,1192],{"id":1188},"What each exit code tells the scheduler to do",[51,1194,1195],{"id":1189},"Exit code zero means the report was written and no action is needed. Code one means the input data was wrong, so someone must fix the file — retrying will fail identically. Code two means the environment failed, such as a missing input or an unreachable share, which is worth a retry before alerting.",[55,1197],{"x":57,"y":57,"width":58,"height":1198,"fill":60},"236",[72,1200,1203],{"x":1201,"y":83,"style":1202},"370","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","sys.exit(main()) — the number the scheduler reads",[55,1205],{"x":63,"y":1206,"width":1207,"height":1208,"rx":67,"fill":110,"stroke":111,"style":70},"52","228","150",[72,1210,57],{"x":1211,"y":1212,"style":1213},"130","84","font-size:20px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[72,1215,1217],{"x":1211,"y":1216,"style":115},"112","report written",[72,1219,1221],{"x":1211,"y":1220,"style":89},"140","nothing to do",[72,1223,1225],{"x":1211,"y":1224,"style":105},"164","a dry run also exits 0 —",[72,1227,1229],{"x":1211,"y":1228,"style":105},"182","it did what it was asked",[55,1231],{"x":1232,"y":1206,"width":1207,"height":1208,"rx":67,"fill":1233,"stroke":1234,"style":70},"256","#fdefd8","var(--gold,#b4740a)",[72,1236,1238],{"x":1201,"y":1212,"style":1237},"font-size:20px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","1",[72,1240,1242],{"x":1201,"y":1216,"style":1241},"font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","the data is wrong",[72,1244,1245],{"x":1201,"y":1220,"style":89},"alert a person",[72,1247,1248],{"x":1201,"y":1224,"style":105},"a retry produces the",[72,1250,1251],{"x":1201,"y":1228,"style":105},"identical failure",[55,1253],{"x":1254,"y":1206,"width":1207,"height":1208,"rx":67,"fill":1255,"stroke":1256,"style":70},"496","#ebebfd","var(--brand,#5b5cf0)",[72,1258,883],{"x":1259,"y":1212,"style":1260},"610","font-size:20px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[72,1262,1264],{"x":1259,"y":1216,"style":1263},"font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","the environment failed",[72,1266,1267],{"x":1259,"y":1220,"style":89},"retry, then alert",[72,1269,1270],{"x":1259,"y":1224,"style":105},"missing input, locked",[72,1272,1273],{"x":1259,"y":1228,"style":105},"file, unreachable share",[136,1275,1277],{"id":1276},"step-3-add-subcommands-when-one-verb-is-not-enough","Step 3: Add subcommands when one verb is not enough",[10,1279,1280,1281,1283],{},"A report tool usually grows a second job — validate the input without producing anything, or re-send yesterday's file. Subparsers keep those in one executable with one ",[14,1282,27],{},":",[141,1285,1287],{"className":195,"code":1286,"language":197,"meta":146,"style":146},"def build_parser():\n    parser = argparse.ArgumentParser(prog=\"report\")\n    parser.add_argument(\"-v\", \"--verbose\", action=\"count\", default=0)\n    sub = parser.add_subparsers(dest=\"command\", required=True)\n\n    build = sub.add_parser(\"build\", help=\"build the report workbook\")\n    build.add_argument(\"source\", type=Path)\n    build.add_argument(\"-o\", \"--output\", type=Path, default=Path(\"report.xlsx\"))\n    build.set_defaults(func=cmd_build)\n\n    check = sub.add_parser(\"check\", help=\"validate the input and stop\")\n    check.add_argument(\"source\", type=Path)\n    check.set_defaults(func=cmd_check)\n\n    send = sub.add_parser(\"send\", help=\"email an existing report\")\n    send.add_argument(\"workbook\", type=Path)\n    send.add_argument(\"--to\", action=\"append\", required=True,\n                      help=\"recipient; repeat for several\")\n    send.set_defaults(func=cmd_send)\n    return parser\n\n\ndef main(argv=None):\n    args = parse_args(argv)\n    return args.func(args)          # set_defaults(func=...) does the dispatch\n",[14,1288,1289,1297,1315,1343,1373,1377,1402,1418,1447,1460,1464,1487,1502,1514,1518,1541,1557,1583,1595,1607,1613,1617,1621,1635,1643],{"__ignoreMap":146},[150,1290,1291,1293,1295],{"class":152,"line":153},[150,1292,261],{"class":211},[150,1294,365],{"class":264},[150,1296,368],{"class":215},[150,1298,1299,1301,1303,1306,1309,1311,1313],{"class":152,"line":208},[150,1300,374],{"class":215},[150,1302,377],{"class":211},[150,1304,1305],{"class":215}," argparse.ArgumentParser(",[150,1307,1308],{"class":386},"prog",[150,1310,377],{"class":211},[150,1312,929],{"class":160},[150,1314,347],{"class":215},[150,1316,1317,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337,1339,1341],{"class":152,"line":219},[150,1318,433],{"class":215},[150,1320,666],{"class":160},[150,1322,17],{"class":215},[150,1324,671],{"class":160},[150,1326,17],{"class":215},[150,1328,639],{"class":386},[150,1330,377],{"class":211},[150,1332,680],{"class":160},[150,1334,17],{"class":215},[150,1336,484],{"class":386},[150,1338,377],{"class":211},[150,1340,57],{"class":307},[150,1342,347],{"class":215},[150,1344,1345,1348,1350,1353,1356,1358,1361,1363,1366,1368,1371],{"class":152,"line":233},[150,1346,1347],{"class":215},"    sub ",[150,1349,377],{"class":211},[150,1351,1352],{"class":215}," parser.add_subparsers(",[150,1354,1355],{"class":386},"dest",[150,1357,377],{"class":211},[150,1359,1360],{"class":160},"\"command\"",[150,1362,17],{"class":215},[150,1364,1365],{"class":386},"required",[150,1367,377],{"class":211},[150,1369,1370],{"class":307},"True",[150,1372,347],{"class":215},[150,1374,1375],{"class":152,"line":246},[150,1376,250],{"emptyLinePlaceholder":249},[150,1378,1379,1382,1384,1387,1390,1392,1395,1397,1400],{"class":152,"line":253},[150,1380,1381],{"class":215},"    build ",[150,1383,377],{"class":211},[150,1385,1386],{"class":215}," sub.add_parser(",[150,1388,1389],{"class":160},"\"build\"",[150,1391,17],{"class":215},[150,1393,1394],{"class":386},"help",[150,1396,377],{"class":211},[150,1398,1399],{"class":160},"\"build the report workbook\"",[150,1401,347],{"class":215},[150,1403,1404,1407,1409,1411,1413,1415],{"class":152,"line":258},[150,1405,1406],{"class":215},"    build.add_argument(",[150,1408,436],{"class":160},[150,1410,17],{"class":215},[150,1412,441],{"class":386},[150,1414,377],{"class":211},[150,1416,1417],{"class":215},"Path)\n",[150,1419,1420,1422,1424,1426,1428,1430,1432,1434,1436,1438,1440,1442,1444],{"class":152,"line":271},[150,1421,1406],{"class":215},[150,1423,467],{"class":160},[150,1425,17],{"class":215},[150,1427,472],{"class":160},[150,1429,17],{"class":215},[150,1431,441],{"class":386},[150,1433,377],{"class":211},[150,1435,481],{"class":215},[150,1437,484],{"class":386},[150,1439,377],{"class":211},[150,1441,489],{"class":215},[150,1443,492],{"class":160},[150,1445,1446],{"class":215},"))\n",[150,1448,1449,1452,1455,1457],{"class":152,"line":277},[150,1450,1451],{"class":215},"    build.set_defaults(",[150,1453,1454],{"class":386},"func",[150,1456,377],{"class":211},[150,1458,1459],{"class":215},"cmd_build)\n",[150,1461,1462],{"class":152,"line":286},[150,1463,250],{"emptyLinePlaceholder":249},[150,1465,1466,1469,1471,1473,1476,1478,1480,1482,1485],{"class":152,"line":301},[150,1467,1468],{"class":215},"    check ",[150,1470,377],{"class":211},[150,1472,1386],{"class":215},[150,1474,1475],{"class":160},"\"check\"",[150,1477,17],{"class":215},[150,1479,1394],{"class":386},[150,1481,377],{"class":211},[150,1483,1484],{"class":160},"\"validate the input and stop\"",[150,1486,347],{"class":215},[150,1488,1489,1492,1494,1496,1498,1500],{"class":152,"line":313},[150,1490,1491],{"class":215},"    check.add_argument(",[150,1493,436],{"class":160},[150,1495,17],{"class":215},[150,1497,441],{"class":386},[150,1499,377],{"class":211},[150,1501,1417],{"class":215},[150,1503,1504,1507,1509,1511],{"class":152,"line":322},[150,1505,1506],{"class":215},"    check.set_defaults(",[150,1508,1454],{"class":386},[150,1510,377],{"class":211},[150,1512,1513],{"class":215},"cmd_check)\n",[150,1515,1516],{"class":152,"line":350},[150,1517,250],{"emptyLinePlaceholder":249},[150,1519,1520,1523,1525,1527,1530,1532,1534,1536,1539],{"class":152,"line":355},[150,1521,1522],{"class":215},"    send ",[150,1524,377],{"class":211},[150,1526,1386],{"class":215},[150,1528,1529],{"class":160},"\"send\"",[150,1531,17],{"class":215},[150,1533,1394],{"class":386},[150,1535,377],{"class":211},[150,1537,1538],{"class":160},"\"email an existing report\"",[150,1540,347],{"class":215},[150,1542,1543,1546,1549,1551,1553,1555],{"class":152,"line":360},[150,1544,1545],{"class":215},"    send.add_argument(",[150,1547,1548],{"class":160},"\"workbook\"",[150,1550,17],{"class":215},[150,1552,441],{"class":386},[150,1554,377],{"class":211},[150,1556,1417],{"class":215},[150,1558,1559,1561,1564,1566,1568,1570,1573,1575,1577,1579,1581],{"class":152,"line":371},[150,1560,1545],{"class":215},[150,1562,1563],{"class":160},"\"--to\"",[150,1565,17],{"class":215},[150,1567,639],{"class":386},[150,1569,377],{"class":211},[150,1571,1572],{"class":160},"\"append\"",[150,1574,17],{"class":215},[150,1576,1365],{"class":386},[150,1578,377],{"class":211},[150,1580,1370],{"class":307},[150,1582,395],{"class":215},[150,1584,1585,1588,1590,1593],{"class":152,"line":383},[150,1586,1587],{"class":386},"                      help",[150,1589,377],{"class":211},[150,1591,1592],{"class":160},"\"recipient; repeat for several\"",[150,1594,347],{"class":215},[150,1596,1597,1600,1602,1604],{"class":152,"line":398},[150,1598,1599],{"class":215},"    send.set_defaults(",[150,1601,1454],{"class":386},[150,1603,377],{"class":211},[150,1605,1606],{"class":215},"cmd_send)\n",[150,1608,1609,1611],{"class":152,"line":411},[150,1610,708],{"class":211},[150,1612,711],{"class":215},[150,1614,1615],{"class":152,"line":424},[150,1616,250],{"emptyLinePlaceholder":249},[150,1618,1619],{"class":152,"line":430},[150,1620,250],{"emptyLinePlaceholder":249},[150,1622,1623,1625,1627,1629,1631,1633],{"class":152,"line":449},[150,1624,261],{"class":211},[150,1626,825],{"class":264},[150,1628,732],{"class":215},[150,1630,377],{"class":211},[150,1632,572],{"class":307},[150,1634,739],{"class":215},[150,1636,1637,1639,1641],{"class":152,"line":462},[150,1638,838],{"class":215},[150,1640,377],{"class":211},[150,1642,843],{"class":215},[150,1644,1645,1647,1650],{"class":152,"line":498},[150,1646,708],{"class":211},[150,1648,1649],{"class":215}," args.func(args)          ",[150,1651,1652],{"class":204},"# set_defaults(func=...) does the dispatch\n",[10,1654,1655,1658,1659,1662,1663,1666,1667,1670,1671,1674,1675,1678,1679,1682,1683,1686,1687,1691],{},[14,1656,1657],{},"set_defaults(func=...)"," avoids a chain of ",[14,1660,1661],{},"if args.command == ..."," comparisons, and ",[14,1664,1665],{},"required=True"," on the subparsers means a bare ",[14,1668,1669],{},"report"," prints usage instead of failing later with an unhelpful ",[14,1672,1673],{},"AttributeError",". ",[14,1676,1677],{},"action=\"append\""," on ",[14,1680,1681],{},"--to"," is the idiomatic way to accept a repeated flag: ",[14,1684,1685],{},"--to a@x.com --to b@x.com"," arrives as a list, which is exactly what ",[30,1688,1690],{"href":1689},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002F","the emailing step"," wants.",[136,1693,1695],{"id":1694},"step-3b-keep-help-worth-reading","Step 3b: Keep --help worth reading",[10,1697,1698],{},"The help text is the only documentation most people will ever see, and argparse assembles it from what you give it. Three habits make the difference between a usage message that answers the question and one that repeats the flag names:",[36,1700,45,1705,45,1708,45,1711,45,1714,45,1720,45,1725,45,1729,45,1733,45,1736,45,1740,45,1743,45,1748,45,1754,45,1756,45,1760,45,1763,45,1768,45,1772,45,1775,45,1780],{"viewBox":1701,"role":39,"ariaLabelledBy":1702,"xmlns":43,"style":44},"0 0 740 224",[1703,1704],"cli-help-t","cli-help-d",[47,1706,1707],{"id":1703},"Which parser argument produces which part of --help",[51,1709,1710],{"id":1704},"The prog name and the argument list produce the usage line. The description appears above the options. Each help string, with a default interpolated by percent-parens-default, produces one option line. The epilog carries a worked example at the bottom, which is the part people copy.",[55,1712],{"x":57,"y":57,"width":58,"height":1713,"fill":60},"224",[55,1715],{"x":1716,"y":64,"width":1717,"height":1224,"rx":1718,"fill":1719,"stroke":85,"style":70},"24","400","10","#f0f2f5",[72,1721,1724],{"x":80,"y":1722,"style":1723},"58","font-size:11px;fill:var(--text,#172033)","usage: monthly-report [-h] [-o OUTPUT] source",[72,1726,1728],{"x":80,"y":1727,"style":1723},"82","Build the monthly regional sales report.",[72,1730,1732],{"x":80,"y":1731,"style":1723},"110","  source        input workbook (.xlsx)",[72,1734,1735],{"x":80,"y":93,"style":1723},"  -o OUTPUT     where to write (default: report.xlsx)",[72,1737,1739],{"x":80,"y":1738,"style":1723},"154","  --month MONTH restrict to one month, e.g. 2026-07",[72,1741,1742],{"x":80,"y":1228,"style":1723},"Example: monthly-report orders.xlsx --month 2026-07",[152,1744],{"x1":1745,"y1":1206,"x2":1746,"y2":1206,"stroke":1256,"style":1747},"428","470","stroke-width:1.5px",[72,1749,1753],{"x":1750,"y":1751,"style":1752},"480","56","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca)","prog= and the arguments",[152,1755],{"x1":1745,"y1":81,"x2":1746,"y2":81,"stroke":1234,"style":1747},[72,1757,1759],{"x":1750,"y":1727,"style":1758},"font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06)","description=",[152,1761],{"x1":1745,"y1":1762,"x2":1746,"y2":1762,"stroke":111,"style":1747},"126",[72,1764,1767],{"x":1750,"y":1765,"style":1766},"120","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157)","help= on each argument",[72,1769,1771],{"x":1750,"y":1220,"style":1770},"font-size:10.5px;fill:var(--muted,#5b6780)","with %(default)s so it stays true",[152,1773],{"x1":1745,"y1":1774,"x2":1746,"y2":1774,"stroke":69,"style":1747},"178",[72,1776,1779],{"x":1750,"y":1777,"style":1778},"174","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d)","epilog=",[72,1781,1783],{"x":1750,"y":1782,"style":1770},"194","the line people actually copy",[10,1785,1786,1787,1790,1791,1794,1795,1798,1799,1801,1802,1804],{},"Name the tool with ",[14,1788,1789],{},"prog="," rather than letting argparse use ",[14,1792,1793],{},"sys.argv[0]",", which shows as ",[14,1796,1797],{},"cli.py"," when run from source and as the executable name after packaging — two different names for the same tool in the same team's notes. Interpolate defaults with ",[14,1800,508],{}," so the help cannot drift from the code. And put a complete, runnable example in ",[14,1803,1779],{},": it is the one part of a help message people read to the end, because it is the part they can paste.",[136,1806,1808],{"id":1807},"step-4-test-the-parser-and-the-run-separately","Step 4: Test the parser and the run separately",[10,1810,1811,1812,1815],{},"Because ",[14,1813,1814],{},"parse_args"," takes a list, the parser tests need no files and no subprocess:",[141,1817,1819],{"className":195,"code":1818,"language":197,"meta":146,"style":146},"import pytest\n\nfrom cli import main, parse_args\n\n\ndef test_defaults():\n    args = parse_args([\"orders.xlsx\"])\n    assert args.output.name == \"report.xlsx\"\n    assert args.sheet == \"Orders\"\n    assert args.dry_run is False\n\n\ndef test_month_is_parsed_to_a_date():\n    assert parse_args([\"in.xlsx\", \"--month\", \"2026-07\"]).month.month == 7\n\n\ndef test_bad_month_exits_with_usage():\n    with pytest.raises(SystemExit) as exc:\n        parse_args([\"in.xlsx\", \"--month\", \"July\"])\n    assert exc.value.code == 2                # argparse's own usage-error code\n\n\ndef test_missing_input_returns_2(tmp_path):\n    assert main([str(tmp_path \u002F \"nope.xlsx\")]) == 2\n",[14,1820,1821,1828,1832,1844,1848,1852,1861,1876,1890,1902,1915,1919,1923,1932,1958,1962,1966,1975,1995,2013,2028,2032,2036,2046],{"__ignoreMap":146},[150,1822,1823,1825],{"class":152,"line":153},[150,1824,212],{"class":211},[150,1826,1827],{"class":215}," pytest\n",[150,1829,1830],{"class":152,"line":208},[150,1831,250],{"emptyLinePlaceholder":249},[150,1833,1834,1836,1839,1841],{"class":152,"line":219},[150,1835,222],{"class":211},[150,1837,1838],{"class":215}," cli ",[150,1840,212],{"class":211},[150,1842,1843],{"class":215}," main, parse_args\n",[150,1845,1846],{"class":152,"line":233},[150,1847,250],{"emptyLinePlaceholder":249},[150,1849,1850],{"class":152,"line":246},[150,1851,250],{"emptyLinePlaceholder":249},[150,1853,1854,1856,1859],{"class":152,"line":253},[150,1855,261],{"class":211},[150,1857,1858],{"class":264}," test_defaults",[150,1860,368],{"class":215},[150,1862,1863,1865,1867,1870,1873],{"class":152,"line":258},[150,1864,838],{"class":215},[150,1866,377],{"class":211},[150,1868,1869],{"class":215}," parse_args([",[150,1871,1872],{"class":160},"\"orders.xlsx\"",[150,1874,1875],{"class":215},"])\n",[150,1877,1878,1881,1884,1887],{"class":152,"line":271},[150,1879,1880],{"class":211},"    assert",[150,1882,1883],{"class":215}," args.output.name ",[150,1885,1886],{"class":211},"==",[150,1888,1889],{"class":160}," \"report.xlsx\"\n",[150,1891,1892,1894,1897,1899],{"class":152,"line":277},[150,1893,1880],{"class":211},[150,1895,1896],{"class":215}," args.sheet ",[150,1898,1886],{"class":211},[150,1900,1901],{"class":160}," \"Orders\"\n",[150,1903,1904,1906,1909,1912],{"class":152,"line":286},[150,1905,1880],{"class":211},[150,1907,1908],{"class":215}," args.dry_run ",[150,1910,1911],{"class":211},"is",[150,1913,1914],{"class":307}," False\n",[150,1916,1917],{"class":152,"line":301},[150,1918,250],{"emptyLinePlaceholder":249},[150,1920,1921],{"class":152,"line":313},[150,1922,250],{"emptyLinePlaceholder":249},[150,1924,1925,1927,1930],{"class":152,"line":322},[150,1926,261],{"class":211},[150,1928,1929],{"class":264}," test_month_is_parsed_to_a_date",[150,1931,368],{"class":215},[150,1933,1934,1936,1938,1941,1943,1945,1947,1950,1953,1955],{"class":152,"line":350},[150,1935,1880],{"class":211},[150,1937,1869],{"class":215},[150,1939,1940],{"class":160},"\"in.xlsx\"",[150,1942,17],{"class":215},[150,1944,556],{"class":160},[150,1946,17],{"class":215},[150,1948,1949],{"class":160},"\"2026-07\"",[150,1951,1952],{"class":215},"]).month.month ",[150,1954,1886],{"class":211},[150,1956,1957],{"class":307}," 7\n",[150,1959,1960],{"class":152,"line":355},[150,1961,250],{"emptyLinePlaceholder":249},[150,1963,1964],{"class":152,"line":360},[150,1965,250],{"emptyLinePlaceholder":249},[150,1967,1968,1970,1973],{"class":152,"line":371},[150,1969,261],{"class":211},[150,1971,1972],{"class":264}," test_bad_month_exits_with_usage",[150,1974,368],{"class":215},[150,1976,1977,1980,1983,1986,1989,1992],{"class":152,"line":383},[150,1978,1979],{"class":211},"    with",[150,1981,1982],{"class":215}," pytest.raises(",[150,1984,1985],{"class":307},"SystemExit",[150,1987,1988],{"class":215},") ",[150,1990,1991],{"class":211},"as",[150,1993,1994],{"class":215}," exc:\n",[150,1996,1997,2000,2002,2004,2006,2008,2011],{"class":152,"line":398},[150,1998,1999],{"class":215},"        parse_args([",[150,2001,1940],{"class":160},[150,2003,17],{"class":215},[150,2005,556],{"class":160},[150,2007,17],{"class":215},[150,2009,2010],{"class":160},"\"July\"",[150,2012,1875],{"class":215},[150,2014,2015,2017,2020,2022,2025],{"class":152,"line":411},[150,2016,1880],{"class":211},[150,2018,2019],{"class":215}," exc.value.code ",[150,2021,1886],{"class":211},[150,2023,2024],{"class":307}," 2",[150,2026,2027],{"class":204},"                # argparse's own usage-error code\n",[150,2029,2030],{"class":152,"line":424},[150,2031,250],{"emptyLinePlaceholder":249},[150,2033,2034],{"class":152,"line":430},[150,2035,250],{"emptyLinePlaceholder":249},[150,2037,2038,2040,2043],{"class":152,"line":449},[150,2039,261],{"class":211},[150,2041,2042],{"class":264}," test_missing_input_returns_2",[150,2044,2045],{"class":215},"(tmp_path):\n",[150,2047,2048,2050,2053,2056,2059,2062,2065,2068,2070],{"class":152,"line":462},[150,2049,1880],{"class":211},[150,2051,2052],{"class":215}," main([",[150,2054,2055],{"class":307},"str",[150,2057,2058],{"class":215},"(tmp_path ",[150,2060,2061],{"class":211},"\u002F",[150,2063,2064],{"class":160}," \"nope.xlsx\"",[150,2066,2067],{"class":215},")]) ",[150,2069,1886],{"class":211},[150,2071,969],{"class":307},[10,2073,2074,2077,2078,2081,2082,2084,2085,2088,2089,34],{},[14,2075,2076],{},"main([...])"," returning an integer rather than calling ",[14,2079,2080],{},"sys.exit"," is what makes that last test one line. Keep the ",[14,2083,2080],{}," at the module's ",[14,2086,2087],{},"__main__"," guard and nowhere else. The rest of the suite is covered in ",[30,2090,2092],{"href":2091},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",[136,2094,2096],{"id":2095},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2098,2099,2100,2116],"table",{},[2101,2102,2103],"thead",{},[2104,2105,2106,2110,2113],"tr",{},[2107,2108,2109],"th",{},"Symptom",[2107,2111,2112],{},"Cause",[2107,2114,2115],{},"Fix",[2117,2118,2119,2140,2154,2168,2187,2207,2219],"tbody",{},[2104,2120,2121,2125,2133],{},[2122,2123,2124],"td",{},"Scheduler never reports a failure",[2122,2126,2127,2130,2131],{},[14,2128,2129],{},"main()"," called without ",[14,2132,2080],{},[2122,2134,2135,2137,2138],{},[14,2136,1173],{}," under ",[14,2139,2087],{},[2104,2141,2142,2145,2148],{},[2122,2143,2144],{},"Flag value ignored when a config file is present",[2122,2146,2147],{},"Merge overwrote the flag",[2122,2149,2150,2151,2153],{},"Skip ",[14,2152,572],{}," values when merging layers",[2104,2155,2156,2162,2165],{},[2122,2157,2158,2161],{},[14,2159,2160],{},"--dry-run"," still writes a file",[2122,2163,2164],{},"The write happens before the check",[2122,2166,2167],{},"Put the guard immediately before the save",[2104,2169,2170,2173,2176],{},[2122,2171,2172],{},"Windows path argument eats the quote",[2122,2174,2175],{},"A trailing backslash escapes the closing quote",[2122,2177,2178,2179,2182,2183,2186],{},"Pass ",[14,2180,2181],{},"\"C:\\Reports\\\""," as ",[14,2184,2185],{},"\"C:\\Reports\""," or use forward slashes",[2104,2188,2189,2195,2198],{},[2122,2190,2191,2194],{},[14,2192,2193],{},"--to a@x.com,b@x.com"," sends to one odd address",[2122,2196,2197],{},"Commas are not split by argparse",[2122,2199,2200,2201,2203,2204],{},"Use ",[14,2202,1677],{},", or ",[14,2205,2206],{},"nargs=\"+\"",[2104,2208,2209,2212,2215],{},[2122,2210,2211],{},"Help text shows a stale default",[2122,2213,2214],{},"Default hardcoded in the help string",[2122,2216,2200,2217],{},[14,2218,508],{},[2104,2220,2221,2224,2230],{},[2122,2222,2223],{},"Tests hang waiting on input",[2122,2225,2226,2227,2229],{},"The parser read the real ",[14,2228,768],{}," under pytest",[2122,2231,2232,2233,2235],{},"Always pass an explicit ",[14,2234,772],{}," list",[136,2237,2239],{"id":2238},"performance-and-scale-notes","Performance and scale notes",[10,2241,2242,2243,34],{},"argparse costs microseconds; it never shows up in a report's runtime. What it changes at scale is the number of scripts you maintain. One parameterised tool called from four schedule entries — one per region, say — replaces four near-identical copies that drift apart over a year. When the number of parameters passes about ten, move the stable ones into a config file and keep the command line for what varies per run; that split is covered in ",[30,2244,2246],{"href":2245},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File",[136,2248,2250],{"id":2249},"conclusion","Conclusion",[10,2252,2253,2254,2256,2257,2259,2260,2263,2264,2266],{},"Give the script arguments and it stops being yours alone: ",[14,2255,27],{}," documents it, the scheduler parameterises it, the tests drive it directly, and nobody edits a constant under time pressure. Type the arguments with ",[14,2258,191],{}," and a small date converter, keep ",[14,2261,2262],{},"main(argv=None)"," returning an exit code, split the verbs into subcommands when a second job appears, and let ",[14,2265,1173],{}," be the only place the process actually stops.",[136,2268,2270],{"id":2269},"frequently-asked-questions","Frequently asked questions",[10,2272,2273,2277],{},[2274,2275,2276],"strong",{},"Why argparse rather than click or typer?","\nargparse is in the standard library, so a scheduled job has one less pinned dependency and a packaged executable stays smaller. click and typer are nicer for large tools; for a report script with six flags the difference is not worth the install.",[10,2279,2280,2283,2284,2286,2287,2289,2290,769,2292,773,2294,2296],{},[2274,2281,2282],{},"How do I make the parser testable?","\nGive the function an ",[14,2285,772],{}," parameter — ",[14,2288,764],{}," — and call it with a list in tests. argparse reads ",[14,2291,768],{},[14,2293,772],{},[14,2295,572],{},", so tests never touch the real command line.",[10,2298,2299,2302],{},[2274,2300,2301],{},"What exit code should a failed report return?","\nAnything non-zero, and ideally distinct codes for distinct causes — 1 for bad input data, 2 for an infrastructure failure. cron and Task Scheduler both surface the code, so distinct values let an alert say what kind of failure it was.",[10,2304,2305,2308],{},[2274,2306,2307],{},"Should the output path be an argument or derived from the input?","\nGive it a flag with a sensible default. Deriving it entirely means two runs with different filters overwrite each other; requiring it every time makes the common case tedious.",[136,2310,2312],{"id":2311},"related","Related",[10,2314,2315],{},"Up to the parent guide:",[2317,2318,2319],"ul",{},[2320,2321,2322,2324],"li",{},[30,2323,33],{"href":32}," — where the command line fits among tests, config and packaging.",[10,2326,2327],{},"Related guides:",[2317,2329,2330,2335,2342,2349],{},[2320,2331,2332,2334],{},[30,2333,2246],{"href":2245}," — for the settings that should not be flags.",[2320,2336,2337,2341],{},[30,2338,2340],{"href":2339},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002F","Package a Python Excel Script as an EXE with PyInstaller"," — shipping the finished command to someone without Python.",[2320,2343,2344,2348],{},[30,2345,2347],{"href":2346},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-python-excel-script-on-windows-task-scheduler\u002F","Run a Python Excel Script on Windows Task Scheduler"," — where the exit codes are read.",[2320,2350,2351,2355,2356,2359],{},[30,2352,2354],{"href":2353},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — wiring ",[14,2357,2358],{},"-v"," into a log the scheduler keeps.",[2361,2362,2363],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":146,"searchDepth":208,"depth":208,"links":2365},[2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376],{"id":138,"depth":208,"text":139},{"id":184,"depth":208,"text":185},{"id":783,"depth":208,"text":784},{"id":1276,"depth":208,"text":1277},{"id":1694,"depth":208,"text":1695},{"id":1807,"depth":208,"text":1808},{"id":2095,"depth":208,"text":2096},{"id":2238,"depth":208,"text":2239},{"id":2249,"depth":208,"text":2250},{"id":2269,"depth":208,"text":2270},{"id":2311,"depth":208,"text":2312},"2026-08-10","Replace the constants at the top of your report script with real arguments: paths, month, dry-run and verbose flags, subcommands, and exit codes a scheduler can act on.","md",[2381,2383,2385,2387],{"q":2276,"a":2382},"argparse is in the standard library, so a scheduled job has one less pinned dependency and a packaged executable stays smaller. click and typer are nicer for large tools; for a report script with six flags the difference is not worth the install.",{"q":2282,"a":2384},"Give the function an argv parameter — parse_args(argv=None) — and call it with a list in tests. argparse reads sys.argv only when argv is None, so tests never touch the real command line.",{"q":2301,"a":2386},"Anything non-zero, and ideally distinct codes for distinct causes — 1 for bad input data, 2 for an infrastructure failure. cron and Task Scheduler both surface the code, so distinct values let an alert say what kind of failure it was.",{"q":2307,"a":2388},"Give it a flag with a sensible default. Deriving it entirely means two runs with different filters overwrite each other; requiring it every time makes the common case tedious.",{"breadcrumb":2390},[2391,2393,2396,2397],{"name":2392,"item":2061},"Home",{"name":2394,"item":2395},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":33,"item":32},{"name":5,"item":2398},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002F","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse",{"title":2401,"description":2402},"Excel Report CLI with argparse in Python","Turn a Python Excel report script into a command-line tool with argparse — typed path arguments, date parsing, dry-run and verbose flags, subcommands and real exit codes.","build-a-command-line-tool-for-excel-reports-with-argparse","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002Findex","how-to","ZXjIZQT63znIxAVA0CLkjAEpbV9Adu_fM_v82_n1CjE",[2408,2411],{"title":33,"path":2409,"stem":2410,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Findex",{"title":2246,"path":2412,"stem":2413,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002Findex",1786800028336]