[{"data":1,"prerenderedAt":1947},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest":1938},{"id":4,"title":5,"body":6,"dateModified":1908,"datePublished":1908,"description":1909,"extension":1910,"faq":1911,"meta":1920,"navigation":266,"path":1930,"seo":1931,"slug":1934,"stem":1935,"type":1936,"__hash__":1937},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002Findex.md","Test Excel Output with pytest",{"type":7,"value":8,"toc":1895},"minimark",[9,13,22,157,162,193,216,220,223,540,546,550,553,837,852,856,862,1077,1087,1106,1232,1236,1247,1367,1435,1442,1446,1454,1603,1610,1614,1753,1757,1764,1775,1779,1788,1792,1802,1815,1831,1837,1841,1844,1853,1856,1891],[10,11,12],"p",{},"A report script that has never been tested fails in the same three ways: a sheet gets renamed, a header row shifts because someone added a title, or a filter quietly drops every row and writes an empty summary. All three are cheap to catch, because the workbook the script saved is a file you can reopen and interrogate — no Excel installation, no screenshots, no manual check.",[10,14,15,16,21],{},"This guide is the testing half of ",[17,18,20],"a",{"href":19},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",". It builds fixtures inside the test, asserts on the saved file, and covers the two things that make Excel tests annoying if you meet them by surprise: floating-point totals and formatting assertions.",[23,24,32,33,32,37,32,41,32,48,32,55,32,65,32,71,32,76,32,81,32,86,32,89,32,94,32,99,32,102,32,108,32,113,32,116,32,123,32,128,32,131,32,134,32,138,32,141,32,147,32,152],"svg",{"viewBox":25,"role":26,"ariaLabelledBy":27,"xmlns":30,"style":31},"0 0 740 232","img",[28,29],"pt-loop-t","pt-loop-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  ",[34,35,36],"title",{"id":28},"The shape of an Excel output test: build, run, reopen, assert",[38,39,40],"desc",{"id":29},"The test writes a small input workbook into pytest's temporary directory, calls the report function, reopens the workbook the function saved with openpyxl, and asserts on its sheet names, header row, row count and totals. Nothing outside the temporary directory is touched.",[42,43],"rect",{"x":44,"y":44,"width":45,"height":46,"fill":47},"0","740","232","#ffffff",[49,50,54],"text",{"x":51,"y":52,"style":53},"370","32","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","One test, four steps, entirely inside tmp_path",[42,56],{"x":57,"y":58,"width":59,"height":60,"rx":61,"fill":62,"stroke":63,"style":64},"16","56","156","70","12","#f0f2f5","var(--line,#cdd5e6)","stroke-width:2px",[49,66,70],{"x":67,"y":68,"style":69},"94","84","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. build input",[49,72,75],{"x":67,"y":73,"style":74},"106","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","five rows of pandas",[42,77],{"x":78,"y":58,"width":59,"height":60,"rx":61,"fill":79,"stroke":80,"style":64},"196","#ebebfd","var(--brand,#5b5cf0)",[49,82,85],{"x":83,"y":68,"style":84},"274","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","2. run the job",[49,87,88],{"x":83,"y":73,"style":74},"build_report(src, dst)",[42,90],{"x":91,"y":58,"width":59,"height":60,"rx":61,"fill":92,"stroke":93,"style":64},"376","#fdefd8","var(--gold,#b4740a)",[49,95,98],{"x":96,"y":68,"style":97},"454","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","3. reopen",[49,100,101],{"x":96,"y":73,"style":74},"load_workbook(dst)",[42,103],{"x":104,"y":58,"width":105,"height":60,"rx":61,"fill":106,"stroke":107,"style":64},"556","168","#d9f4f1","var(--teal,#0f9488)",[49,109,112],{"x":110,"y":68,"style":111},"640","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","4. assert",[49,114,115],{"x":110,"y":73,"style":74},"sheets · headers · totals",[117,118],"line",{"x1":119,"y1":120,"x2":121,"y2":120,"stroke":122,"style":64},"176","91","190","var(--muted,#5b6780)",[124,125],"polygon",{"points":126,"fill":127},"194,91 184,86 184,96","#5b6780",[117,129],{"x1":130,"y1":120,"x2":51,"y2":120,"stroke":122,"style":64},"356",[124,132],{"points":133,"fill":127},"374,91 364,86 364,96",[117,135],{"x1":136,"y1":120,"x2":137,"y2":120,"stroke":122,"style":64},"536","550",[124,139],{"points":140,"fill":127},"554,91 544,86 544,96",[42,142],{"x":57,"y":143,"width":144,"height":145,"rx":61,"fill":146,"stroke":80},"150","708","62","#f0f4ff",[49,148,151],{"x":149,"y":119,"style":150},"40","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca)","Why reopen instead of trusting the DataFrame you passed in?",[49,153,156],{"x":149,"y":154,"style":155},"199","font-size:11.5px;fill:var(--text,#172033)","Because the bugs live in the writing: a sheet renamed, index=True adding a column, a header row pushed down by a title.",[158,159,161],"h2",{"id":160},"prerequisites","Prerequisites",[163,164,169],"pre",{"className":165,"code":166,"language":167,"meta":168,"style":168},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pytest pandas openpyxl\n","bash","",[170,171,172],"code",{"__ignoreMap":168},[173,174,176,180,184,187,190],"span",{"class":117,"line":175},1,[173,177,179],{"class":178},"sMTad","pip",[173,181,183],{"class":182},"srMev"," install",[173,185,186],{"class":182}," pytest",[173,188,189],{"class":182}," pandas",[173,191,192],{"class":182}," openpyxl\n",[10,194,195,196,199,200,203,204,207,208,211,212,215],{},"You also need a report script with the transform separated from the file handling — the ",[170,197,198],{},"summarise()"," \u002F ",[170,201,202],{},"build_report()"," split described in the ",[17,205,206],{"href":19},"parent guide",". Everything below assumes a ",[170,209,210],{},"report.py"," at the project root and tests in ",[170,213,214],{},"tests\u002F",".",[158,217,219],{"id":218},"step-1-generate-the-input-inside-the-test","Step 1: Generate the input inside the test",[10,221,222],{},"Resist the urge to check a sample workbook into the repository. A fixture built in code is readable, contains no real customer data, and can be varied per test:",[163,224,228],{"className":225,"code":226,"language":227,"meta":168,"style":168},"language-python shiki shiki-themes github-light github-dark-high-contrast","# tests\u002Fconftest.py\nimport pandas as pd\nimport pytest\n\n\n@pytest.fixture\ndef orders_frame():\n    return pd.DataFrame({\n        \"Order_ID\": [1, 2, 3, 4, 5],\n        \"Region\": [\"North\", \"South\", \"North\", \"East\", \"South\"],\n        \"Amount\": [100.0, 250.5, 50.25, 75.0, 24.25],\n        \"Date\": pd.to_datetime(\n            [\"2026-07-01\", \"2026-07-02\", \"2026-07-02\", \"2026-07-05\", \"2026-07-09\"]\n        ),\n    })\n\n\n@pytest.fixture\ndef orders_file(orders_frame, tmp_path):\n    \"\"\"The same frame, saved as a real .xlsx the code under test can read.\"\"\"\n    path = tmp_path \u002F \"orders.xlsx\"\n    orders_frame.to_excel(path, sheet_name=\"Orders\", index=False)\n    return path\n","python",[170,229,230,236,253,261,268,273,280,292,301,338,369,402,411,442,448,454,459,464,469,480,486,504,532],{"__ignoreMap":168},[173,231,232],{"class":117,"line":175},[173,233,235],{"class":234},"s-wDw","# tests\u002Fconftest.py\n",[173,237,239,243,247,250],{"class":117,"line":238},2,[173,240,242],{"class":241},"s-kum","import",[173,244,246],{"class":245},"skGVy"," pandas ",[173,248,249],{"class":241},"as",[173,251,252],{"class":245}," pd\n",[173,254,256,258],{"class":117,"line":255},3,[173,257,242],{"class":241},[173,259,260],{"class":245}," pytest\n",[173,262,264],{"class":117,"line":263},4,[173,265,267],{"emptyLinePlaceholder":266},true,"\n",[173,269,271],{"class":117,"line":270},5,[173,272,267],{"emptyLinePlaceholder":266},[173,274,276],{"class":117,"line":275},6,[173,277,279],{"class":278},"s_Opv","@pytest.fixture\n",[173,281,283,286,289],{"class":117,"line":282},7,[173,284,285],{"class":241},"def",[173,287,288],{"class":278}," orders_frame",[173,290,291],{"class":245},"():\n",[173,293,295,298],{"class":117,"line":294},8,[173,296,297],{"class":241},"    return",[173,299,300],{"class":245}," pd.DataFrame({\n",[173,302,304,307,310,314,317,320,322,325,327,330,332,335],{"class":117,"line":303},9,[173,305,306],{"class":182},"        \"Order_ID\"",[173,308,309],{"class":245},": [",[173,311,313],{"class":312},"sP0c6","1",[173,315,316],{"class":245},", ",[173,318,319],{"class":312},"2",[173,321,316],{"class":245},[173,323,324],{"class":312},"3",[173,326,316],{"class":245},[173,328,329],{"class":312},"4",[173,331,316],{"class":245},[173,333,334],{"class":312},"5",[173,336,337],{"class":245},"],\n",[173,339,341,344,346,349,351,354,356,358,360,363,365,367],{"class":117,"line":340},10,[173,342,343],{"class":182},"        \"Region\"",[173,345,309],{"class":245},[173,347,348],{"class":182},"\"North\"",[173,350,316],{"class":245},[173,352,353],{"class":182},"\"South\"",[173,355,316],{"class":245},[173,357,348],{"class":182},[173,359,316],{"class":245},[173,361,362],{"class":182},"\"East\"",[173,364,316],{"class":245},[173,366,353],{"class":182},[173,368,337],{"class":245},[173,370,372,375,377,380,382,385,387,390,392,395,397,400],{"class":117,"line":371},11,[173,373,374],{"class":182},"        \"Amount\"",[173,376,309],{"class":245},[173,378,379],{"class":312},"100.0",[173,381,316],{"class":245},[173,383,384],{"class":312},"250.5",[173,386,316],{"class":245},[173,388,389],{"class":312},"50.25",[173,391,316],{"class":245},[173,393,394],{"class":312},"75.0",[173,396,316],{"class":245},[173,398,399],{"class":312},"24.25",[173,401,337],{"class":245},[173,403,405,408],{"class":117,"line":404},12,[173,406,407],{"class":182},"        \"Date\"",[173,409,410],{"class":245},": pd.to_datetime(\n",[173,412,414,417,420,422,425,427,429,431,434,436,439],{"class":117,"line":413},13,[173,415,416],{"class":245},"            [",[173,418,419],{"class":182},"\"2026-07-01\"",[173,421,316],{"class":245},[173,423,424],{"class":182},"\"2026-07-02\"",[173,426,316],{"class":245},[173,428,424],{"class":182},[173,430,316],{"class":245},[173,432,433],{"class":182},"\"2026-07-05\"",[173,435,316],{"class":245},[173,437,438],{"class":182},"\"2026-07-09\"",[173,440,441],{"class":245},"]\n",[173,443,445],{"class":117,"line":444},14,[173,446,447],{"class":245},"        ),\n",[173,449,451],{"class":117,"line":450},15,[173,452,453],{"class":245},"    })\n",[173,455,457],{"class":117,"line":456},16,[173,458,267],{"emptyLinePlaceholder":266},[173,460,462],{"class":117,"line":461},17,[173,463,267],{"emptyLinePlaceholder":266},[173,465,467],{"class":117,"line":466},18,[173,468,279],{"class":278},[173,470,472,474,477],{"class":117,"line":471},19,[173,473,285],{"class":241},[173,475,476],{"class":278}," orders_file",[173,478,479],{"class":245},"(orders_frame, tmp_path):\n",[173,481,483],{"class":117,"line":482},20,[173,484,485],{"class":182},"    \"\"\"The same frame, saved as a real .xlsx the code under test can read.\"\"\"\n",[173,487,489,492,495,498,501],{"class":117,"line":488},21,[173,490,491],{"class":245},"    path ",[173,493,494],{"class":241},"=",[173,496,497],{"class":245}," tmp_path ",[173,499,500],{"class":241},"\u002F",[173,502,503],{"class":182}," \"orders.xlsx\"\n",[173,505,507,510,514,516,519,521,524,526,529],{"class":117,"line":506},22,[173,508,509],{"class":245},"    orders_frame.to_excel(path, ",[173,511,513],{"class":512},"sa561","sheet_name",[173,515,494],{"class":241},[173,517,518],{"class":182},"\"Orders\"",[173,520,316],{"class":245},[173,522,523],{"class":512},"index",[173,525,494],{"class":241},[173,527,528],{"class":312},"False",[173,530,531],{"class":245},")\n",[173,533,535,537],{"class":117,"line":534},23,[173,536,297],{"class":241},[173,538,539],{"class":245}," path\n",[10,541,542,545],{},[170,543,544],{},"tmp_path"," is a per-test directory that pytest creates and cleans up, so tests never collide and never write into your working tree. Two fixtures rather than one is deliberate: transform tests take the frame and skip the file entirely, and only the tests that exercise reading need the workbook.",[158,547,549],{"id":548},"step-2-assert-on-the-saved-workbook","Step 2: Assert on the saved workbook",[10,551,552],{},"Reopen the file the code wrote and check the things a reader would notice first:",[163,554,556],{"className":225,"code":555,"language":227,"meta":168,"style":168},"# tests\u002Ftest_report.py\nfrom openpyxl import load_workbook\n\nfrom report import build_report\n\n\ndef test_report_structure(orders_file, tmp_path):\n    target = build_report(orders_file, tmp_path \u002F \"report.xlsx\")\n\n    wb = load_workbook(target)\n    assert wb.sheetnames == [\"Summary\", \"Detail\"]\n\n    ws = wb[\"Summary\"]\n    header = [cell.value for cell in ws[1]]\n    assert header == [\"Region\", \"Amount\", \"Share\"]\n\n    regions = [ws.cell(row=r, column=1).value for r in range(2, ws.max_row + 1)]\n    assert regions == [\"South\", \"North\", \"East\"]     # sorted by amount, descending\n    assert ws.max_row == 4                            # header + three regions\n",[170,557,558,563,576,580,592,596,600,610,627,631,641,665,669,683,710,736,740,795,822],{"__ignoreMap":168},[173,559,560],{"class":117,"line":175},[173,561,562],{"class":234},"# tests\u002Ftest_report.py\n",[173,564,565,568,571,573],{"class":117,"line":238},[173,566,567],{"class":241},"from",[173,569,570],{"class":245}," openpyxl ",[173,572,242],{"class":241},[173,574,575],{"class":245}," load_workbook\n",[173,577,578],{"class":117,"line":255},[173,579,267],{"emptyLinePlaceholder":266},[173,581,582,584,587,589],{"class":117,"line":263},[173,583,567],{"class":241},[173,585,586],{"class":245}," report ",[173,588,242],{"class":241},[173,590,591],{"class":245}," build_report\n",[173,593,594],{"class":117,"line":270},[173,595,267],{"emptyLinePlaceholder":266},[173,597,598],{"class":117,"line":275},[173,599,267],{"emptyLinePlaceholder":266},[173,601,602,604,607],{"class":117,"line":282},[173,603,285],{"class":241},[173,605,606],{"class":278}," test_report_structure",[173,608,609],{"class":245},"(orders_file, tmp_path):\n",[173,611,612,615,617,620,622,625],{"class":117,"line":294},[173,613,614],{"class":245},"    target ",[173,616,494],{"class":241},[173,618,619],{"class":245}," build_report(orders_file, tmp_path ",[173,621,500],{"class":241},[173,623,624],{"class":182}," \"report.xlsx\"",[173,626,531],{"class":245},[173,628,629],{"class":117,"line":303},[173,630,267],{"emptyLinePlaceholder":266},[173,632,633,636,638],{"class":117,"line":340},[173,634,635],{"class":245},"    wb ",[173,637,494],{"class":241},[173,639,640],{"class":245}," load_workbook(target)\n",[173,642,643,646,649,652,655,658,660,663],{"class":117,"line":371},[173,644,645],{"class":241},"    assert",[173,647,648],{"class":245}," wb.sheetnames ",[173,650,651],{"class":241},"==",[173,653,654],{"class":245}," [",[173,656,657],{"class":182},"\"Summary\"",[173,659,316],{"class":245},[173,661,662],{"class":182},"\"Detail\"",[173,664,441],{"class":245},[173,666,667],{"class":117,"line":404},[173,668,267],{"emptyLinePlaceholder":266},[173,670,671,674,676,679,681],{"class":117,"line":413},[173,672,673],{"class":245},"    ws ",[173,675,494],{"class":241},[173,677,678],{"class":245}," wb[",[173,680,657],{"class":182},[173,682,441],{"class":245},[173,684,685,688,690,693,696,699,702,705,707],{"class":117,"line":444},[173,686,687],{"class":245},"    header ",[173,689,494],{"class":241},[173,691,692],{"class":245}," [cell.value ",[173,694,695],{"class":241},"for",[173,697,698],{"class":245}," cell ",[173,700,701],{"class":241},"in",[173,703,704],{"class":245}," ws[",[173,706,313],{"class":312},[173,708,709],{"class":245},"]]\n",[173,711,712,714,717,719,721,724,726,729,731,734],{"class":117,"line":450},[173,713,645],{"class":241},[173,715,716],{"class":245}," header ",[173,718,651],{"class":241},[173,720,654],{"class":245},[173,722,723],{"class":182},"\"Region\"",[173,725,316],{"class":245},[173,727,728],{"class":182},"\"Amount\"",[173,730,316],{"class":245},[173,732,733],{"class":182},"\"Share\"",[173,735,441],{"class":245},[173,737,738],{"class":117,"line":456},[173,739,267],{"emptyLinePlaceholder":266},[173,741,742,745,747,750,753,755,758,761,763,765,768,770,773,775,778,781,783,786,789,792],{"class":117,"line":461},[173,743,744],{"class":245},"    regions ",[173,746,494],{"class":241},[173,748,749],{"class":245}," [ws.cell(",[173,751,752],{"class":512},"row",[173,754,494],{"class":241},[173,756,757],{"class":245},"r, ",[173,759,760],{"class":512},"column",[173,762,494],{"class":241},[173,764,313],{"class":312},[173,766,767],{"class":245},").value ",[173,769,695],{"class":241},[173,771,772],{"class":245}," r ",[173,774,701],{"class":241},[173,776,777],{"class":312}," range",[173,779,780],{"class":245},"(",[173,782,319],{"class":312},[173,784,785],{"class":245},", ws.max_row ",[173,787,788],{"class":241},"+",[173,790,791],{"class":312}," 1",[173,793,794],{"class":245},")]\n",[173,796,797,799,802,804,806,808,810,812,814,816,819],{"class":117,"line":466},[173,798,645],{"class":241},[173,800,801],{"class":245}," regions ",[173,803,651],{"class":241},[173,805,654],{"class":245},[173,807,353],{"class":182},[173,809,316],{"class":245},[173,811,348],{"class":182},[173,813,316],{"class":245},[173,815,362],{"class":182},[173,817,818],{"class":245},"]     ",[173,820,821],{"class":234},"# sorted by amount, descending\n",[173,823,824,826,829,831,834],{"class":117,"line":471},[173,825,645],{"class":241},[173,827,828],{"class":245}," ws.max_row ",[173,830,651],{"class":241},[173,832,833],{"class":312}," 4",[173,835,836],{"class":234},"                            # header + three regions\n",[10,838,839,840,843,844,847,848,851],{},"Four assertions, four real failure modes. ",[170,841,842],{},"sheetnames"," catches a renamed or missing tab. The header comparison catches ",[170,845,846],{},"index=False"," being dropped, which silently adds an unnamed first column and shifts everything right. The region list catches a broken sort or a filter that removed a group. ",[170,849,850],{},"max_row"," catches the empty-summary case, which is the failure that survives longest in production because an empty report still arrives on time and still looks like a report.",[158,853,855],{"id":854},"step-3-compare-numbers-without-false-failures","Step 3: Compare numbers without false failures",[10,857,858,859,861],{},"Excel stores numbers as IEEE doubles, and pandas sums in floating point, so an exact ",[170,860,651],{}," on a total is a test that fails for reasons that have nothing to do with your code:",[163,863,865],{"className":225,"code":864,"language":227,"meta":168,"style":168},"import pytest\n\n\ndef test_totals(orders_file, tmp_path):\n    target = build_report(orders_file, tmp_path \u002F \"report.xlsx\")\n    wb = load_workbook(target, data_only=True)\n    ws = wb[\"Summary\"]\n\n    totals = {ws.cell(row=r, column=1).value: ws.cell(row=r, column=2).value\n              for r in range(2, ws.max_row + 1)}\n\n    assert totals[\"North\"] == pytest.approx(150.25)\n    assert totals[\"South\"] == pytest.approx(274.75)\n    assert sum(totals.values()) == pytest.approx(500.0, abs=0.01)\n",[170,866,867,873,877,881,890,904,923,935,939,979,1003,1007,1029,1048],{"__ignoreMap":168},[173,868,869,871],{"class":117,"line":175},[173,870,242],{"class":241},[173,872,260],{"class":245},[173,874,875],{"class":117,"line":238},[173,876,267],{"emptyLinePlaceholder":266},[173,878,879],{"class":117,"line":255},[173,880,267],{"emptyLinePlaceholder":266},[173,882,883,885,888],{"class":117,"line":263},[173,884,285],{"class":241},[173,886,887],{"class":278}," test_totals",[173,889,609],{"class":245},[173,891,892,894,896,898,900,902],{"class":117,"line":270},[173,893,614],{"class":245},[173,895,494],{"class":241},[173,897,619],{"class":245},[173,899,500],{"class":241},[173,901,624],{"class":182},[173,903,531],{"class":245},[173,905,906,908,910,913,916,918,921],{"class":117,"line":275},[173,907,635],{"class":245},[173,909,494],{"class":241},[173,911,912],{"class":245}," load_workbook(target, ",[173,914,915],{"class":512},"data_only",[173,917,494],{"class":241},[173,919,920],{"class":312},"True",[173,922,531],{"class":245},[173,924,925,927,929,931,933],{"class":117,"line":282},[173,926,673],{"class":245},[173,928,494],{"class":241},[173,930,678],{"class":245},[173,932,657],{"class":182},[173,934,441],{"class":245},[173,936,937],{"class":117,"line":294},[173,938,267],{"emptyLinePlaceholder":266},[173,940,941,944,946,949,951,953,955,957,959,961,964,966,968,970,972,974,976],{"class":117,"line":303},[173,942,943],{"class":245},"    totals ",[173,945,494],{"class":241},[173,947,948],{"class":245}," {ws.cell(",[173,950,752],{"class":512},[173,952,494],{"class":241},[173,954,757],{"class":245},[173,956,760],{"class":512},[173,958,494],{"class":241},[173,960,313],{"class":312},[173,962,963],{"class":245},").value: ws.cell(",[173,965,752],{"class":512},[173,967,494],{"class":241},[173,969,757],{"class":245},[173,971,760],{"class":512},[173,973,494],{"class":241},[173,975,319],{"class":312},[173,977,978],{"class":245},").value\n",[173,980,981,984,986,988,990,992,994,996,998,1000],{"class":117,"line":340},[173,982,983],{"class":241},"              for",[173,985,772],{"class":245},[173,987,701],{"class":241},[173,989,777],{"class":312},[173,991,780],{"class":245},[173,993,319],{"class":312},[173,995,785],{"class":245},[173,997,788],{"class":241},[173,999,791],{"class":312},[173,1001,1002],{"class":245},")}\n",[173,1004,1005],{"class":117,"line":371},[173,1006,267],{"emptyLinePlaceholder":266},[173,1008,1009,1011,1014,1016,1019,1021,1024,1027],{"class":117,"line":404},[173,1010,645],{"class":241},[173,1012,1013],{"class":245}," totals[",[173,1015,348],{"class":182},[173,1017,1018],{"class":245},"] ",[173,1020,651],{"class":241},[173,1022,1023],{"class":245}," pytest.approx(",[173,1025,1026],{"class":312},"150.25",[173,1028,531],{"class":245},[173,1030,1031,1033,1035,1037,1039,1041,1043,1046],{"class":117,"line":413},[173,1032,645],{"class":241},[173,1034,1013],{"class":245},[173,1036,353],{"class":182},[173,1038,1018],{"class":245},[173,1040,651],{"class":241},[173,1042,1023],{"class":245},[173,1044,1045],{"class":312},"274.75",[173,1047,531],{"class":245},[173,1049,1050,1052,1055,1058,1060,1062,1065,1067,1070,1072,1075],{"class":117,"line":444},[173,1051,645],{"class":241},[173,1053,1054],{"class":312}," sum",[173,1056,1057],{"class":245},"(totals.values()) ",[173,1059,651],{"class":241},[173,1061,1023],{"class":245},[173,1063,1064],{"class":312},"500.0",[173,1066,316],{"class":245},[173,1068,1069],{"class":512},"abs",[173,1071,494],{"class":241},[173,1073,1074],{"class":312},"0.01",[173,1076,531],{"class":245},[10,1078,1079,1082,1083,1086],{},[170,1080,1081],{},"pytest.approx"," handles the ordinary case. The ",[170,1084,1085],{},"abs=0.01"," on the grand total is a different decision: for money, an absolute tolerance of one cent expresses the real requirement better than a relative one, because it stays meaningful whether the total is 500 or 500,000.",[10,1088,1089,1092,1093,1096,1097,1100,1101,1105],{},[170,1090,1091],{},"data_only=True"," matters if the report writes formulas. openpyxl does not evaluate them — it returns the cached value Excel stored, or ",[170,1094,1095],{},"None"," if the file has never been opened in Excel. If your report writes ",[170,1098,1099],{},"=SUM(B2:B4)"," rather than a computed number, assert on the formula string instead, and do the arithmetic check on the DataFrame the transform returned. ",[17,1102,1104],{"href":1103},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fread-formula-results-with-openpyxl-data-only\u002F","Read Formula Results with openpyxl data_only"," explains the caching rules that make this behave the way it does.",[23,1107,32,1112,32,1115,32,1118,32,1121,32,1127,32,1131,32,1137,32,1140,32,1144,32,1147,32,1151,32,1154,32,1158,32,1161,32,1165,32,1171,32,1176,32,1180,32,1184,32,1186,32,1191,32,1194,32,1198,32,1201,32,1203,32,1208,32,1211,32,1214,32,1217,32,1219,32,1223,32,1226,32,1229],{"viewBox":1108,"role":26,"ariaLabelledBy":1109,"xmlns":30,"style":31},"0 0 740 250",[1110,1111],"pt-float-t","pt-float-d",[34,1113,1114],{"id":1110},"Choosing an assertion for a numeric cell",[38,1116,1117],{"id":1111},"If the cell holds a formula, assert on the formula string and check the arithmetic on the DataFrame instead. If it holds money, use an absolute tolerance of one cent. If it holds any other computed float, use pytest.approx with its default relative tolerance. Only integers and identifiers are safe to compare exactly.",[42,1119],{"x":44,"y":44,"width":45,"height":1120,"fill":47},"250",[42,1122],{"x":1123,"y":1124,"width":1125,"height":1126,"rx":61,"fill":79,"stroke":80,"style":64},"228","20","284","46",[49,1128,1130],{"x":51,"y":1129,"style":84},"49","What is in the cell?",[117,1132],{"x1":1133,"y1":1134,"x2":1135,"y2":73,"stroke":122,"style":1136},"290","68","120","stroke-width:1.5px",[124,1138],{"points":1139,"fill":127},"116,108 128,102 126,112",[117,1141],{"x1":1142,"y1":1134,"x2":1143,"y2":73,"stroke":122,"style":1136},"340","300",[124,1145],{"points":1146,"fill":127},"297,108 308,104 305,114",[117,1148],{"x1":1149,"y1":1134,"x2":1150,"y2":73,"stroke":122,"style":1136},"400","440",[124,1152],{"points":1153,"fill":127},"443,108 432,104 435,114",[117,1155],{"x1":1156,"y1":1134,"x2":1157,"y2":73,"stroke":122,"style":1136},"450","620",[124,1159],{"points":1160,"fill":127},"624,108 612,102 614,112",[42,1162],{"x":57,"y":1163,"width":1164,"height":1163,"rx":61,"fill":92,"stroke":93,"style":64},"112","164",[49,1166,1170],{"x":1167,"y":1168,"style":1169},"98","140","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","a formula",[49,1172,1175],{"x":1167,"y":1173,"style":1174},"166","font-size:11px;fill:var(--text,#172033);text-anchor:middle","assert the string",[49,1177,1179],{"x":1167,"y":1178,"style":1174},"186","\"=SUM(B2:B4)\"",[49,1181,1183],{"x":1167,"y":1182,"style":74},"210","check maths upstream",[42,1185],{"x":78,"y":1163,"width":1164,"height":1163,"rx":61,"fill":106,"stroke":107,"style":64},[49,1187,1190],{"x":1188,"y":1168,"style":1189},"278","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","money",[49,1192,1193],{"x":1188,"y":1173,"style":1174},"approx(x, abs=0.01)",[49,1195,1197],{"x":1188,"y":1196,"style":74},"192","one cent, at any",[49,1199,1200],{"x":1188,"y":1182,"style":74},"order of magnitude",[42,1202],{"x":91,"y":1163,"width":1164,"height":1163,"rx":61,"fill":146,"stroke":80,"style":64},[49,1204,1207],{"x":1205,"y":1168,"style":1206},"458","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","another float",[49,1209,1210],{"x":1205,"y":1173,"style":1174},"pytest.approx(x)",[49,1212,1213],{"x":1205,"y":1196,"style":74},"relative tolerance",[49,1215,1216],{"x":1205,"y":1182,"style":74},"of one in a million",[42,1218],{"x":104,"y":1163,"width":105,"height":1163,"rx":61,"fill":62,"stroke":63,"style":64},[49,1220,1222],{"x":110,"y":1168,"style":1221},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","a count or an id",[49,1224,1225],{"x":110,"y":1173,"style":1174},"plain ==",[49,1227,1228],{"x":110,"y":1196,"style":74},"integers are exact,",[49,1230,1231],{"x":110,"y":1182,"style":74},"so demand exactness",[158,1233,1235],{"id":1234},"step-4-test-the-formatting-that-carries-meaning","Step 4: Test the formatting that carries meaning",[10,1237,1238,1239,1242,1243,1246],{},"Not all formatting is worth a test. The number format on a currency column is, because a report showing ",[170,1240,1241],{},"1234.5"," where the finance team expects ",[170,1244,1245],{},"£1,234.50"," is wrong in the way that gets it sent back. openpyxl exposes every style attribute it wrote, so the assertion is direct:",[163,1248,1250],{"className":225,"code":1249,"language":227,"meta":168,"style":168},"def test_currency_column_is_formatted(orders_file, tmp_path):\n    target = build_report(orders_file, tmp_path \u002F \"report.xlsx\")\n    ws = load_workbook(target)[\"Summary\"]\n\n    assert ws[\"A1\"].font.bold is True                 # header stands out\n    assert ws[\"B2\"].number_format == '\"£\"#,##0.00'    # money reads as money\n    assert ws[\"C2\"].number_format == \"0.0%\"           # share reads as a percentage\n    assert ws.freeze_panes == \"A2\"                    # header stays visible\n",[170,1251,1252,1261,1275,1288,1292,1313,1333,1352],{"__ignoreMap":168},[173,1253,1254,1256,1259],{"class":117,"line":175},[173,1255,285],{"class":241},[173,1257,1258],{"class":278}," test_currency_column_is_formatted",[173,1260,609],{"class":245},[173,1262,1263,1265,1267,1269,1271,1273],{"class":117,"line":238},[173,1264,614],{"class":245},[173,1266,494],{"class":241},[173,1268,619],{"class":245},[173,1270,500],{"class":241},[173,1272,624],{"class":182},[173,1274,531],{"class":245},[173,1276,1277,1279,1281,1284,1286],{"class":117,"line":255},[173,1278,673],{"class":245},[173,1280,494],{"class":241},[173,1282,1283],{"class":245}," load_workbook(target)[",[173,1285,657],{"class":182},[173,1287,441],{"class":245},[173,1289,1290],{"class":117,"line":263},[173,1291,267],{"emptyLinePlaceholder":266},[173,1293,1294,1296,1298,1301,1304,1307,1310],{"class":117,"line":270},[173,1295,645],{"class":241},[173,1297,704],{"class":245},[173,1299,1300],{"class":182},"\"A1\"",[173,1302,1303],{"class":245},"].font.bold ",[173,1305,1306],{"class":241},"is",[173,1308,1309],{"class":312}," True",[173,1311,1312],{"class":234},"                 # header stands out\n",[173,1314,1315,1317,1319,1322,1325,1327,1330],{"class":117,"line":275},[173,1316,645],{"class":241},[173,1318,704],{"class":245},[173,1320,1321],{"class":182},"\"B2\"",[173,1323,1324],{"class":245},"].number_format ",[173,1326,651],{"class":241},[173,1328,1329],{"class":182}," '\"£\"#,##0.00'",[173,1331,1332],{"class":234},"    # money reads as money\n",[173,1334,1335,1337,1339,1342,1344,1346,1349],{"class":117,"line":282},[173,1336,645],{"class":241},[173,1338,704],{"class":245},[173,1340,1341],{"class":182},"\"C2\"",[173,1343,1324],{"class":245},[173,1345,651],{"class":241},[173,1347,1348],{"class":182}," \"0.0%\"",[173,1350,1351],{"class":234},"           # share reads as a percentage\n",[173,1353,1354,1356,1359,1361,1364],{"class":117,"line":294},[173,1355,645],{"class":241},[173,1357,1358],{"class":245}," ws.freeze_panes ",[173,1360,651],{"class":241},[173,1362,1363],{"class":182}," \"A2\"",[173,1365,1366],{"class":234},"                    # header stays visible\n",[23,1368,32,1373,32,1376,32,1379,32,1382,32,1388,32,1392,32,1397,32,1401,32,1405,32,1409,32,1412,32,1415,32,1419,32,1423,32,1426,32,1429,32,1432],{"viewBox":1369,"role":26,"ariaLabelledBy":1370,"xmlns":30,"style":31},"0 0 740 226",[1371,1372],"pt-fmt-t","pt-fmt-d",[34,1374,1375],{"id":1371},"Which formatting is worth an assertion",[38,1377,1378],{"id":1372},"Assert the formatting that changes what a number means — the currency code, the percentage code, a date code, the frozen header. Do not assert shades, column widths or fill colours: they change for good reasons, and a suite that fails on a palette tweak is a suite people learn to ignore.",[42,1380],{"x":44,"y":44,"width":45,"height":1381,"fill":47},"226",[42,1383],{"x":57,"y":1384,"width":1385,"height":1386,"rx":1387,"fill":106,"stroke":107,"style":64},"34","344","172","14",[49,1389,1391],{"x":1390,"y":145,"style":1189},"188","assert these — they carry meaning",[49,1393,1396],{"x":1390,"y":1394,"style":1395},"92","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","number_format on the money column",[49,1398,1400],{"x":1390,"y":1399,"style":1395},"114","the percentage code on a share",[49,1402,1404],{"x":1390,"y":1403,"style":1395},"136","a date code, not a serial number",[49,1406,1408],{"x":1390,"y":1407,"style":1395},"158","freeze_panes on the header row",[49,1410,1411],{"x":1390,"y":1178,"style":74},"wrong here and the report is wrong",[42,1413],{"x":1414,"y":1384,"width":1385,"height":1386,"rx":1387,"fill":62,"stroke":63,"style":64},"380",[49,1416,1418],{"x":1417,"y":145,"style":1221},"552","leave these alone — they are taste",[49,1420,1422],{"x":1417,"y":1394,"style":1421},"font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","the exact header fill colour",[49,1424,1425],{"x":1417,"y":1399,"style":1421},"column widths in character units",[49,1427,1428],{"x":1417,"y":1403,"style":1421},"font family and point size",[49,1430,1431],{"x":1417,"y":1407,"style":1421},"banding and border weights",[49,1433,1434],{"x":1417,"y":1178,"style":74},"a suite that fails on a shade gets ignored",[10,1436,1437,1438,215],{},"Keep these assertions to the handful that change the meaning of the page. Testing every fill colour produces a suite that fails whenever someone adjusts a shade, which trains people to ignore it. The rules behind these formats are in ",[17,1439,1441],{"href":1440},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fapplying-number-and-date-formats-in-excel\u002F","Applying Number and Date Formats in Excel",[158,1443,1445],{"id":1444},"step-5-test-the-failure-path-too","Step 5: Test the failure path too",[10,1447,1448,1449,1453],{},"The most valuable test in a report suite is often the one asserting that bad input is ",[1450,1451,1452],"em",{},"rejected",", because a job that writes a plausible-looking report from broken input is worse than a job that stops:",[163,1455,1457],{"className":225,"code":1456,"language":227,"meta":168,"style":168},"import pytest\n\n\ndef test_missing_column_is_reported_clearly(orders_frame, tmp_path):\n    broken = orders_frame.drop(columns=[\"Region\"])\n    source = tmp_path \u002F \"broken.xlsx\"\n    broken.to_excel(source, sheet_name=\"Orders\", index=False)\n\n    with pytest.raises(ValueError, match=\"Region\"):\n        build_report(source, tmp_path \u002F \"out.xlsx\")\n\n    assert not (tmp_path \u002F \"out.xlsx\").exists()       # nothing half-written\n",[170,1458,1459,1465,1469,1473,1482,1505,1519,1540,1544,1567,1579,1583],{"__ignoreMap":168},[173,1460,1461,1463],{"class":117,"line":175},[173,1462,242],{"class":241},[173,1464,260],{"class":245},[173,1466,1467],{"class":117,"line":238},[173,1468,267],{"emptyLinePlaceholder":266},[173,1470,1471],{"class":117,"line":255},[173,1472,267],{"emptyLinePlaceholder":266},[173,1474,1475,1477,1480],{"class":117,"line":263},[173,1476,285],{"class":241},[173,1478,1479],{"class":278}," test_missing_column_is_reported_clearly",[173,1481,479],{"class":245},[173,1483,1484,1487,1489,1492,1495,1497,1500,1502],{"class":117,"line":270},[173,1485,1486],{"class":245},"    broken ",[173,1488,494],{"class":241},[173,1490,1491],{"class":245}," orders_frame.drop(",[173,1493,1494],{"class":512},"columns",[173,1496,494],{"class":241},[173,1498,1499],{"class":245},"[",[173,1501,723],{"class":182},[173,1503,1504],{"class":245},"])\n",[173,1506,1507,1510,1512,1514,1516],{"class":117,"line":275},[173,1508,1509],{"class":245},"    source ",[173,1511,494],{"class":241},[173,1513,497],{"class":245},[173,1515,500],{"class":241},[173,1517,1518],{"class":182}," \"broken.xlsx\"\n",[173,1520,1521,1524,1526,1528,1530,1532,1534,1536,1538],{"class":117,"line":282},[173,1522,1523],{"class":245},"    broken.to_excel(source, ",[173,1525,513],{"class":512},[173,1527,494],{"class":241},[173,1529,518],{"class":182},[173,1531,316],{"class":245},[173,1533,523],{"class":512},[173,1535,494],{"class":241},[173,1537,528],{"class":312},[173,1539,531],{"class":245},[173,1541,1542],{"class":117,"line":294},[173,1543,267],{"emptyLinePlaceholder":266},[173,1545,1546,1549,1552,1555,1557,1560,1562,1564],{"class":117,"line":303},[173,1547,1548],{"class":241},"    with",[173,1550,1551],{"class":245}," pytest.raises(",[173,1553,1554],{"class":312},"ValueError",[173,1556,316],{"class":245},[173,1558,1559],{"class":512},"match",[173,1561,494],{"class":241},[173,1563,723],{"class":182},[173,1565,1566],{"class":245},"):\n",[173,1568,1569,1572,1574,1577],{"class":117,"line":340},[173,1570,1571],{"class":245},"        build_report(source, tmp_path ",[173,1573,500],{"class":241},[173,1575,1576],{"class":182}," \"out.xlsx\"",[173,1578,531],{"class":245},[173,1580,1581],{"class":117,"line":371},[173,1582,267],{"emptyLinePlaceholder":266},[173,1584,1585,1587,1590,1593,1595,1597,1600],{"class":117,"line":404},[173,1586,645],{"class":241},[173,1588,1589],{"class":241}," not",[173,1591,1592],{"class":245}," (tmp_path ",[173,1594,500],{"class":241},[173,1596,1576],{"class":182},[173,1598,1599],{"class":245},").exists()       ",[173,1601,1602],{"class":234},"# nothing half-written\n",[10,1604,1605,1606,1609],{},"The ",[170,1607,1608],{},"match=\"Region\""," is the point: it pins not just that the code raised, but that the message names the missing column, which is the difference between a five-second diagnosis and a debugging session. The final assertion pins that a failed run leaves no partial file behind for someone to email by mistake.",[158,1611,1613],{"id":1612},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1615,1616,1617,1633],"table",{},[1618,1619,1620],"thead",{},[1621,1622,1623,1627,1630],"tr",{},[1624,1625,1626],"th",{},"Symptom",[1624,1628,1629],{},"Cause",[1624,1631,1632],{},"Fix",[1634,1635,1636,1651,1670,1685,1701,1715,1726],"tbody",{},[1621,1637,1638,1642,1648],{},[1639,1640,1641],"td",{},"Byte comparison of two workbooks always differs",[1639,1643,1644,1647],{},[170,1645,1646],{},".xlsx"," is a zip with timestamps and unordered parts",[1639,1649,1650],{},"Compare values by reopening, never bytes",[1621,1652,1653,1659,1662],{},[1639,1654,1655,1658],{},[170,1656,1657],{},"assert total == 150.25"," fails by 1e-13",[1639,1660,1661],{},"Floating-point summation",[1639,1663,1664,1666,1667,1669],{},[170,1665,1081],{},", or ",[170,1668,1085],{}," for money",[1621,1671,1672,1677,1682],{},[1639,1673,1674,1675],{},"Reading a formula cell returns ",[170,1676,1095],{},[1639,1678,1679,1681],{},[170,1680,1091],{}," on a file Excel has never opened",[1639,1683,1684],{},"Assert the formula string instead",[1621,1686,1687,1690,1696],{},[1639,1688,1689],{},"Header assertion fails with an extra empty first column",[1639,1691,1692,1695],{},[170,1693,1694],{},"to_excel"," wrote the index",[1639,1697,1698,1699],{},"Pass ",[170,1700,846],{},[1621,1702,1703,1706,1709],{},[1639,1704,1705],{},"Tests pass locally, fail in CI",[1639,1707,1708],{},"Test wrote to a relative path that exists on your machine",[1639,1710,1711,1712,1714],{},"Use ",[170,1713,544],{}," everywhere",[1621,1716,1717,1720,1723],{},[1639,1718,1719],{},"Suite slows to a crawl",[1639,1721,1722],{},"Every test builds a large workbook",[1639,1724,1725],{},"Five rows prove the logic; keep the big file for one smoke test",[1621,1727,1728,1738,1743],{},[1639,1729,1730,1731,1734,1735],{},"Dates come back as ",[170,1732,1733],{},"datetime"," not ",[170,1736,1737],{},"date",[1639,1739,1740,1741],{},"openpyxl always reads date cells as ",[170,1742,1733],{},[1639,1744,1745,1746,1749,1750],{},"Compare with ",[170,1747,1748],{},"datetime(2026, 7, 1)"," or call ",[170,1751,1752],{},".date()",[158,1754,1756],{"id":1755},"performance-and-scale-notes","Performance and scale notes",[10,1758,1759,1760,1763],{},"Excel output tests are fast when the fixtures are small: writing a five-row workbook and reopening it costs a few milliseconds, so a suite of thirty runs in under a second. The cost appears when fixtures grow — a 50,000-row fixture written per test turns a one-second suite into a minute. Keep one realistic-size test, mark it ",[170,1761,1762],{},"@pytest.mark.slow",", and run it separately.",[10,1765,1766,1767,1770,1771,215],{},"If the report writes large files, test the ",[1450,1768,1769],{},"streaming"," path rather than the volume: assert that the code uses write-only or chunked mode by checking peak memory with a small file, not by writing a real one. The techniques themselves are covered in ",[17,1772,1774],{"href":1773},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002F","Working with Large Excel Files in Python",[158,1776,1778],{"id":1777},"conclusion","Conclusion",[10,1780,1781,1782,1784,1785,1787],{},"Testing an Excel report means reopening the file and asking the four questions a reader would ask: are the sheets there, is the header right, are there rows, and do the totals add up. Build fixtures inside the test with ",[170,1783,544],{},", use ",[170,1786,1081],{}," for anything computed, assert the handful of formats that carry meaning, and pin the error messages for bad input. That suite is small, runs in a second, and catches the three failures that otherwise reach the person waiting for the report.",[158,1789,1791],{"id":1790},"frequently-asked-questions","Frequently asked questions",[10,1793,1794,1798,1799,1801],{},[1795,1796,1797],"strong",{},"Should I compare the whole file against a golden workbook?","\nNo. Two ",[170,1800,1646],{}," files written from identical data differ in zip timestamps and internal ordering, so a byte comparison fails constantly. Compare the values you care about — sheet names, headers, a total, a row count — by reopening the file.",[10,1803,1804,1807,1808,1811,1812,1814],{},[1795,1805,1806],{},"Why do my float assertions fail by a tiny amount?","\nExcel stores numbers as IEEE doubles and pandas sums in floating point, so a total can land on ",[170,1809,1810],{},"149.99999999999997",". Use ",[170,1813,1081],{}," or round before comparing.",[10,1816,1817,1820,1821,316,1824,316,1827,1830],{},[1795,1818,1819],{},"How do I test formatting rather than values?","\nReopen with openpyxl and read the style attributes — ",[170,1822,1823],{},"cell.number_format",[170,1825,1826],{},"cell.font.bold",[170,1828,1829],{},"cell.fill.start_color.rgb",". openpyxl exposes what was written, so the assertion is direct.",[10,1832,1833,1836],{},[1795,1834,1835],{},"Do the tests need Microsoft Excel installed?","\nNo. openpyxl, pandas and xlsxwriter write and read the file format directly, so the whole suite runs on a headless CI runner with no Excel anywhere.",[158,1838,1840],{"id":1839},"related","Related",[10,1842,1843],{},"Up to the parent guide:",[1845,1846,1847],"ul",{},[1848,1849,1850,1852],"li",{},[17,1851,20],{"href":19}," — where the transform\u002FI-O split that makes these tests possible comes from.",[10,1854,1855],{},"Related guides:",[1845,1857,1858,1865,1876,1883],{},[1848,1859,1860,1864],{},[17,1861,1863],{"href":1862},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," — the same assertions, run in production instead of in CI.",[1848,1866,1867,1871,1872,1875],{},[17,1868,1870],{"href":1869},"\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"," — making ",[170,1873,1874],{},"parse_args"," testable without a subprocess.",[1848,1877,1878,1882],{},[17,1879,1881],{"href":1880},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcompare-two-excel-files-for-differences-with-python\u002F","Compare Two Excel Files for Differences with Python"," — when you do want to diff two workbooks.",[1848,1884,1885,1887,1888,1890],{},[17,1886,1104],{"href":1103}," — why a formula cell reads back as ",[170,1889,1095],{}," in a test.",[1892,1893,1894],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":168,"searchDepth":238,"depth":238,"links":1896},[1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907],{"id":160,"depth":238,"text":161},{"id":218,"depth":238,"text":219},{"id":548,"depth":238,"text":549},{"id":854,"depth":238,"text":855},{"id":1234,"depth":238,"text":1235},{"id":1444,"depth":238,"text":1445},{"id":1612,"depth":238,"text":1613},{"id":1755,"depth":238,"text":1756},{"id":1777,"depth":238,"text":1778},{"id":1790,"depth":238,"text":1791},{"id":1839,"depth":238,"text":1840},"2026-08-10","Assert on the workbook your script actually wrote: build fixtures in tmp_path, check sheets, headers, totals and number formats, and compare floats without false failures.","md",[1912,1914,1916,1918],{"q":1797,"a":1913},"No. Two .xlsx files written from identical data differ in zip timestamps and internal ordering, so a byte comparison fails constantly. Compare the values you care about — sheet names, headers, a total, a row count — by reopening the file.",{"q":1806,"a":1915},"Excel stores numbers as IEEE doubles and pandas sums in floating point, so a total can land on 149.99999999999997. Use pytest.approx or round before comparing.",{"q":1819,"a":1917},"Reopen with openpyxl and read the style attributes — cell.number_format, cell.font.bold, cell.fill.start_color.rgb. openpyxl exposes what was written, so the assertion is direct.",{"q":1835,"a":1919},"No. openpyxl, pandas and xlsxwriter write and read the file format directly, so the whole suite runs on a headless CI runner with no Excel anywhere.",{"breadcrumb":1921},[1922,1924,1927,1928],{"name":1923,"item":500},"Home",{"name":1925,"item":1926},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":20,"item":19},{"name":5,"item":1929},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest",{"title":1932,"description":1933},"Test Excel Output with pytest (Python)","Write pytest tests for Python Excel reports: generate fixtures with tmp_path, reopen the saved workbook with openpyxl, and assert on sheets, headers, totals and formats.","test-excel-output-with-pytest","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002Findex","how-to","RCfogydKjbrWayaUos2b9MuVjumq_AQVR2mN5qa8oNY",[1939,1943],{"title":1940,"path":1941,"stem":1942,"children":-1},"Package a Python Excel Script as an EXE with PyInstaller","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fpackage-a-python-excel-script-as-an-exe-with-pyinstaller\u002Findex",{"title":1944,"path":1945,"stem":1946,"children":-1},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python","formatting-and-charting-excel-reports-with-python\u002Findex",1786800027142]