[{"data":1,"prerenderedAt":2629},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project":2594,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project":2603,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project":2608,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project":2609},{"id":4,"title":5,"body":6,"dateModified":2561,"datePublished":2561,"description":2562,"extension":2563,"faq":2564,"keyword":2573,"kind":2574,"meta":2575,"navigation":274,"path":2584,"seo":2585,"slug":2588,"stem":2589,"topics":2590,"type":2592,"__hash__":2593},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project\u002Findex.md","Lint and Type-Check an Excel Automation Project",{"type":7,"value":8,"toc":2550},"minimark",[9,24,171,176,209,223,419,442,446,449,1003,1029,1032,1225,1246,1250,1253,1907,1946,1977,2099,2103,2118,2147,2154,2225,2229,2236,2373,2395,2399,2446,2450,2482,2486,2492,2501,2507,2513,2517,2546],[10,11,12,13,17,18,23],"p",{},"Excel automation scripts fail in predictable ways: a sheet name changes and ",[14,15,16],"code",{},"read_excel"," returns something else, a cell holds text where a number was expected, a default argument quietly accumulates values between runs. Most of these can be found without running anything. A linter catches the risky patterns, and a type checker — given type information for pandas and openpyxl — catches the calls that would crash. This guide, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", runs both on a small report module, shows what they report and how to fix each finding, and wires them into the editor and CI.",[25,26,34,35,34,39,34,43,34,51,34,61,34,72,34,79,34,84,34,90,34,95,34,98,34,104,34,108,34,111,34,115,34,118,34,123,34,130,34,136,34,140,34,144,34,148,34,151,34,156,34,163,34,168],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"4 0 752 142","img",[30,31],"tc-flow-t","tc-flow-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Where the checks run",[40,41,42],"desc",{"id":31},"Ruff runs in the editor as you type and fixes what it safely can. A pre-commit hook runs ruff and mypy before each commit. CI runs the same commands on every pull request, alongside the tests, so nothing unchecked reaches the main branch.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"4","0","752","142","#ffffff",[44,52],{"x":53,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"20.0","16.0","153.0","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[62,63,65],"g",{"transform":64},"translate(84.5,37.8) scale(1.000)",[66,67],"path",{"d":68,"fill":69,"stroke":70,"style":71},"M9 8l-4 4 4 4 M15 8l4 4-4 4","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[73,74,78],"text",{"x":75,"y":76,"style":77},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Editor",[73,80,83],{"x":75,"y":81,"style":82},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","ruff on save",[85,86],"line",{"x1":87,"y1":88,"x2":89,"y2":88,"stroke":59,"style":60},"178.0","71.2","200.0",[91,92],"polygon",{"points":93,"fill":94},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[44,96],{"x":97,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"209.0",[62,99,101],{"transform":100},"translate(273.5,37.8) scale(1.000)",[66,102],{"d":103,"fill":69,"stroke":70,"style":71},"M5 12.5l4.5 4.5L19 7",[73,105,107],{"x":106,"y":76,"style":77},"285.5","Pre-commit",[73,109,110],{"x":106,"y":81,"style":82},"ruff + mypy",[85,112],{"x1":113,"y1":88,"x2":114,"y2":88,"stroke":59,"style":60},"367.0","389.0",[91,116],{"points":117,"fill":94},"393.0,71.2 384.0,76.2 384.0,66.2",[44,119],{"x":120,"y":54,"width":55,"height":56,"rx":57,"fill":121,"stroke":122,"style":60},"398.0","#fdefd8","var(--gold,#b4740a)",[62,124,126],{"transform":125},"translate(462.5,29.0) scale(1.000)",[66,127],{"d":128,"fill":69,"stroke":129,"style":71},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--gold-ink,#7a4e06)",[73,131,135],{"x":132,"y":133,"style":134},"474.5","75.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","CI",[73,137,139],{"x":132,"y":138,"style":82},"95.0","same commands, plus",[73,141,143],{"x":132,"y":142,"style":82},"112.5","pytest",[85,145],{"x1":146,"y1":88,"x2":147,"y2":88,"stroke":59,"style":60},"556.0","578.0",[91,149],{"points":150,"fill":94},"582.0,71.2 573.0,76.2 573.0,66.2",[44,152],{"x":153,"y":54,"width":55,"height":56,"rx":57,"fill":154,"stroke":155,"style":60},"587.0","#d9f4f1","var(--teal,#0f9488)",[62,157,159],{"transform":158},"translate(651.5,37.8) scale(1.000)",[66,160],{"d":161,"fill":69,"stroke":162,"style":71},"M6 11h12v9H6z M8.5 11V8a3.5 3.5 0 0 1 7 0v3","var(--teal-ink,#0b6157)",[73,164,167],{"x":165,"y":76,"style":166},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Main branch",[73,169,170],{"x":165,"y":81,"style":82},"only checked code",[172,173,175],"h2",{"id":174},"prerequisites","Prerequisites",[177,178,183],"pre",{"className":179,"code":180,"language":181,"meta":182,"style":182},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install ruff mypy pandas-stubs types-openpyxl\n","bash","",[14,184,185],{"__ignoreMap":182},[186,187,189,193,197,200,203,206],"span",{"class":85,"line":188},1,[186,190,192],{"class":191},"sMTad","pip",[186,194,196],{"class":195},"srMev"," install",[186,198,199],{"class":195}," ruff",[186,201,202],{"class":195}," mypy",[186,204,205],{"class":195}," pandas-stubs",[186,207,208],{"class":195}," types-openpyxl\n",[10,210,211,214,215,218,219,222],{},[14,212,213],{},"pandas-stubs"," and ",[14,216,217],{},"types-openpyxl"," contain only type information. Without them mypy treats pandas and openpyxl as untyped and checks almost nothing about how you call them. Configure both tools in ",[14,220,221],{},"pyproject.toml",":",[177,224,228],{"className":225,"code":226,"language":227,"meta":182,"style":182},"language-toml shiki shiki-themes github-light github-dark-high-contrast","[tool.ruff]\nline-length = 100\nsrc = [\"src\"]\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"W\", \"I\", \"B\", \"UP\", \"DTZ\", \"PD\", \"PTH\", \"SIM\", \"RUF\"]\n\n[tool.mypy]\npython_version = \"3.12\"\nfiles = [\"src\"]\nwarn_unused_ignores = true\ndisallow_untyped_defs = true\ncheck_untyped_defs = true\n","toml",[14,229,230,248,258,269,276,294,356,361,375,384,394,403,411],{"__ignoreMap":182},[186,231,232,236,239,242,245],{"class":85,"line":188},[186,233,235],{"class":234},"skGVy","[",[186,237,238],{"class":191},"tool",[186,240,241],{"class":234},".",[186,243,244],{"class":191},"ruff",[186,246,247],{"class":234},"]\n",[186,249,251,254],{"class":85,"line":250},2,[186,252,253],{"class":234},"line-length = ",[186,255,257],{"class":256},"sP0c6","100\n",[186,259,261,264,267],{"class":85,"line":260},3,[186,262,263],{"class":234},"src = [",[186,265,266],{"class":195},"\"src\"",[186,268,247],{"class":234},[186,270,272],{"class":85,"line":271},4,[186,273,275],{"emptyLinePlaceholder":274},true,"\n",[186,277,279,281,283,285,287,289,292],{"class":85,"line":278},5,[186,280,235],{"class":234},[186,282,238],{"class":191},[186,284,241],{"class":234},[186,286,244],{"class":191},[186,288,241],{"class":234},[186,290,291],{"class":191},"lint",[186,293,247],{"class":234},[186,295,297,300,303,306,309,311,314,316,319,321,324,326,329,331,334,336,339,341,344,346,349,351,354],{"class":85,"line":296},6,[186,298,299],{"class":234},"select = [",[186,301,302],{"class":195},"\"E\"",[186,304,305],{"class":234},", ",[186,307,308],{"class":195},"\"F\"",[186,310,305],{"class":234},[186,312,313],{"class":195},"\"W\"",[186,315,305],{"class":234},[186,317,318],{"class":195},"\"I\"",[186,320,305],{"class":234},[186,322,323],{"class":195},"\"B\"",[186,325,305],{"class":234},[186,327,328],{"class":195},"\"UP\"",[186,330,305],{"class":234},[186,332,333],{"class":195},"\"DTZ\"",[186,335,305],{"class":234},[186,337,338],{"class":195},"\"PD\"",[186,340,305],{"class":234},[186,342,343],{"class":195},"\"PTH\"",[186,345,305],{"class":234},[186,347,348],{"class":195},"\"SIM\"",[186,350,305],{"class":234},[186,352,353],{"class":195},"\"RUF\"",[186,355,247],{"class":234},[186,357,359],{"class":85,"line":358},7,[186,360,275],{"emptyLinePlaceholder":274},[186,362,364,366,368,370,373],{"class":85,"line":363},8,[186,365,235],{"class":234},[186,367,238],{"class":191},[186,369,241],{"class":234},[186,371,372],{"class":191},"mypy",[186,374,247],{"class":234},[186,376,378,381],{"class":85,"line":377},9,[186,379,380],{"class":234},"python_version = ",[186,382,383],{"class":195},"\"3.12\"\n",[186,385,387,390,392],{"class":85,"line":386},10,[186,388,389],{"class":234},"files = [",[186,391,266],{"class":195},[186,393,247],{"class":234},[186,395,397,400],{"class":85,"line":396},11,[186,398,399],{"class":234},"warn_unused_ignores = ",[186,401,402],{"class":256},"true\n",[186,404,406,409],{"class":85,"line":405},12,[186,407,408],{"class":234},"disallow_untyped_defs = ",[186,410,402],{"class":256},[186,412,414,417],{"class":85,"line":413},13,[186,415,416],{"class":234},"check_untyped_defs = ",[186,418,402],{"class":256},[10,420,421,422,425,426,429,430,433,434,437,438,441],{},"The extra ruff rule sets matter for report code: ",[14,423,424],{},"B"," (bugbear) finds classic bugs, ",[14,427,428],{},"PD"," pandas pitfalls, ",[14,431,432],{},"DTZ"," timezone-naive timestamps and ",[14,435,436],{},"PTH"," string paths. ",[14,439,440],{},"disallow_untyped_defs"," makes mypy insist on annotated functions, which is what gives it something to check.",[172,443,445],{"id":444},"step-1-run-them-on-real-report-code","Step 1 — Run them on real report code",[10,447,448],{},"Here is a module that works on the author's machine most of the time:",[177,450,454],{"className":451,"code":452,"language":453,"meta":182,"style":182},"language-python shiki shiki-themes github-light github-dark-high-contrast","import datetime\nimport os\nimport pandas as pd\nfrom openpyxl import Workbook, load_workbook\n\n\ndef load_orders(path: str) -> pd.DataFrame:\n    orders = pd.read_excel(path, sheet_name=None)\n    return orders.dropna(how=\"all\")\n\n\ndef region_totals(orders: pd.DataFrame, regions=[]) -> pd.DataFrame:\n    regions.append(\"Unknown\")\n    orders.fillna({\"region\": \"Unknown\"}, inplace=True)\n    return orders.groupby(\"region\")[\"amount\"].sum().reset_index()\n\n\ndef write_summary(totals: pd.DataFrame, path: str) -> None:\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Summary\"\n    ws[\"A1\"] = f\"Summary\"\n    ws[\"B1\"] = datetime.datetime.now()\n    for row in totals.itertuples(index=False):\n        ws.append(list(row))\n    wb.save(path)\n\n\ndef grand_total(path: str) -> float:\n    ws = load_workbook(path, data_only=True)[\"Summary\"]\n    total = 0.0\n    for (value,) in ws.iter_rows(min_row=2, min_col=2, max_col=2, values_only=True):\n        total += value\n    return total\n\n\ndef main() -> None:\n    try:\n        orders = load_orders(\"orders.xlsx\")\n        write_summary(region_totals(orders), \"summary.xlsx\")\n    except:\n        print(\"failed\")\n","python",[14,455,456,465,472,485,498,502,506,524,547,565,569,573,588,598,625,644,649,654,675,686,697,708,728,743,769,781,787,792,797,816,840,851,901,913,921,926,931,946,954,970,981,989],{"__ignoreMap":182},[186,457,458,462],{"class":85,"line":188},[186,459,461],{"class":460},"s-kum","import",[186,463,464],{"class":234}," datetime\n",[186,466,467,469],{"class":85,"line":250},[186,468,461],{"class":460},[186,470,471],{"class":234}," os\n",[186,473,474,476,479,482],{"class":85,"line":260},[186,475,461],{"class":460},[186,477,478],{"class":234}," pandas ",[186,480,481],{"class":460},"as",[186,483,484],{"class":234}," pd\n",[186,486,487,490,493,495],{"class":85,"line":271},[186,488,489],{"class":460},"from",[186,491,492],{"class":234}," openpyxl ",[186,494,461],{"class":460},[186,496,497],{"class":234}," Workbook, load_workbook\n",[186,499,500],{"class":85,"line":278},[186,501,275],{"emptyLinePlaceholder":274},[186,503,504],{"class":85,"line":296},[186,505,275],{"emptyLinePlaceholder":274},[186,507,508,511,515,518,521],{"class":85,"line":358},[186,509,510],{"class":460},"def",[186,512,514],{"class":513},"s_Opv"," load_orders",[186,516,517],{"class":234},"(path: ",[186,519,520],{"class":256},"str",[186,522,523],{"class":234},") -> pd.DataFrame:\n",[186,525,526,529,532,535,539,541,544],{"class":85,"line":363},[186,527,528],{"class":234},"    orders ",[186,530,531],{"class":460},"=",[186,533,534],{"class":234}," pd.read_excel(path, ",[186,536,538],{"class":537},"sa561","sheet_name",[186,540,531],{"class":460},[186,542,543],{"class":256},"None",[186,545,546],{"class":234},")\n",[186,548,549,552,555,558,560,563],{"class":85,"line":377},[186,550,551],{"class":460},"    return",[186,553,554],{"class":234}," orders.dropna(",[186,556,557],{"class":537},"how",[186,559,531],{"class":460},[186,561,562],{"class":195},"\"all\"",[186,564,546],{"class":234},[186,566,567],{"class":85,"line":386},[186,568,275],{"emptyLinePlaceholder":274},[186,570,571],{"class":85,"line":396},[186,572,275],{"emptyLinePlaceholder":274},[186,574,575,577,580,583,585],{"class":85,"line":405},[186,576,510],{"class":460},[186,578,579],{"class":513}," region_totals",[186,581,582],{"class":234},"(orders: pd.DataFrame, regions",[186,584,531],{"class":460},[186,586,587],{"class":234},"[]) -> pd.DataFrame:\n",[186,589,590,593,596],{"class":85,"line":413},[186,591,592],{"class":234},"    regions.append(",[186,594,595],{"class":195},"\"Unknown\"",[186,597,546],{"class":234},[186,599,601,604,607,610,612,615,618,620,623],{"class":85,"line":600},14,[186,602,603],{"class":234},"    orders.fillna({",[186,605,606],{"class":195},"\"region\"",[186,608,609],{"class":234},": ",[186,611,595],{"class":195},[186,613,614],{"class":234},"}, ",[186,616,617],{"class":537},"inplace",[186,619,531],{"class":460},[186,621,622],{"class":256},"True",[186,624,546],{"class":234},[186,626,628,630,633,635,638,641],{"class":85,"line":627},15,[186,629,551],{"class":460},[186,631,632],{"class":234}," orders.groupby(",[186,634,606],{"class":195},[186,636,637],{"class":234},")[",[186,639,640],{"class":195},"\"amount\"",[186,642,643],{"class":234},"].sum().reset_index()\n",[186,645,647],{"class":85,"line":646},16,[186,648,275],{"emptyLinePlaceholder":274},[186,650,652],{"class":85,"line":651},17,[186,653,275],{"emptyLinePlaceholder":274},[186,655,657,659,662,665,667,670,672],{"class":85,"line":656},18,[186,658,510],{"class":460},[186,660,661],{"class":513}," write_summary",[186,663,664],{"class":234},"(totals: pd.DataFrame, path: ",[186,666,520],{"class":256},[186,668,669],{"class":234},") -> ",[186,671,543],{"class":256},[186,673,674],{"class":234},":\n",[186,676,678,681,683],{"class":85,"line":677},19,[186,679,680],{"class":234},"    wb ",[186,682,531],{"class":460},[186,684,685],{"class":234}," Workbook()\n",[186,687,689,692,694],{"class":85,"line":688},20,[186,690,691],{"class":234},"    ws ",[186,693,531],{"class":460},[186,695,696],{"class":234}," wb.active\n",[186,698,700,703,705],{"class":85,"line":699},21,[186,701,702],{"class":234},"    ws.title ",[186,704,531],{"class":460},[186,706,707],{"class":195}," \"Summary\"\n",[186,709,711,714,717,720,722,725],{"class":85,"line":710},22,[186,712,713],{"class":234},"    ws[",[186,715,716],{"class":195},"\"A1\"",[186,718,719],{"class":234},"] ",[186,721,531],{"class":460},[186,723,724],{"class":460}," f",[186,726,727],{"class":195},"\"Summary\"\n",[186,729,731,733,736,738,740],{"class":85,"line":730},23,[186,732,713],{"class":234},[186,734,735],{"class":195},"\"B1\"",[186,737,719],{"class":234},[186,739,531],{"class":460},[186,741,742],{"class":234}," datetime.datetime.now()\n",[186,744,746,749,752,755,758,761,763,766],{"class":85,"line":745},24,[186,747,748],{"class":460},"    for",[186,750,751],{"class":234}," row ",[186,753,754],{"class":460},"in",[186,756,757],{"class":234}," totals.itertuples(",[186,759,760],{"class":537},"index",[186,762,531],{"class":460},[186,764,765],{"class":256},"False",[186,767,768],{"class":234},"):\n",[186,770,772,775,778],{"class":85,"line":771},25,[186,773,774],{"class":234},"        ws.append(",[186,776,777],{"class":256},"list",[186,779,780],{"class":234},"(row))\n",[186,782,784],{"class":85,"line":783},26,[186,785,786],{"class":234},"    wb.save(path)\n",[186,788,790],{"class":85,"line":789},27,[186,791,275],{"emptyLinePlaceholder":274},[186,793,795],{"class":85,"line":794},28,[186,796,275],{"emptyLinePlaceholder":274},[186,798,800,802,805,807,809,811,814],{"class":85,"line":799},29,[186,801,510],{"class":460},[186,803,804],{"class":513}," grand_total",[186,806,517],{"class":234},[186,808,520],{"class":256},[186,810,669],{"class":234},[186,812,813],{"class":256},"float",[186,815,674],{"class":234},[186,817,819,821,823,826,829,831,833,835,838],{"class":85,"line":818},30,[186,820,691],{"class":234},[186,822,531],{"class":460},[186,824,825],{"class":234}," load_workbook(path, ",[186,827,828],{"class":537},"data_only",[186,830,531],{"class":460},[186,832,622],{"class":256},[186,834,637],{"class":234},[186,836,837],{"class":195},"\"Summary\"",[186,839,247],{"class":234},[186,841,843,846,848],{"class":85,"line":842},31,[186,844,845],{"class":234},"    total ",[186,847,531],{"class":460},[186,849,850],{"class":256}," 0.0\n",[186,852,854,856,859,861,864,867,869,872,874,877,879,881,883,886,888,890,892,895,897,899],{"class":85,"line":853},32,[186,855,748],{"class":460},[186,857,858],{"class":234}," (value,) ",[186,860,754],{"class":460},[186,862,863],{"class":234}," ws.iter_rows(",[186,865,866],{"class":537},"min_row",[186,868,531],{"class":460},[186,870,871],{"class":256},"2",[186,873,305],{"class":234},[186,875,876],{"class":537},"min_col",[186,878,531],{"class":460},[186,880,871],{"class":256},[186,882,305],{"class":234},[186,884,885],{"class":537},"max_col",[186,887,531],{"class":460},[186,889,871],{"class":256},[186,891,305],{"class":234},[186,893,894],{"class":537},"values_only",[186,896,531],{"class":460},[186,898,622],{"class":256},[186,900,768],{"class":234},[186,902,904,907,910],{"class":85,"line":903},33,[186,905,906],{"class":234},"        total ",[186,908,909],{"class":460},"+=",[186,911,912],{"class":234}," value\n",[186,914,916,918],{"class":85,"line":915},34,[186,917,551],{"class":460},[186,919,920],{"class":234}," total\n",[186,922,924],{"class":85,"line":923},35,[186,925,275],{"emptyLinePlaceholder":274},[186,927,929],{"class":85,"line":928},36,[186,930,275],{"emptyLinePlaceholder":274},[186,932,934,936,939,942,944],{"class":85,"line":933},37,[186,935,510],{"class":460},[186,937,938],{"class":513}," main",[186,940,941],{"class":234},"() -> ",[186,943,543],{"class":256},[186,945,674],{"class":234},[186,947,949,952],{"class":85,"line":948},38,[186,950,951],{"class":460},"    try",[186,953,674],{"class":234},[186,955,957,960,962,965,968],{"class":85,"line":956},39,[186,958,959],{"class":234},"        orders ",[186,961,531],{"class":460},[186,963,964],{"class":234}," load_orders(",[186,966,967],{"class":195},"\"orders.xlsx\"",[186,969,546],{"class":234},[186,971,973,976,979],{"class":85,"line":972},40,[186,974,975],{"class":234},"        write_summary(region_totals(orders), ",[186,977,978],{"class":195},"\"summary.xlsx\"",[186,980,546],{"class":234},[186,982,984,987],{"class":85,"line":983},41,[186,985,986],{"class":460},"    except",[186,988,674],{"class":234},[186,990,992,995,998,1001],{"class":85,"line":991},42,[186,993,994],{"class":256},"        print",[186,996,997],{"class":234},"(",[186,999,1000],{"class":195},"\"failed\"",[186,1002,546],{"class":234},[177,1004,1006],{"className":179,"code":1005,"language":181,"meta":182,"style":182},"ruff check --output-format concise src\nmypy\n",[14,1007,1008,1024],{"__ignoreMap":182},[186,1009,1010,1012,1015,1018,1021],{"class":85,"line":188},[186,1011,244],{"class":191},[186,1013,1014],{"class":195}," check",[186,1016,1017],{"class":256}," --output-format",[186,1019,1020],{"class":195}," concise",[186,1022,1023],{"class":195}," src\n",[186,1025,1026],{"class":85,"line":250},[186,1027,1028],{"class":191},"mypy\n",[10,1030,1031],{},"Ruff reported seven findings and mypy sixteen errors. The interesting ones:",[25,1033,34,1038,34,1041,34,1044,34,1047,34,1053,34,1058,34,1064,34,1067,34,1071,34,1074,34,1078,34,1083,34,1089,34,1091,34,1095,34,1097,34,1101,34,1105,34,1107,34,1110,34,1113,34,1117,34,1119,34,1123,34,1125,34,1128,34,1130,34,1133,34,1136,34,1140,34,1142,34,1146,34,1148,34,1151,34,1153,34,1156,34,1159,34,1163,34,1165,34,1169,34,1171,34,1174,34,1176,34,1179,34,1182,34,1186,34,1188,34,1192,34,1194,34,1197,34,1199,34,1202,34,1205,34,1209,34,1211,34,1215,34,1217,34,1220,34,1222],{"viewBox":1034,"role":28,"ariaLabelledBy":1035,"xmlns":32,"style":33},"4 0 752 276",[1036,1037],"tc-findings-t","tc-findings-d",[36,1039,1040],{"id":1036},"What each tool found in the report module",[40,1042,1043],{"id":1037},"Ruff flagged the mutable default argument, inplace=True, the bare except and datetime.now without a time zone. mypy found that read_excel with sheet_name=None returns a dict that has no dropna, that wb.active may be None so title, indexing and append are unsafe, and that adding a cell value to a float fails for text, dates, formula objects and None.",[44,1045],{"x":46,"y":47,"width":48,"height":1046,"fill":50},"276",[44,1048],{"x":53,"y":54,"width":1049,"height":1050,"rx":47,"fill":50,"stroke":1051,"style":1052},"115.9","40.0","var(--line,#cdd5e6)","stroke-width:1px",[44,1054],{"x":1055,"y":54,"width":1056,"height":1050,"rx":47,"fill":58,"stroke":59,"style":1057},"135.9","201.4","stroke-width:1.2px",[73,1059,1063],{"x":1060,"y":1061,"style":1062},"236.6","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Tool and code",[44,1065],{"x":1066,"y":54,"width":1056,"height":1050,"rx":47,"fill":58,"stroke":59,"style":1057},"337.3",[73,1068,1070],{"x":1069,"y":1061,"style":1062},"438.0","Message",[44,1072],{"x":1073,"y":54,"width":1056,"height":1050,"rx":47,"fill":58,"stroke":59,"style":1057},"538.6",[73,1075,1077],{"x":1076,"y":1061,"style":1062},"639.3","Risk",[44,1079],{"x":53,"y":1080,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"56.0","34.0","#e7ebef",[73,1084,1088],{"x":1085,"y":1086,"style":1087},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","load_orders",[44,1090],{"x":1055,"y":1080,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1092,1094],{"x":1060,"y":1093,"style":82},"77.0","mypy attr-defined",[44,1096],{"x":1066,"y":1080,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1098,1100],{"x":1069,"y":1099,"style":82},"71.0","\"dict[str, DataFrame]\" has no",[73,1102,1104],{"x":1069,"y":1103,"style":82},"84.0","attribute \"dropna\"",[44,1106],{"x":1073,"y":1080,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1108,1109],{"x":1076,"y":1093,"style":82},"crash on every run",[44,1111],{"x":53,"y":1112,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"90.0",[73,1114,1116],{"x":1085,"y":1115,"style":1087},"112.0","regions=[]",[44,1118],{"x":1055,"y":1112,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1120,1122],{"x":1060,"y":1121,"style":82},"111.0","ruff B006",[44,1124],{"x":1066,"y":1112,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1126,1127],{"x":1069,"y":1121,"style":82},"mutable default argument",[44,1129],{"x":1073,"y":1112,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1131,1132],{"x":1076,"y":1121,"style":82},"values leak between calls",[44,1134],{"x":53,"y":1135,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"124.0",[73,1137,1139],{"x":1085,"y":1138,"style":1087},"146.0","inplace=True",[44,1141],{"x":1055,"y":1135,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1143,1145],{"x":1060,"y":1144,"style":82},"145.0","ruff PD002",[44,1147],{"x":1066,"y":1135,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1149,1150],{"x":1069,"y":1144,"style":82},"inconsistent behaviour",[44,1152],{"x":1073,"y":1135,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1154,1155],{"x":1076,"y":1144,"style":82},"silent no-op on copies",[44,1157],{"x":53,"y":1158,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"158.0",[73,1160,1162],{"x":1085,"y":1161,"style":1087},"180.0","wb.active",[44,1164],{"x":1055,"y":1158,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1166,1168],{"x":1060,"y":1167,"style":82},"179.0","mypy union-attr",[44,1170],{"x":1066,"y":1158,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1172,1173],{"x":1069,"y":1167,"style":82},"Item None … has no attribute title",[44,1175],{"x":1073,"y":1158,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1177,1178],{"x":1076,"y":1167,"style":82},"crash if None",[44,1180],{"x":53,"y":1181,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"192.0",[73,1183,1185],{"x":1085,"y":1184,"style":1087},"214.0","total += value",[44,1187],{"x":1055,"y":1181,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1189,1191],{"x":1060,"y":1190,"style":82},"213.0","mypy operator",[44,1193],{"x":1066,"y":1181,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1195,1196],{"x":1069,"y":1190,"style":82},"float + str, date, None, …",[44,1198],{"x":1073,"y":1181,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1200,1201],{"x":1076,"y":1190,"style":82},"crash on a text cell",[44,1203],{"x":53,"y":1204,"width":1049,"height":1081,"rx":47,"fill":1082,"stroke":1051,"style":1052},"226.0",[73,1206,1208],{"x":1085,"y":1207,"style":1087},"248.0","except:",[44,1210],{"x":1055,"y":1204,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1212,1214],{"x":1060,"y":1213,"style":82},"247.0","ruff E722",[44,1216],{"x":1066,"y":1204,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1218,1219],{"x":1069,"y":1213,"style":82},"bare except",[44,1221],{"x":1073,"y":1204,"width":1056,"height":1081,"rx":47,"fill":50,"stroke":1051,"style":1052},[73,1223,1224],{"x":1076,"y":1213,"style":82},"hides every failure",[10,1226,1227,1228,1231,1232,1236,1237,1240,1241,1243,1244,241],{},"The first mypy error is a real bug: ",[14,1229,1230],{},"sheet_name=None"," reads ",[1233,1234,1235],"em",{},"every"," sheet into a dictionary, so ",[14,1238,1239],{},".dropna"," would fail on the first run. mypy knows this because pandas-stubs gives ",[14,1242,16],{}," separate signatures for a single sheet and for ",[14,1245,543],{},[172,1247,1249],{"id":1248},"step-2-fix-the-findings","Step 2 — Fix the findings",[10,1251,1252],{},"Each fix is small, and several make the code more honest about what it expects:",[177,1254,1256],{"className":451,"code":1255,"language":453,"meta":182,"style":182},"import datetime as dt\nimport logging\nfrom pathlib import Path\n\nimport pandas as pd\nfrom openpyxl import Workbook, load_workbook\nfrom openpyxl.worksheet.worksheet import Worksheet\n\nlog = logging.getLogger(__name__)\n\n\ndef load_orders(path: Path) -> pd.DataFrame:\n    orders = pd.read_excel(path, sheet_name=\"Orders\")\n    return orders.dropna(how=\"all\")\n\n\ndef region_totals(orders: pd.DataFrame, regions: list[str] | None = None) -> pd.DataFrame:\n    orders = orders.fillna({\"region\": \"Unknown\"})\n    if regions is not None:\n        orders = orders[orders[\"region\"].isin([*regions, \"Unknown\"])]\n    return orders.groupby(\"region\")[\"amount\"].sum().reset_index()\n\n\ndef write_summary(totals: pd.DataFrame, path: Path) -> None:\n    wb = Workbook()\n    ws = wb.active\n    if not isinstance(ws, Worksheet):\n        raise TypeError(\"new workbook has no active worksheet\")\n    ws.title = \"Summary\"\n    ws[\"A1\"] = \"Summary\"\n    ws[\"B1\"] = dt.datetime.now(tz=dt.UTC).replace(tzinfo=None)\n    for row in totals.itertuples(index=False):\n        ws.append(list(row))\n    wb.save(path)\n\n\ndef grand_total(path: Path) -> float:\n    ws = load_workbook(path, data_only=True)[\"Summary\"]\n    total = 0.0\n    for (value,) in ws.iter_rows(min_row=2, min_col=2, max_col=2, values_only=True):\n        if not isinstance(value, (int, float)):\n            raise ValueError(f\"expected a number in column B, found {value!r}\")\n        total += value\n    return total\n\n\ndef main() -> int:\n    try:\n        orders = load_orders(Path(\"orders.xlsx\"))\n        write_summary(region_totals(orders), Path(\"summary.xlsx\"))\n    except Exception:\n        log.exception(\"summary failed\")\n        return 1\n    return 0\n",[14,1257,1258,1270,1277,1289,1293,1303,1313,1325,1329,1344,1348,1352,1361,1378,1392,1396,1400,1426,1444,1462,1487,1501,1505,1509,1522,1530,1538,1550,1565,1573,1585,1621,1639,1647,1651,1655,1659,1672,1692,1700,1742,1764,1798,1807,1814,1819,1824,1837,1844,1859,1869,1879,1890,1899],{"__ignoreMap":182},[186,1259,1260,1262,1265,1267],{"class":85,"line":188},[186,1261,461],{"class":460},[186,1263,1264],{"class":234}," datetime ",[186,1266,481],{"class":460},[186,1268,1269],{"class":234}," dt\n",[186,1271,1272,1274],{"class":85,"line":250},[186,1273,461],{"class":460},[186,1275,1276],{"class":234}," logging\n",[186,1278,1279,1281,1284,1286],{"class":85,"line":260},[186,1280,489],{"class":460},[186,1282,1283],{"class":234}," pathlib ",[186,1285,461],{"class":460},[186,1287,1288],{"class":234}," Path\n",[186,1290,1291],{"class":85,"line":271},[186,1292,275],{"emptyLinePlaceholder":274},[186,1294,1295,1297,1299,1301],{"class":85,"line":278},[186,1296,461],{"class":460},[186,1298,478],{"class":234},[186,1300,481],{"class":460},[186,1302,484],{"class":234},[186,1304,1305,1307,1309,1311],{"class":85,"line":296},[186,1306,489],{"class":460},[186,1308,492],{"class":234},[186,1310,461],{"class":460},[186,1312,497],{"class":234},[186,1314,1315,1317,1320,1322],{"class":85,"line":358},[186,1316,489],{"class":460},[186,1318,1319],{"class":234}," openpyxl.worksheet.worksheet ",[186,1321,461],{"class":460},[186,1323,1324],{"class":234}," Worksheet\n",[186,1326,1327],{"class":85,"line":363},[186,1328,275],{"emptyLinePlaceholder":274},[186,1330,1331,1334,1336,1339,1342],{"class":85,"line":377},[186,1332,1333],{"class":234},"log ",[186,1335,531],{"class":460},[186,1337,1338],{"class":234}," logging.getLogger(",[186,1340,1341],{"class":256},"__name__",[186,1343,546],{"class":234},[186,1345,1346],{"class":85,"line":386},[186,1347,275],{"emptyLinePlaceholder":274},[186,1349,1350],{"class":85,"line":396},[186,1351,275],{"emptyLinePlaceholder":274},[186,1353,1354,1356,1358],{"class":85,"line":405},[186,1355,510],{"class":460},[186,1357,514],{"class":513},[186,1359,1360],{"class":234},"(path: Path) -> pd.DataFrame:\n",[186,1362,1363,1365,1367,1369,1371,1373,1376],{"class":85,"line":413},[186,1364,528],{"class":234},[186,1366,531],{"class":460},[186,1368,534],{"class":234},[186,1370,538],{"class":537},[186,1372,531],{"class":460},[186,1374,1375],{"class":195},"\"Orders\"",[186,1377,546],{"class":234},[186,1379,1380,1382,1384,1386,1388,1390],{"class":85,"line":600},[186,1381,551],{"class":460},[186,1383,554],{"class":234},[186,1385,557],{"class":537},[186,1387,531],{"class":460},[186,1389,562],{"class":195},[186,1391,546],{"class":234},[186,1393,1394],{"class":85,"line":627},[186,1395,275],{"emptyLinePlaceholder":274},[186,1397,1398],{"class":85,"line":646},[186,1399,275],{"emptyLinePlaceholder":274},[186,1401,1402,1404,1406,1409,1411,1413,1416,1419,1422,1424],{"class":85,"line":651},[186,1403,510],{"class":460},[186,1405,579],{"class":513},[186,1407,1408],{"class":234},"(orders: pd.DataFrame, regions: list[",[186,1410,520],{"class":256},[186,1412,719],{"class":234},[186,1414,1415],{"class":460},"|",[186,1417,1418],{"class":256}," None",[186,1420,1421],{"class":460}," =",[186,1423,1418],{"class":256},[186,1425,523],{"class":234},[186,1427,1428,1430,1432,1435,1437,1439,1441],{"class":85,"line":656},[186,1429,528],{"class":234},[186,1431,531],{"class":460},[186,1433,1434],{"class":234}," orders.fillna({",[186,1436,606],{"class":195},[186,1438,609],{"class":234},[186,1440,595],{"class":195},[186,1442,1443],{"class":234},"})\n",[186,1445,1446,1449,1452,1455,1458,1460],{"class":85,"line":677},[186,1447,1448],{"class":460},"    if",[186,1450,1451],{"class":234}," regions ",[186,1453,1454],{"class":460},"is",[186,1456,1457],{"class":460}," not",[186,1459,1418],{"class":256},[186,1461,674],{"class":234},[186,1463,1464,1466,1468,1471,1473,1476,1479,1482,1484],{"class":85,"line":688},[186,1465,959],{"class":234},[186,1467,531],{"class":460},[186,1469,1470],{"class":234}," orders[orders[",[186,1472,606],{"class":195},[186,1474,1475],{"class":234},"].isin([",[186,1477,1478],{"class":460},"*",[186,1480,1481],{"class":234},"regions, ",[186,1483,595],{"class":195},[186,1485,1486],{"class":234},"])]\n",[186,1488,1489,1491,1493,1495,1497,1499],{"class":85,"line":699},[186,1490,551],{"class":460},[186,1492,632],{"class":234},[186,1494,606],{"class":195},[186,1496,637],{"class":234},[186,1498,640],{"class":195},[186,1500,643],{"class":234},[186,1502,1503],{"class":85,"line":710},[186,1504,275],{"emptyLinePlaceholder":274},[186,1506,1507],{"class":85,"line":730},[186,1508,275],{"emptyLinePlaceholder":274},[186,1510,1511,1513,1515,1518,1520],{"class":85,"line":745},[186,1512,510],{"class":460},[186,1514,661],{"class":513},[186,1516,1517],{"class":234},"(totals: pd.DataFrame, path: Path) -> ",[186,1519,543],{"class":256},[186,1521,674],{"class":234},[186,1523,1524,1526,1528],{"class":85,"line":771},[186,1525,680],{"class":234},[186,1527,531],{"class":460},[186,1529,685],{"class":234},[186,1531,1532,1534,1536],{"class":85,"line":783},[186,1533,691],{"class":234},[186,1535,531],{"class":460},[186,1537,696],{"class":234},[186,1539,1540,1542,1544,1547],{"class":85,"line":789},[186,1541,1448],{"class":460},[186,1543,1457],{"class":460},[186,1545,1546],{"class":256}," isinstance",[186,1548,1549],{"class":234},"(ws, Worksheet):\n",[186,1551,1552,1555,1558,1560,1563],{"class":85,"line":794},[186,1553,1554],{"class":460},"        raise",[186,1556,1557],{"class":256}," TypeError",[186,1559,997],{"class":234},[186,1561,1562],{"class":195},"\"new workbook has no active worksheet\"",[186,1564,546],{"class":234},[186,1566,1567,1569,1571],{"class":85,"line":799},[186,1568,702],{"class":234},[186,1570,531],{"class":460},[186,1572,707],{"class":195},[186,1574,1575,1577,1579,1581,1583],{"class":85,"line":818},[186,1576,713],{"class":234},[186,1578,716],{"class":195},[186,1580,719],{"class":234},[186,1582,531],{"class":460},[186,1584,707],{"class":195},[186,1586,1587,1589,1591,1593,1595,1598,1601,1603,1606,1609,1612,1615,1617,1619],{"class":85,"line":842},[186,1588,713],{"class":234},[186,1590,735],{"class":195},[186,1592,719],{"class":234},[186,1594,531],{"class":460},[186,1596,1597],{"class":234}," dt.datetime.now(",[186,1599,1600],{"class":537},"tz",[186,1602,531],{"class":460},[186,1604,1605],{"class":234},"dt.",[186,1607,1608],{"class":256},"UTC",[186,1610,1611],{"class":234},").replace(",[186,1613,1614],{"class":537},"tzinfo",[186,1616,531],{"class":460},[186,1618,543],{"class":256},[186,1620,546],{"class":234},[186,1622,1623,1625,1627,1629,1631,1633,1635,1637],{"class":85,"line":853},[186,1624,748],{"class":460},[186,1626,751],{"class":234},[186,1628,754],{"class":460},[186,1630,757],{"class":234},[186,1632,760],{"class":537},[186,1634,531],{"class":460},[186,1636,765],{"class":256},[186,1638,768],{"class":234},[186,1640,1641,1643,1645],{"class":85,"line":903},[186,1642,774],{"class":234},[186,1644,777],{"class":256},[186,1646,780],{"class":234},[186,1648,1649],{"class":85,"line":915},[186,1650,786],{"class":234},[186,1652,1653],{"class":85,"line":923},[186,1654,275],{"emptyLinePlaceholder":274},[186,1656,1657],{"class":85,"line":928},[186,1658,275],{"emptyLinePlaceholder":274},[186,1660,1661,1663,1665,1668,1670],{"class":85,"line":933},[186,1662,510],{"class":460},[186,1664,804],{"class":513},[186,1666,1667],{"class":234},"(path: Path) -> ",[186,1669,813],{"class":256},[186,1671,674],{"class":234},[186,1673,1674,1676,1678,1680,1682,1684,1686,1688,1690],{"class":85,"line":948},[186,1675,691],{"class":234},[186,1677,531],{"class":460},[186,1679,825],{"class":234},[186,1681,828],{"class":537},[186,1683,531],{"class":460},[186,1685,622],{"class":256},[186,1687,637],{"class":234},[186,1689,837],{"class":195},[186,1691,247],{"class":234},[186,1693,1694,1696,1698],{"class":85,"line":956},[186,1695,845],{"class":234},[186,1697,531],{"class":460},[186,1699,850],{"class":256},[186,1701,1702,1704,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724,1726,1728,1730,1732,1734,1736,1738,1740],{"class":85,"line":972},[186,1703,748],{"class":460},[186,1705,858],{"class":234},[186,1707,754],{"class":460},[186,1709,863],{"class":234},[186,1711,866],{"class":537},[186,1713,531],{"class":460},[186,1715,871],{"class":256},[186,1717,305],{"class":234},[186,1719,876],{"class":537},[186,1721,531],{"class":460},[186,1723,871],{"class":256},[186,1725,305],{"class":234},[186,1727,885],{"class":537},[186,1729,531],{"class":460},[186,1731,871],{"class":256},[186,1733,305],{"class":234},[186,1735,894],{"class":537},[186,1737,531],{"class":460},[186,1739,622],{"class":256},[186,1741,768],{"class":234},[186,1743,1744,1747,1749,1751,1754,1757,1759,1761],{"class":85,"line":983},[186,1745,1746],{"class":460},"        if",[186,1748,1457],{"class":460},[186,1750,1546],{"class":256},[186,1752,1753],{"class":234},"(value, (",[186,1755,1756],{"class":256},"int",[186,1758,305],{"class":234},[186,1760,813],{"class":256},[186,1762,1763],{"class":234},")):\n",[186,1765,1766,1769,1772,1774,1777,1780,1784,1787,1790,1793,1796],{"class":85,"line":991},[186,1767,1768],{"class":460},"            raise",[186,1770,1771],{"class":256}," ValueError",[186,1773,997],{"class":234},[186,1775,1776],{"class":460},"f",[186,1778,1779],{"class":195},"\"expected a number in column B, found ",[186,1781,1783],{"class":1782},"sSjpA","{",[186,1785,1786],{"class":234},"value",[186,1788,1789],{"class":460},"!r",[186,1791,1792],{"class":1782},"}",[186,1794,1795],{"class":195},"\"",[186,1797,546],{"class":234},[186,1799,1801,1803,1805],{"class":85,"line":1800},43,[186,1802,906],{"class":234},[186,1804,909],{"class":460},[186,1806,912],{"class":234},[186,1808,1810,1812],{"class":85,"line":1809},44,[186,1811,551],{"class":460},[186,1813,920],{"class":234},[186,1815,1817],{"class":85,"line":1816},45,[186,1818,275],{"emptyLinePlaceholder":274},[186,1820,1822],{"class":85,"line":1821},46,[186,1823,275],{"emptyLinePlaceholder":274},[186,1825,1827,1829,1831,1833,1835],{"class":85,"line":1826},47,[186,1828,510],{"class":460},[186,1830,938],{"class":513},[186,1832,941],{"class":234},[186,1834,1756],{"class":256},[186,1836,674],{"class":234},[186,1838,1840,1842],{"class":85,"line":1839},48,[186,1841,951],{"class":460},[186,1843,674],{"class":234},[186,1845,1847,1849,1851,1854,1856],{"class":85,"line":1846},49,[186,1848,959],{"class":234},[186,1850,531],{"class":460},[186,1852,1853],{"class":234}," load_orders(Path(",[186,1855,967],{"class":195},[186,1857,1858],{"class":234},"))\n",[186,1860,1862,1865,1867],{"class":85,"line":1861},50,[186,1863,1864],{"class":234},"        write_summary(region_totals(orders), Path(",[186,1866,978],{"class":195},[186,1868,1858],{"class":234},[186,1870,1872,1874,1877],{"class":85,"line":1871},51,[186,1873,986],{"class":460},[186,1875,1876],{"class":256}," Exception",[186,1878,674],{"class":234},[186,1880,1882,1885,1888],{"class":85,"line":1881},52,[186,1883,1884],{"class":234},"        log.exception(",[186,1886,1887],{"class":195},"\"summary failed\"",[186,1889,546],{"class":234},[186,1891,1893,1896],{"class":85,"line":1892},53,[186,1894,1895],{"class":460},"        return",[186,1897,1898],{"class":256}," 1\n",[186,1900,1902,1904],{"class":85,"line":1901},54,[186,1903,551],{"class":460},[186,1905,1906],{"class":256}," 0\n",[10,1908,1909,1911,1912,1914,1915,1918,1919,305,1922,214,1925,1928,1929,1932,1933,1935,1936,1938,1939,1941,1942,1945],{},[14,1910,1162],{}," is typed as \"worksheet, chartsheet or ",[14,1913,543],{},"\", so the ",[14,1916,1917],{},"isinstance"," check narrows it — after that line mypy accepts ",[14,1920,1921],{},"ws.title",[14,1923,1924],{},"ws[\"A1\"]",[14,1926,1927],{},"ws.append",". In ",[14,1930,1931],{},"grand_total",", a cell value can be a number, text, date, time, formula object, rich text or ",[14,1934,543],{},"; mypy listed all of them when the code added ",[14,1937,1786],{}," to a float. The ",[14,1940,1917],{}," check both satisfies mypy and turns a text cell into a clear error message. ",[14,1943,1944],{},"main"," now logs the exception and returns an exit code instead of printing \"failed\".",[10,1947,1948,1949,1952,1953,1956,1957,1960,1961,1963,1964,1967,1968,1971,1972,1976],{},"The timestamp fix needs one more step than ruff's ",[14,1950,1951],{},"DTZ005"," message suggests. ",[14,1954,1955],{},"datetime.now(tz=dt.UTC)"," makes the time unambiguous, but openpyxl refuses to save it: ",[14,1958,1959],{},"TypeError: Excel does not support timezones in datetimes. The tzinfo in the datetime\u002Ftime object must be set to None."," Excel cells have no time zone, so decide which zone the report shows — UTC here — and drop the ",[14,1962,1614],{}," after converting, as ",[14,1965,1966],{},"write_summary"," does with ",[14,1969,1970],{},".replace(tzinfo=None)",". The error itself is covered in ",[19,1973,1975],{"href":1974},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-does-not-support-timezones-in-datetimes\u002F","Fix \"Excel does not support datetimes with timezones\" in pandas",". Both tools then passed, and a quick run confirmed the behaviour:",[177,1978,1980],{"className":451,"code":1979,"language":453,"meta":182,"style":182},"import pandas as pd\n\norders = pd.DataFrame({\"region\": [\"North\", None, \"South\", \"North\"], \"amount\": [10.0, 5.0, 7.5, 2.5]})\ntotals = region_totals(orders)\nwrite_summary(totals, Path(\"summary.xlsx\"))\nprint(totals.to_dict(\"records\"), grand_total(Path(\"summary.xlsx\")))\n# [{'region': 'North', 'amount': 12.5}, {'region': 'South', 'amount': 7.5}, {'region': 'Unknown', 'amount': 5.0}] 25.0\n",[14,1981,1982,1992,1996,2055,2065,2074,2093],{"__ignoreMap":182},[186,1983,1984,1986,1988,1990],{"class":85,"line":188},[186,1985,461],{"class":460},[186,1987,478],{"class":234},[186,1989,481],{"class":460},[186,1991,484],{"class":234},[186,1993,1994],{"class":85,"line":250},[186,1995,275],{"emptyLinePlaceholder":274},[186,1997,1998,2001,2003,2006,2008,2011,2014,2016,2018,2020,2023,2025,2027,2030,2032,2034,2037,2039,2042,2044,2047,2049,2052],{"class":85,"line":260},[186,1999,2000],{"class":234},"orders ",[186,2002,531],{"class":460},[186,2004,2005],{"class":234}," pd.DataFrame({",[186,2007,606],{"class":195},[186,2009,2010],{"class":234},": [",[186,2012,2013],{"class":195},"\"North\"",[186,2015,305],{"class":234},[186,2017,543],{"class":256},[186,2019,305],{"class":234},[186,2021,2022],{"class":195},"\"South\"",[186,2024,305],{"class":234},[186,2026,2013],{"class":195},[186,2028,2029],{"class":234},"], ",[186,2031,640],{"class":195},[186,2033,2010],{"class":234},[186,2035,2036],{"class":256},"10.0",[186,2038,305],{"class":234},[186,2040,2041],{"class":256},"5.0",[186,2043,305],{"class":234},[186,2045,2046],{"class":256},"7.5",[186,2048,305],{"class":234},[186,2050,2051],{"class":256},"2.5",[186,2053,2054],{"class":234},"]})\n",[186,2056,2057,2060,2062],{"class":85,"line":271},[186,2058,2059],{"class":234},"totals ",[186,2061,531],{"class":460},[186,2063,2064],{"class":234}," region_totals(orders)\n",[186,2066,2067,2070,2072],{"class":85,"line":278},[186,2068,2069],{"class":234},"write_summary(totals, Path(",[186,2071,978],{"class":195},[186,2073,1858],{"class":234},[186,2075,2076,2079,2082,2085,2088,2090],{"class":85,"line":296},[186,2077,2078],{"class":256},"print",[186,2080,2081],{"class":234},"(totals.to_dict(",[186,2083,2084],{"class":195},"\"records\"",[186,2086,2087],{"class":234},"), grand_total(Path(",[186,2089,978],{"class":195},[186,2091,2092],{"class":234},")))\n",[186,2094,2095],{"class":85,"line":358},[186,2096,2098],{"class":2097},"s-wDw","# [{'region': 'North', 'amount': 12.5}, {'region': 'South', 'amount': 7.5}, {'region': 'Unknown', 'amount': 5.0}] 25.0\n",[172,2100,2102],{"id":2101},"step-3-when-the-stubs-are-wrong","Step 3 — When the stubs are wrong",[10,2104,2105,2106,2109,2110,2113,2114,2117],{},"Type stubs describe the libraries imperfectly. The first version of the fix used ",[14,2107,2108],{},"orders.groupby(\"region\", as_index=False)[\"amount\"].sum()",", which returns a DataFrame at runtime, but mypy reported ",[14,2111,2112],{},"Incompatible return value type (got \"Series[Any]\", expected \"DataFrame\")"," — the stubs do not model ",[14,2115,2116],{},"as_index=False",". Three ways out, in order of preference:",[2119,2120,2121,2129,2136],"ol",{},[2122,2123,2124,2125,2128],"li",{},"Use an equivalent spelling the stubs understand — here ",[14,2126,2127],{},".sum().reset_index()",", which is what the final code does.",[2122,2130,2131,2132,2135],{},"Use ",[14,2133,2134],{},"typing.cast(pd.DataFrame, …)"," when there is no such spelling, which documents the assumption.",[2122,2137,2138,2139,2142,2143,2146],{},"As a last resort, ",[14,2140,2141],{},"# type: ignore[return-value]"," with the specific error code. ",[14,2144,2145],{},"warn_unused_ignores"," then reports the comment once a stub update makes it unnecessary.",[10,2148,2149,2150,2153],{},"Avoid silencing whole modules with ",[14,2151,2152],{},"ignore_errors","; the value of the checker is in the calls you did not think about.",[25,2155,34,2161,34,2164,34,2167,34,2171,34,2176,34,2182,34,2187,34,2192,34,2194,34,2198,34,2201,34,2206,34,2209,34,2213,34,2216,34,2220],{"viewBox":2156,"role":28,"ariaLabelledBy":2157,"xmlns":32,"style":2160},"4 0 735 166",[2158,2159],"tc-time-t","tc-time-d","width:100%;max-width:735px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,2162,2163],{"id":2158},"How long the checks took on the example project",[40,2165,2166],{"id":2159},"Ruff checked the project in about 8 milliseconds. mypy took 6.3 seconds on the first run, mostly reading the pandas stubs, and 0.15 seconds on later runs with its cache.",[44,2168],{"x":46,"y":47,"width":2169,"height":2170,"fill":50},"735","166",[44,2172],{"x":53,"y":54,"width":2173,"height":2174,"rx":2175,"fill":1082,"stroke":1051,"style":1052},"149.8","28.0","7",[73,2177,2181],{"x":2178,"y":2179,"style":2180},"94.9","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","ruff check",[44,2183],{"x":2184,"y":54,"width":2185,"height":2174,"rx":2175,"fill":154,"stroke":155,"style":2186},"179.8","6.0","stroke-width:1.5px",[73,2188,2191],{"x":2189,"y":2179,"style":2190},"193.8","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","0.008 s",[44,2193],{"x":53,"y":1080,"width":2173,"height":2174,"rx":2175,"fill":1082,"stroke":1051,"style":1052},[73,2195,2197],{"x":2178,"y":2196,"style":2180},"74.5","mypy, cold cache",[44,2199],{"x":2184,"y":1080,"width":2200,"height":2174,"rx":2175,"fill":121,"stroke":122,"style":2186},"496.2",[73,2202,2205],{"x":2203,"y":2196,"style":2204},"684.1","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","6.29 s",[44,2207],{"x":53,"y":2208,"width":2173,"height":2174,"rx":2175,"fill":1082,"stroke":1051,"style":1052},"96.0",[73,2210,2212],{"x":2178,"y":2211,"style":2180},"114.5","mypy, warm cache",[44,2214],{"x":2184,"y":2208,"width":2215,"height":2174,"rx":2175,"fill":154,"stroke":155,"style":2186},"11.8",[73,2217,2219],{"x":2218,"y":2211,"style":2190},"199.7","0.15 s",[73,2221,2224],{"x":2222,"y":1138,"style":2223},"380.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","cache .mypy_cache in CI to keep the type check fast",[172,2226,2228],{"id":2227},"step-4-run-the-checks-everywhere","Step 4 — Run the checks everywhere",[10,2230,2231,2232,2235],{},"Checks help only if they run without anyone remembering to. Ruff's editor integration flags problems as you type; ",[14,2233,2234],{},"ruff check --fix"," applies the safe fixes, such as sorting imports and removing unused ones. A pre-commit hook runs both tools before every commit:",[177,2237,2241],{"className":2238,"code":2239,"language":2240,"meta":182,"style":182},"language-yaml shiki shiki-themes github-light github-dark-high-contrast","# .pre-commit-config.yaml\nrepos:\n  - repo: https:\u002F\u002Fgithub.com\u002Fastral-sh\u002Fruff-pre-commit\n    rev: v0.16.8\n    hooks:\n      - id: ruff\n        args: [--fix]\n      - id: ruff-format\n  - repo: https:\u002F\u002Fgithub.com\u002Fpre-commit\u002Fmirrors-mypy\n    rev: v2.3.1\n    hooks:\n      - id: mypy\n        additional_dependencies: [pandas-stubs, types-openpyxl]\n","yaml",[14,2242,2243,2248,2256,2269,2279,2286,2299,2311,2322,2333,2342,2348,2358],{"__ignoreMap":182},[186,2244,2245],{"class":85,"line":188},[186,2246,2247],{"class":2097},"# .pre-commit-config.yaml\n",[186,2249,2250,2254],{"class":85,"line":250},[186,2251,2253],{"class":2252},"sx1sP","repos",[186,2255,674],{"class":234},[186,2257,2258,2261,2264,2266],{"class":85,"line":260},[186,2259,2260],{"class":234},"  - ",[186,2262,2263],{"class":2252},"repo",[186,2265,609],{"class":234},[186,2267,2268],{"class":195},"https:\u002F\u002Fgithub.com\u002Fastral-sh\u002Fruff-pre-commit\n",[186,2270,2271,2274,2276],{"class":85,"line":271},[186,2272,2273],{"class":2252},"    rev",[186,2275,609],{"class":234},[186,2277,2278],{"class":195},"v0.16.8\n",[186,2280,2281,2284],{"class":85,"line":278},[186,2282,2283],{"class":2252},"    hooks",[186,2285,674],{"class":234},[186,2287,2288,2291,2294,2296],{"class":85,"line":296},[186,2289,2290],{"class":234},"      - ",[186,2292,2293],{"class":2252},"id",[186,2295,609],{"class":234},[186,2297,2298],{"class":195},"ruff\n",[186,2300,2301,2304,2306,2309],{"class":85,"line":358},[186,2302,2303],{"class":2252},"        args",[186,2305,2010],{"class":234},[186,2307,2308],{"class":195},"--fix",[186,2310,247],{"class":234},[186,2312,2313,2315,2317,2319],{"class":85,"line":363},[186,2314,2290],{"class":234},[186,2316,2293],{"class":2252},[186,2318,609],{"class":234},[186,2320,2321],{"class":195},"ruff-format\n",[186,2323,2324,2326,2328,2330],{"class":85,"line":377},[186,2325,2260],{"class":234},[186,2327,2263],{"class":2252},[186,2329,609],{"class":234},[186,2331,2332],{"class":195},"https:\u002F\u002Fgithub.com\u002Fpre-commit\u002Fmirrors-mypy\n",[186,2334,2335,2337,2339],{"class":85,"line":386},[186,2336,2273],{"class":2252},[186,2338,609],{"class":234},[186,2340,2341],{"class":195},"v2.3.1\n",[186,2343,2344,2346],{"class":85,"line":396},[186,2345,2283],{"class":2252},[186,2347,674],{"class":234},[186,2349,2350,2352,2354,2356],{"class":85,"line":405},[186,2351,2290],{"class":234},[186,2353,2293],{"class":2252},[186,2355,609],{"class":234},[186,2357,1028],{"class":195},[186,2359,2360,2363,2365,2367,2369,2371],{"class":85,"line":413},[186,2361,2362],{"class":2252},"        additional_dependencies",[186,2364,2010],{"class":234},[186,2366,213],{"class":195},[186,2368,305],{"class":234},[186,2370,217],{"class":195},[186,2372,247],{"class":234},[10,2374,2375,2376,2379,2380,305,2382,305,2385,214,2387,2389,2390,2394],{},"mypy in pre-commit runs in its own environment, so the stub packages must be listed in ",[14,2377,2378],{},"additional_dependencies",", or the hook checks against untyped pandas and passes code the command line would reject. In CI, run ",[14,2381,2181],{},[14,2383,2384],{},"ruff format --check",[14,2386,372],{},[14,2388,143],{}," as separate steps so a failure says which one broke; ",[19,2391,2393],{"href":2392},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-github-actions\u002F","Run a Python Excel Report in GitHub Actions"," shows a workflow to add them to.",[172,2396,2398],{"id":2397},"common-pitfalls","Common pitfalls",[2400,2401,2402,2412,2425,2435],"ul",{},[2122,2403,2404,2408,2409,2411],{},[2405,2406,2407],"strong",{},"Stubs out of step with the library."," Pin ",[14,2410,213],{}," to a release matching your pandas version, or mypy reports functions that do not exist or misses ones that do.",[2122,2413,2414,2420,2421,2424],{},[2405,2415,2416,2417,241],{},"Annotating everything as ",[14,2418,2419],{},"Any"," It silences the checker and removes its value. Annotate DataFrame parameters as ",[14,2422,2423],{},"pd.DataFrame"," and let the stubs do the rest.",[2122,2426,2427,2430,2431,241],{},[2405,2428,2429],{},"Types do not check columns."," mypy knows an object is a DataFrame, not which columns it has. Validate columns at the boundary, for example with ",[19,2432,2434],{"href":2433},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fvalidate-excel-data-with-pandera-schemas\u002F","Validate Excel Data with pandera Schemas",[2122,2436,2437,2440,2441,214,2443,2445],{},[2405,2438,2439],{},"Turning everything on at once in an old project."," Hundreds of findings get ignored. Start with the defaults plus ",[14,2442,424],{},[14,2444,428],{},", fix them, then add rule sets one at a time.",[172,2447,2449],{"id":2448},"conclusion","Conclusion",[10,2451,2452,2453,2455,2456,2458,2459,214,2461,2463,2464,2467,2468,2470,2471,2474,2475,2477,2478,2481],{},"Add ",[14,2454,244],{}," with the bugbear, pandas, timezone and pathlib rules, and ",[14,2457,372],{}," with ",[14,2460,213],{},[14,2462,217],{},". On a small report module they found a guaranteed crash from ",[14,2465,2466],{},"read_excel(sheet_name=None)",", unsafe uses of ",[14,2469,1162],{}," and cell values, a leaking mutable default and a bare ",[14,2472,2473],{},"except",". Narrow openpyxl's unions with ",[14,2476,1917],{},", work around stub gaps with equivalent code or ",[14,2479,2480],{},"cast",", and run both tools in the editor, in pre-commit and in CI.",[172,2483,2485],{"id":2484},"frequently-asked-questions","Frequently asked questions",[10,2487,2488,2491],{},[2405,2489,2490],{},"How do I type-check pandas and openpyxl code?","\nInstall mypy with pandas-stubs and types-openpyxl, then run mypy on your source folder. The stubs describe what each function returns, so mypy can tell, for example, that read_excel(..., sheet_name=None) returns a dict of DataFrames rather than a DataFrame.",[10,2493,2494,2497,2498,2500],{},[2405,2495,2496],{},"Why does mypy say wb.active might be None?","\nIn openpyxl's type stubs, Workbook.active can be a worksheet, a chartsheet or None. Narrow it with if not isinstance(ws, Worksheet) raise, and mypy then accepts ws.title, ws",[186,2499,716],{}," and ws.append.",[10,2502,2503,2506],{},[2405,2504,2505],{},"Why can't I add a cell value to a number without a type error?","\nA cell value can be a number, text, date, time, formula object, rich text or None. mypy lists every one of those when you add it to a float. Check isinstance(value, (int, float)) first, which also turns a text cell into a clear error instead of a TypeError.",[10,2508,2509,2512],{},[2405,2510,2511],{},"Which ruff rules are useful for pandas report scripts?","\nBeyond the defaults, B (bugbear) catches mutable default arguments, PD flags inplace=True and other pandas pitfalls, DTZ flags timezone-naive datetime.now(), PTH suggests pathlib, and I sorts imports. They ran in about 8 milliseconds on the example.",[172,2514,2516],{"id":2515},"related","Related",[2400,2518,2519,2525,2532,2539],{},[2122,2520,2521,2522,2524],{},"Up one level: ",[19,2523,22],{"href":21}," — making report code dependable.",[2122,2526,2527,2531],{},[19,2528,2530],{"href":2529},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — the tests that run next to these checks.",[2122,2533,2534,2538],{},[19,2535,2537],{"href":2536},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002F","Build a Command-Line Tool for Excel Reports with argparse"," — typed arguments at the entry point.",[2122,2540,2541,2545],{},[19,2542,2544],{"href":2543},"\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"," — validated settings instead of constants.",[2547,2548,2549],"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-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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}",{"title":182,"searchDepth":250,"depth":250,"links":2551},[2552,2553,2554,2555,2556,2557,2558,2559,2560],{"id":174,"depth":250,"text":175},{"id":444,"depth":250,"text":445},{"id":1248,"depth":250,"text":1249},{"id":2101,"depth":250,"text":2102},{"id":2227,"depth":250,"text":2228},{"id":2397,"depth":250,"text":2398},{"id":2448,"depth":250,"text":2449},{"id":2484,"depth":250,"text":2485},{"id":2515,"depth":250,"text":2516},"2026-09-23","Catch Excel-script bugs before they run — ruff for the mistakes that hide in report code (mutable defaults, inplace pandas calls, bare except, naive timestamps) and mypy with pandas-stubs and types-openpyxl for the ones that crash at runtime, such as read_excel returning a dict, wb.active possibly being None and cell values of mixed type. Every message shown was produced by the real tools.","md",[2565,2567,2569,2571],{"q":2490,"a":2566},"Install mypy with pandas-stubs and types-openpyxl, then run mypy on your source folder. The stubs describe what each function returns, so mypy can tell, for example, that read_excel(..., sheet_name=None) returns a dict of DataFrames rather than a DataFrame.",{"q":2496,"a":2568},"In openpyxl's type stubs, Workbook.active can be a worksheet, a chartsheet or None. Narrow it with if not isinstance(ws, Worksheet) raise, and mypy then accepts ws.title, ws[\"A1\"] and ws.append.",{"q":2505,"a":2570},"A cell value can be a number, text, date, time, formula object, rich text or None. mypy lists every one of those when you add it to a float. Check isinstance(value, (int, float)) first, which also turns a text cell into a clear error instead of a TypeError.",{"q":2511,"a":2572},"Beyond the defaults, B (bugbear) catches mutable default arguments, PD flags inplace=True and other pandas pitfalls, DTZ flags timezone-naive datetime.now(), PTH suggests pathlib, and I sorts imports. They ran in about 8 milliseconds on the example.","lint and type-check an excel automation project",null,{"breadcrumb":2576},[2577,2580,2583],{"name":2578,"item":2579},"Home","\u002F",{"name":2581,"item":2582},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project",{"title":2586,"description":2587},"Lint and Type-Check a Python Excel Project (ruff, mypy, pandas-stubs)","Set up ruff and mypy for pandas and openpyxl code — pyproject configuration, pandas-stubs and types-openpyxl, the real errors they report on a report script, narrowing wb.active and cell values with isinstance, stub imprecisions and how to work around them, timings, pre-commit and CI.","type-check-an-excel-automation-project","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project\u002Findex",[2591],"reliable-jobs","how-to","kjDp5FD7kiZBq28A1S5KPK2v3xyYbzRDsXxtFt6VraM",[2595,2599],{"title":2596,"path":2597,"stem":2598,"children":-1},"Test win32com and xlwings Code Without Excel","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel\u002Findex",{"title":2600,"path":2601,"stem":2602,"children":-1},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python","formatting-and-charting-excel-reports-with-python\u002Findex",[2604,2606],{"path":2605,"title":2581},"\u002Fautomating-reporting-workflows",{"path":2607,"title":22},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2610,2614,2617,2620,2623,2626],{"path":2611,"title":2612,"kind":2613},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":2615,"title":2616,"kind":2592},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":2618,"title":2619,"kind":2592},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":2621,"title":2622,"kind":2592},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","Log Python Excel Script Output to a File",{"path":2624,"title":2625,"kind":2592},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":2627,"title":2628,"kind":2592},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972143010]