[{"data":1,"prerenderedAt":2685},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests":2651,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests":2659,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests":2664,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests":2665},{"id":4,"title":5,"body":6,"dateModified":2618,"datePublished":2618,"description":2619,"extension":2620,"faq":2621,"keyword":2630,"kind":2631,"meta":2632,"navigation":274,"path":2640,"seo":2641,"slug":2644,"stem":2645,"topics":2646,"type":2649,"__hash__":2650},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests\u002Findex.md","Compare Excel Output to a Golden File in Tests",{"type":7,"value":8,"toc":2607},"minimark",[9,24,203,208,236,239,753,757,768,771,822,828,832,835,1558,1565,1579,1586,1701,1705,1719,2148,2331,2344,2348,2359,2365,2376,2484,2488,2528,2532,2538,2542,2548,2554,2564,2570,2574,2603],[10,11,12,13,17,18,23],"p",{},"Unit tests check the parts of a report you thought to assert: this total, that header. A golden file test checks everything else too. The approved output is stored next to the tests, every run compares against it, and any change — a lost number format, a header that is no longer bold, a total that moved a row — fails with a diff that says exactly what changed. For Excel output the trick is choosing ",[14,15,16],"em",{},"what"," to compare, because the workbook's bytes change every time it is saved. 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",", builds a readable snapshot format, a pytest fixture with an update option and shows the diff a real regression produces.",[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,109,34,113,34,117,34,121,34,124,34,129,34,136,34,141,34,144,34,147,34,151,34,154,34,157,34,163,34,167,34,170,34,173,34,177,34,180,34,185,34,192,34,197,34,200],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"4 0 752 142","img",[30,31],"gd-flow-t","gd-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},"A golden file test run",[40,41,42],"desc",{"id":31},"The test builds the report into a temporary folder, turns the workbook into a text snapshot with volatile cells masked, and compares it with the approved golden file in the repository. If they match the test passes. If they differ the test fails with a unified diff; when the change is intended, rerunning with --update-golden rewrites the golden file, and the change is reviewed in the pull request.",[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","115.2","110.5","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[62,63,65],"g",{"transform":64},"translate(65.6,37.8) scale(1.000)",[66,67],"path",{"d":68,"fill":69,"stroke":70,"style":71},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","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},"77.6","84.2","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Build report",[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","into tmp_path",[85,86],"line",{"x1":87,"y1":88,"x2":89,"y2":88,"stroke":59,"style":60},"140.2","71.2","162.2",[91,92],"polygon",{"points":93,"fill":94},"166.2,71.2 157.2,76.2 157.2,66.2","#5b5cf0",[44,96],{"x":97,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"171.2",[62,99,101],{"transform":100},"translate(216.8,29.0) scale(1.000)",[66,102],{"d":103,"fill":69,"stroke":70,"style":71},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[73,105,108],{"x":106,"y":107,"style":77},"228.8","75.5","Snapshot",[73,110,112],{"x":106,"y":111,"style":82},"95.0","one line per",[73,114,116],{"x":106,"y":115,"style":82},"112.5","cell, masked",[85,118],{"x1":119,"y1":88,"x2":120,"y2":88,"stroke":59,"style":60},"291.4","313.4",[91,122],{"points":123,"fill":94},"317.4,71.2 308.4,76.2 308.4,66.2",[44,125],{"x":126,"y":54,"width":55,"height":56,"rx":57,"fill":127,"stroke":128,"style":60},"322.4","#fdefd8","var(--gold,#b4740a)",[62,130,132],{"transform":131},"translate(368.0,29.0) scale(1.000)",[66,133],{"d":134,"fill":69,"stroke":135,"style":71},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z","var(--gold-ink,#7a4e06)",[73,137,140],{"x":138,"y":107,"style":139},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Compare",[73,142,143],{"x":138,"y":111,"style":82},"with",[73,145,146],{"x":138,"y":115,"style":82},"golden\u002F*.txt",[85,148],{"x1":149,"y1":88,"x2":150,"y2":88,"stroke":59,"style":60},"442.6","464.6",[91,152],{"points":153,"fill":94},"468.6,71.2 459.6,76.2 459.6,66.2",[44,155],{"x":156,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"473.6",[62,158,160],{"transform":159},"translate(519.2,29.0) scale(1.000)",[66,161],{"d":162,"fill":69,"stroke":70,"style":71},"M5 12.5l4.5 4.5L19 7",[73,164,166],{"x":165,"y":107,"style":77},"531.2","Pass or diff",[73,168,169],{"x":165,"y":111,"style":82},"unified diff on",[73,171,172],{"x":165,"y":115,"style":82},"failure",[85,174],{"x1":175,"y1":88,"x2":176,"y2":88,"stroke":59,"style":60},"593.8","615.8",[91,178],{"points":179,"fill":94},"619.8,71.2 610.8,76.2 610.8,66.2",[44,181],{"x":182,"y":54,"width":55,"height":56,"rx":57,"fill":183,"stroke":184,"style":60},"624.8","#d9f4f1","var(--teal,#0f9488)",[62,186,188],{"transform":187},"translate(670.4,29.0) scale(1.000)",[66,189],{"d":190,"fill":69,"stroke":191,"style":71},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--teal-ink,#0b6157)",[73,193,196],{"x":194,"y":107,"style":195},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","--update-golden",[73,198,199],{"x":194,"y":111,"style":82},"reviewed in the",[73,201,202],{"x":194,"y":115,"style":82},"PR",[204,205,207],"h2",{"id":206},"prerequisites","Prerequisites",[209,210,215],"pre",{"className":211,"code":212,"language":213,"meta":214,"style":214},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pytest\n","bash","",[216,217,218],"code",{"__ignoreMap":214},[219,220,222,226,230,233],"span",{"class":85,"line":221},1,[219,223,225],{"class":224},"sMTad","pip",[219,227,229],{"class":228},"srMev"," install",[219,231,232],{"class":228}," openpyxl",[219,234,235],{"class":228}," pytest\n",[10,237,238],{},"The report under test — a title, a generated-at timestamp, a styled header, data rows with a number format and a total row:",[209,240,244],{"className":241,"code":242,"language":243,"meta":214,"style":214},"language-python shiki shiki-themes github-light github-dark-high-contrast","# report.py — the code under test\nimport datetime as dt\n\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill\n\ndef build_report(rows, path, generated=None):\n    wb = Workbook()\n    ws = wb.active\n    ws.title = \"Summary\"\n    ws[\"A1\"] = \"Regional sales\"\n    ws[\"A1\"].font = Font(size=14, bold=True)\n    ws[\"D1\"] = generated or dt.datetime.now()\n    ws.append([])\n    ws.append([\"Region\", \"Orders\", \"Revenue\"])\n    for cell in ws[3]:\n        cell.font = Font(bold=True)\n        cell.fill = PatternFill(\"solid\", fgColor=\"FFDDEBF7\")\n    for region, orders, revenue in rows:\n        ws.append([region, orders, revenue])\n    total_row = ws.max_row + 1\n    ws.cell(total_row, 1, \"Total\").font = Font(bold=True)\n    ws.cell(total_row, 2, f\"=SUM(B4:B{total_row - 1})\")\n    ws.cell(total_row, 3, f\"=SUM(C4:C{total_row - 1})\")\n    for row in ws.iter_rows(min_row=4, min_col=3, max_col=3):\n        row[0].number_format = \"#,##0.00\"\n    ws.column_dimensions[\"A\"].width = 18\n    ws.freeze_panes = \"A4\"\n    wb.save(path)\n","python",[216,245,246,252,269,276,290,303,308,331,342,353,364,381,419,440,446,468,489,507,533,546,552,569,598,635,663,703,719,736,747],{"__ignoreMap":214},[219,247,248],{"class":85,"line":221},[219,249,251],{"class":250},"s-wDw","# report.py — the code under test\n",[219,253,255,259,263,266],{"class":85,"line":254},2,[219,256,258],{"class":257},"s-kum","import",[219,260,262],{"class":261},"skGVy"," datetime ",[219,264,265],{"class":257},"as",[219,267,268],{"class":261}," dt\n",[219,270,272],{"class":85,"line":271},3,[219,273,275],{"emptyLinePlaceholder":274},true,"\n",[219,277,279,282,285,287],{"class":85,"line":278},4,[219,280,281],{"class":257},"from",[219,283,284],{"class":261}," openpyxl ",[219,286,258],{"class":257},[219,288,289],{"class":261}," Workbook\n",[219,291,293,295,298,300],{"class":85,"line":292},5,[219,294,281],{"class":257},[219,296,297],{"class":261}," openpyxl.styles ",[219,299,258],{"class":257},[219,301,302],{"class":261}," Font, PatternFill\n",[219,304,306],{"class":85,"line":305},6,[219,307,275],{"emptyLinePlaceholder":274},[219,309,311,314,318,321,324,328],{"class":85,"line":310},7,[219,312,313],{"class":257},"def",[219,315,317],{"class":316},"s_Opv"," build_report",[219,319,320],{"class":261},"(rows, path, generated",[219,322,323],{"class":257},"=",[219,325,327],{"class":326},"sP0c6","None",[219,329,330],{"class":261},"):\n",[219,332,334,337,339],{"class":85,"line":333},8,[219,335,336],{"class":261},"    wb ",[219,338,323],{"class":257},[219,340,341],{"class":261}," Workbook()\n",[219,343,345,348,350],{"class":85,"line":344},9,[219,346,347],{"class":261},"    ws ",[219,349,323],{"class":257},[219,351,352],{"class":261}," wb.active\n",[219,354,356,359,361],{"class":85,"line":355},10,[219,357,358],{"class":261},"    ws.title ",[219,360,323],{"class":257},[219,362,363],{"class":228}," \"Summary\"\n",[219,365,367,370,373,376,378],{"class":85,"line":366},11,[219,368,369],{"class":261},"    ws[",[219,371,372],{"class":228},"\"A1\"",[219,374,375],{"class":261},"] ",[219,377,323],{"class":257},[219,379,380],{"class":228}," \"Regional sales\"\n",[219,382,384,386,388,391,393,396,400,402,405,408,411,413,416],{"class":85,"line":383},12,[219,385,369],{"class":261},[219,387,372],{"class":228},[219,389,390],{"class":261},"].font ",[219,392,323],{"class":257},[219,394,395],{"class":261}," Font(",[219,397,399],{"class":398},"sa561","size",[219,401,323],{"class":257},[219,403,404],{"class":326},"14",[219,406,407],{"class":261},", ",[219,409,410],{"class":398},"bold",[219,412,323],{"class":257},[219,414,415],{"class":326},"True",[219,417,418],{"class":261},")\n",[219,420,422,424,427,429,431,434,437],{"class":85,"line":421},13,[219,423,369],{"class":261},[219,425,426],{"class":228},"\"D1\"",[219,428,375],{"class":261},[219,430,323],{"class":257},[219,432,433],{"class":261}," generated ",[219,435,436],{"class":257},"or",[219,438,439],{"class":261}," dt.datetime.now()\n",[219,441,443],{"class":85,"line":442},14,[219,444,445],{"class":261},"    ws.append([])\n",[219,447,449,452,455,457,460,462,465],{"class":85,"line":448},15,[219,450,451],{"class":261},"    ws.append([",[219,453,454],{"class":228},"\"Region\"",[219,456,407],{"class":261},[219,458,459],{"class":228},"\"Orders\"",[219,461,407],{"class":261},[219,463,464],{"class":228},"\"Revenue\"",[219,466,467],{"class":261},"])\n",[219,469,471,474,477,480,483,486],{"class":85,"line":470},16,[219,472,473],{"class":257},"    for",[219,475,476],{"class":261}," cell ",[219,478,479],{"class":257},"in",[219,481,482],{"class":261}," ws[",[219,484,485],{"class":326},"3",[219,487,488],{"class":261},"]:\n",[219,490,492,495,497,499,501,503,505],{"class":85,"line":491},17,[219,493,494],{"class":261},"        cell.font ",[219,496,323],{"class":257},[219,498,395],{"class":261},[219,500,410],{"class":398},[219,502,323],{"class":257},[219,504,415],{"class":326},[219,506,418],{"class":261},[219,508,510,513,515,518,521,523,526,528,531],{"class":85,"line":509},18,[219,511,512],{"class":261},"        cell.fill ",[219,514,323],{"class":257},[219,516,517],{"class":261}," PatternFill(",[219,519,520],{"class":228},"\"solid\"",[219,522,407],{"class":261},[219,524,525],{"class":398},"fgColor",[219,527,323],{"class":257},[219,529,530],{"class":228},"\"FFDDEBF7\"",[219,532,418],{"class":261},[219,534,536,538,541,543],{"class":85,"line":535},19,[219,537,473],{"class":257},[219,539,540],{"class":261}," region, orders, revenue ",[219,542,479],{"class":257},[219,544,545],{"class":261}," rows:\n",[219,547,549],{"class":85,"line":548},20,[219,550,551],{"class":261},"        ws.append([region, orders, revenue])\n",[219,553,555,558,560,563,566],{"class":85,"line":554},21,[219,556,557],{"class":261},"    total_row ",[219,559,323],{"class":257},[219,561,562],{"class":261}," ws.max_row ",[219,564,565],{"class":257},"+",[219,567,568],{"class":326}," 1\n",[219,570,572,575,578,580,583,586,588,590,592,594,596],{"class":85,"line":571},22,[219,573,574],{"class":261},"    ws.cell(total_row, ",[219,576,577],{"class":326},"1",[219,579,407],{"class":261},[219,581,582],{"class":228},"\"Total\"",[219,584,585],{"class":261},").font ",[219,587,323],{"class":257},[219,589,395],{"class":261},[219,591,410],{"class":398},[219,593,323],{"class":257},[219,595,415],{"class":326},[219,597,418],{"class":261},[219,599,601,603,606,608,611,614,618,621,624,627,630,633],{"class":85,"line":600},23,[219,602,574],{"class":261},[219,604,605],{"class":326},"2",[219,607,407],{"class":261},[219,609,610],{"class":257},"f",[219,612,613],{"class":228},"\"=SUM(B4:B",[219,615,617],{"class":616},"sSjpA","{",[219,619,620],{"class":261},"total_row ",[219,622,623],{"class":257},"-",[219,625,626],{"class":326}," 1",[219,628,629],{"class":616},"}",[219,631,632],{"class":228},")\"",[219,634,418],{"class":261},[219,636,638,640,642,644,646,649,651,653,655,657,659,661],{"class":85,"line":637},24,[219,639,574],{"class":261},[219,641,485],{"class":326},[219,643,407],{"class":261},[219,645,610],{"class":257},[219,647,648],{"class":228},"\"=SUM(C4:C",[219,650,617],{"class":616},[219,652,620],{"class":261},[219,654,623],{"class":257},[219,656,626],{"class":326},[219,658,629],{"class":616},[219,660,632],{"class":228},[219,662,418],{"class":261},[219,664,666,668,671,673,676,679,681,683,685,688,690,692,694,697,699,701],{"class":85,"line":665},25,[219,667,473],{"class":257},[219,669,670],{"class":261}," row ",[219,672,479],{"class":257},[219,674,675],{"class":261}," ws.iter_rows(",[219,677,678],{"class":398},"min_row",[219,680,323],{"class":257},[219,682,46],{"class":326},[219,684,407],{"class":261},[219,686,687],{"class":398},"min_col",[219,689,323],{"class":257},[219,691,485],{"class":326},[219,693,407],{"class":261},[219,695,696],{"class":398},"max_col",[219,698,323],{"class":257},[219,700,485],{"class":326},[219,702,330],{"class":261},[219,704,706,709,711,714,716],{"class":85,"line":705},26,[219,707,708],{"class":261},"        row[",[219,710,47],{"class":326},[219,712,713],{"class":261},"].number_format ",[219,715,323],{"class":257},[219,717,718],{"class":228}," \"#,##0.00\"\n",[219,720,722,725,728,731,733],{"class":85,"line":721},27,[219,723,724],{"class":261},"    ws.column_dimensions[",[219,726,727],{"class":228},"\"A\"",[219,729,730],{"class":261},"].width ",[219,732,323],{"class":257},[219,734,735],{"class":326}," 18\n",[219,737,739,742,744],{"class":85,"line":738},28,[219,740,741],{"class":261},"    ws.freeze_panes ",[219,743,323],{"class":257},[219,745,746],{"class":228}," \"A4\"\n",[219,748,750],{"class":85,"line":749},29,[219,751,752],{"class":261},"    wb.save(path)\n",[204,754,756],{"id":755},"step-1-do-not-compare-bytes","Step 1 — Do not compare bytes",[10,758,759,760,763,764,767],{},"The obvious golden test — save the expected ",[216,761,762],{},".xlsx",", compare files — fails on the second run. openpyxl sets the workbook's modified time to \"now\" in ",[216,765,766],{},"docProps\u002Fcore.xml"," every time it saves, whatever the code does, so identical reports produce different bytes. Even if the bytes matched, a failure would only say \"files differ\", leaving you to open both in Excel and hunt.",[10,769,770],{},"XlsxWriter is different: with a fixed creation date its output is reproducible, and in a test two builds a second apart were byte-identical:",[209,772,774],{"className":241,"code":773,"language":243,"meta":214,"style":214},"workbook = xlsxwriter.Workbook(buffer, {\"in_memory\": True})\nworkbook.set_properties({\"created\": datetime.datetime(2026, 1, 1)})\n",[216,775,776,797],{"__ignoreMap":214},[219,777,778,781,783,786,789,792,794],{"class":85,"line":221},[219,779,780],{"class":261},"workbook ",[219,782,323],{"class":257},[219,784,785],{"class":261}," xlsxwriter.Workbook(buffer, {",[219,787,788],{"class":228},"\"in_memory\"",[219,790,791],{"class":261},": ",[219,793,415],{"class":326},[219,795,796],{"class":261},"})\n",[219,798,799,802,805,808,811,813,815,817,819],{"class":85,"line":254},[219,800,801],{"class":261},"workbook.set_properties({",[219,803,804],{"class":228},"\"created\"",[219,806,807],{"class":261},": datetime.datetime(",[219,809,810],{"class":326},"2026",[219,812,407],{"class":261},[219,814,577],{"class":326},[219,816,407],{"class":261},[219,818,577],{"class":326},[219,820,821],{"class":261},")})\n",[10,823,824,825,827],{},"That makes a byte comparison possible for XlsxWriter reports, but it still cannot tell you ",[14,826,16],{}," changed, and a library upgrade that reorders XML fails every test at once. A text snapshot solves both problems.",[204,829,831],{"id":830},"step-2-describe-the-workbook-as-text","Step 2 — Describe the workbook as text",[10,833,834],{},"The snapshot lists each sheet's settings, then one line per non-empty cell with its value, number format, bold and fill. Floats are rounded to ten significant digits so harmless floating-point noise does not fail tests, and masked cells are replaced by a placeholder:",[209,836,838],{"className":241,"code":837,"language":243,"meta":214,"style":214},"# snapshot.py — a readable, diffable description of a workbook\nfrom openpyxl import load_workbook\n\ndef describe(cell):\n    value = cell.value\n    if isinstance(value, float):\n        value = float(f\"{value:.10g}\")                   # ignore float noise\n    parts = [repr(value.isoformat() if hasattr(value, \"isoformat\") else value)]\n    if cell.number_format != \"General\":\n        parts.append(f\"fmt={cell.number_format}\")\n    if cell.font.b:\n        parts.append(\"bold\")\n    if cell.fill.fill_type:\n        parts.append(f\"fill={cell.fill.fgColor.rgb}\")\n    return \" \".join(parts)\n\ndef snapshot(path, mask=()):\n    wb = load_workbook(path)\n    lines = []\n    for ws in wb.worksheets:\n        widths = \",\".join(f\"{k}:{d.width:g}\" for k, d in sorted(ws.column_dimensions.items()) if d.customWidth)\n        merged = \",\".join(sorted(str(r) for r in ws.merged_cells.ranges))\n        lines.append(f\"[{ws.title}] state={ws.sheet_state} freeze={ws.freeze_panes} widths={widths} merged={merged}\")\n        for row in ws.iter_rows():\n            for cell in row:\n                if cell.value is None and not cell.fill.fill_type:\n                    continue\n                key = f\"{ws.title}!{cell.coordinate}\"\n                lines.append(f\"{cell.coordinate} \" + (\"\u003Cmasked>\" if key in mask else describe(cell)))\n    return \"\\n\".join(lines) + \"\\n\"\n\nimport datetime as dt\n\nrows = [(\"North\", 120, 18240.5), (\"South\", 95, 14195.0), (\"East\", 60, 9730.25)]\nbuild_report(rows, \"summary.xlsx\", generated=dt.datetime(2026, 9, 23, 7, 30))\nprint(snapshot(\"summary.xlsx\", mask={\"Summary!D1\"}))\n",[216,839,840,845,856,860,870,880,896,932,968,984,1005,1012,1021,1028,1048,1059,1063,1078,1087,1097,1109,1167,1200,1261,1273,1285,1307,1312,1343,1386,1409,1414,1425,1430,1488,1532],{"__ignoreMap":214},[219,841,842],{"class":85,"line":221},[219,843,844],{"class":250},"# snapshot.py — a readable, diffable description of a workbook\n",[219,846,847,849,851,853],{"class":85,"line":254},[219,848,281],{"class":257},[219,850,284],{"class":261},[219,852,258],{"class":257},[219,854,855],{"class":261}," load_workbook\n",[219,857,858],{"class":85,"line":271},[219,859,275],{"emptyLinePlaceholder":274},[219,861,862,864,867],{"class":85,"line":278},[219,863,313],{"class":257},[219,865,866],{"class":316}," describe",[219,868,869],{"class":261},"(cell):\n",[219,871,872,875,877],{"class":85,"line":292},[219,873,874],{"class":261},"    value ",[219,876,323],{"class":257},[219,878,879],{"class":261}," cell.value\n",[219,881,882,885,888,891,894],{"class":85,"line":305},[219,883,884],{"class":257},"    if",[219,886,887],{"class":326}," isinstance",[219,889,890],{"class":261},"(value, ",[219,892,893],{"class":326},"float",[219,895,330],{"class":261},[219,897,898,901,903,906,909,911,914,916,919,922,924,926,929],{"class":85,"line":310},[219,899,900],{"class":261},"        value ",[219,902,323],{"class":257},[219,904,905],{"class":326}," float",[219,907,908],{"class":261},"(",[219,910,610],{"class":257},[219,912,913],{"class":228},"\"",[219,915,617],{"class":616},[219,917,918],{"class":261},"value",[219,920,921],{"class":257},":.10g",[219,923,629],{"class":616},[219,925,913],{"class":228},[219,927,928],{"class":261},")                   ",[219,930,931],{"class":250},"# ignore float noise\n",[219,933,934,937,939,942,945,948,951,954,956,959,962,965],{"class":85,"line":333},[219,935,936],{"class":261},"    parts ",[219,938,323],{"class":257},[219,940,941],{"class":261}," [",[219,943,944],{"class":326},"repr",[219,946,947],{"class":261},"(value.isoformat() ",[219,949,950],{"class":257},"if",[219,952,953],{"class":326}," hasattr",[219,955,890],{"class":261},[219,957,958],{"class":228},"\"isoformat\"",[219,960,961],{"class":261},") ",[219,963,964],{"class":257},"else",[219,966,967],{"class":261}," value)]\n",[219,969,970,972,975,978,981],{"class":85,"line":344},[219,971,884],{"class":257},[219,973,974],{"class":261}," cell.number_format ",[219,976,977],{"class":257},"!=",[219,979,980],{"class":228}," \"General\"",[219,982,983],{"class":261},":\n",[219,985,986,989,991,994,996,999,1001,1003],{"class":85,"line":355},[219,987,988],{"class":261},"        parts.append(",[219,990,610],{"class":257},[219,992,993],{"class":228},"\"fmt=",[219,995,617],{"class":616},[219,997,998],{"class":261},"cell.number_format",[219,1000,629],{"class":616},[219,1002,913],{"class":228},[219,1004,418],{"class":261},[219,1006,1007,1009],{"class":85,"line":366},[219,1008,884],{"class":257},[219,1010,1011],{"class":261}," cell.font.b:\n",[219,1013,1014,1016,1019],{"class":85,"line":383},[219,1015,988],{"class":261},[219,1017,1018],{"class":228},"\"bold\"",[219,1020,418],{"class":261},[219,1022,1023,1025],{"class":85,"line":421},[219,1024,884],{"class":257},[219,1026,1027],{"class":261}," cell.fill.fill_type:\n",[219,1029,1030,1032,1034,1037,1039,1042,1044,1046],{"class":85,"line":442},[219,1031,988],{"class":261},[219,1033,610],{"class":257},[219,1035,1036],{"class":228},"\"fill=",[219,1038,617],{"class":616},[219,1040,1041],{"class":261},"cell.fill.fgColor.rgb",[219,1043,629],{"class":616},[219,1045,913],{"class":228},[219,1047,418],{"class":261},[219,1049,1050,1053,1056],{"class":85,"line":448},[219,1051,1052],{"class":257},"    return",[219,1054,1055],{"class":228}," \" \"",[219,1057,1058],{"class":261},".join(parts)\n",[219,1060,1061],{"class":85,"line":470},[219,1062,275],{"emptyLinePlaceholder":274},[219,1064,1065,1067,1070,1073,1075],{"class":85,"line":491},[219,1066,313],{"class":257},[219,1068,1069],{"class":316}," snapshot",[219,1071,1072],{"class":261},"(path, mask",[219,1074,323],{"class":257},[219,1076,1077],{"class":261},"()):\n",[219,1079,1080,1082,1084],{"class":85,"line":509},[219,1081,336],{"class":261},[219,1083,323],{"class":257},[219,1085,1086],{"class":261}," load_workbook(path)\n",[219,1088,1089,1092,1094],{"class":85,"line":535},[219,1090,1091],{"class":261},"    lines ",[219,1093,323],{"class":257},[219,1095,1096],{"class":261}," []\n",[219,1098,1099,1101,1104,1106],{"class":85,"line":548},[219,1100,473],{"class":257},[219,1102,1103],{"class":261}," ws ",[219,1105,479],{"class":257},[219,1107,1108],{"class":261}," wb.worksheets:\n",[219,1110,1111,1114,1116,1119,1122,1124,1126,1128,1131,1133,1136,1138,1141,1144,1146,1148,1151,1154,1156,1159,1162,1164],{"class":85,"line":554},[219,1112,1113],{"class":261},"        widths ",[219,1115,323],{"class":257},[219,1117,1118],{"class":228}," \",\"",[219,1120,1121],{"class":261},".join(",[219,1123,610],{"class":257},[219,1125,913],{"class":228},[219,1127,617],{"class":616},[219,1129,1130],{"class":261},"k",[219,1132,629],{"class":616},[219,1134,1135],{"class":228},":",[219,1137,617],{"class":616},[219,1139,1140],{"class":261},"d.width",[219,1142,1143],{"class":257},":g",[219,1145,629],{"class":616},[219,1147,913],{"class":228},[219,1149,1150],{"class":257}," for",[219,1152,1153],{"class":261}," k, d ",[219,1155,479],{"class":257},[219,1157,1158],{"class":326}," sorted",[219,1160,1161],{"class":261},"(ws.column_dimensions.items()) ",[219,1163,950],{"class":257},[219,1165,1166],{"class":261}," d.customWidth)\n",[219,1168,1169,1172,1174,1176,1178,1181,1183,1186,1189,1192,1195,1197],{"class":85,"line":571},[219,1170,1171],{"class":261},"        merged ",[219,1173,323],{"class":257},[219,1175,1118],{"class":228},[219,1177,1121],{"class":261},[219,1179,1180],{"class":326},"sorted",[219,1182,908],{"class":261},[219,1184,1185],{"class":326},"str",[219,1187,1188],{"class":261},"(r) ",[219,1190,1191],{"class":257},"for",[219,1193,1194],{"class":261}," r ",[219,1196,479],{"class":257},[219,1198,1199],{"class":261}," ws.merged_cells.ranges))\n",[219,1201,1202,1205,1207,1210,1212,1215,1217,1220,1222,1225,1227,1230,1232,1235,1237,1240,1242,1245,1247,1250,1252,1255,1257,1259],{"class":85,"line":600},[219,1203,1204],{"class":261},"        lines.append(",[219,1206,610],{"class":257},[219,1208,1209],{"class":228},"\"[",[219,1211,617],{"class":616},[219,1213,1214],{"class":261},"ws.title",[219,1216,629],{"class":616},[219,1218,1219],{"class":228},"] state=",[219,1221,617],{"class":616},[219,1223,1224],{"class":261},"ws.sheet_state",[219,1226,629],{"class":616},[219,1228,1229],{"class":228}," freeze=",[219,1231,617],{"class":616},[219,1233,1234],{"class":261},"ws.freeze_panes",[219,1236,629],{"class":616},[219,1238,1239],{"class":228}," widths=",[219,1241,617],{"class":616},[219,1243,1244],{"class":261},"widths",[219,1246,629],{"class":616},[219,1248,1249],{"class":228}," merged=",[219,1251,617],{"class":616},[219,1253,1254],{"class":261},"merged",[219,1256,629],{"class":616},[219,1258,913],{"class":228},[219,1260,418],{"class":261},[219,1262,1263,1266,1268,1270],{"class":85,"line":637},[219,1264,1265],{"class":257},"        for",[219,1267,670],{"class":261},[219,1269,479],{"class":257},[219,1271,1272],{"class":261}," ws.iter_rows():\n",[219,1274,1275,1278,1280,1282],{"class":85,"line":665},[219,1276,1277],{"class":257},"            for",[219,1279,476],{"class":261},[219,1281,479],{"class":257},[219,1283,1284],{"class":261}," row:\n",[219,1286,1287,1290,1293,1296,1299,1302,1305],{"class":85,"line":705},[219,1288,1289],{"class":257},"                if",[219,1291,1292],{"class":261}," cell.value ",[219,1294,1295],{"class":257},"is",[219,1297,1298],{"class":326}," None",[219,1300,1301],{"class":257}," and",[219,1303,1304],{"class":257}," not",[219,1306,1027],{"class":261},[219,1308,1309],{"class":85,"line":721},[219,1310,1311],{"class":257},"                    continue\n",[219,1313,1314,1317,1319,1322,1324,1326,1328,1330,1333,1335,1338,1340],{"class":85,"line":738},[219,1315,1316],{"class":261},"                key ",[219,1318,323],{"class":257},[219,1320,1321],{"class":257}," f",[219,1323,913],{"class":228},[219,1325,617],{"class":616},[219,1327,1214],{"class":261},[219,1329,629],{"class":616},[219,1331,1332],{"class":228},"!",[219,1334,617],{"class":616},[219,1336,1337],{"class":261},"cell.coordinate",[219,1339,629],{"class":616},[219,1341,1342],{"class":228},"\"\n",[219,1344,1345,1348,1350,1352,1354,1356,1358,1361,1364,1367,1370,1373,1376,1378,1381,1383],{"class":85,"line":749},[219,1346,1347],{"class":261},"                lines.append(",[219,1349,610],{"class":257},[219,1351,913],{"class":228},[219,1353,617],{"class":616},[219,1355,1337],{"class":261},[219,1357,629],{"class":616},[219,1359,1360],{"class":228}," \"",[219,1362,1363],{"class":257}," +",[219,1365,1366],{"class":261}," (",[219,1368,1369],{"class":228},"\"\u003Cmasked>\"",[219,1371,1372],{"class":257}," if",[219,1374,1375],{"class":261}," key ",[219,1377,479],{"class":257},[219,1379,1380],{"class":261}," mask ",[219,1382,964],{"class":257},[219,1384,1385],{"class":261}," describe(cell)))\n",[219,1387,1389,1391,1393,1396,1398,1401,1403,1405,1407],{"class":85,"line":1388},30,[219,1390,1052],{"class":257},[219,1392,1360],{"class":228},[219,1394,1395],{"class":616},"\\n",[219,1397,913],{"class":228},[219,1399,1400],{"class":261},".join(lines) ",[219,1402,565],{"class":257},[219,1404,1360],{"class":228},[219,1406,1395],{"class":616},[219,1408,1342],{"class":228},[219,1410,1412],{"class":85,"line":1411},31,[219,1413,275],{"emptyLinePlaceholder":274},[219,1415,1417,1419,1421,1423],{"class":85,"line":1416},32,[219,1418,258],{"class":257},[219,1420,262],{"class":261},[219,1422,265],{"class":257},[219,1424,268],{"class":261},[219,1426,1428],{"class":85,"line":1427},33,[219,1429,275],{"emptyLinePlaceholder":274},[219,1431,1433,1436,1438,1441,1444,1446,1449,1451,1454,1457,1460,1462,1465,1467,1470,1472,1475,1477,1480,1482,1485],{"class":85,"line":1432},34,[219,1434,1435],{"class":261},"rows ",[219,1437,323],{"class":257},[219,1439,1440],{"class":261}," [(",[219,1442,1443],{"class":228},"\"North\"",[219,1445,407],{"class":261},[219,1447,1448],{"class":326},"120",[219,1450,407],{"class":261},[219,1452,1453],{"class":326},"18240.5",[219,1455,1456],{"class":261},"), (",[219,1458,1459],{"class":228},"\"South\"",[219,1461,407],{"class":261},[219,1463,1464],{"class":326},"95",[219,1466,407],{"class":261},[219,1468,1469],{"class":326},"14195.0",[219,1471,1456],{"class":261},[219,1473,1474],{"class":228},"\"East\"",[219,1476,407],{"class":261},[219,1478,1479],{"class":326},"60",[219,1481,407],{"class":261},[219,1483,1484],{"class":326},"9730.25",[219,1486,1487],{"class":261},")]\n",[219,1489,1491,1494,1497,1499,1502,1504,1507,1509,1511,1514,1516,1519,1521,1524,1526,1529],{"class":85,"line":1490},35,[219,1492,1493],{"class":261},"build_report(rows, ",[219,1495,1496],{"class":228},"\"summary.xlsx\"",[219,1498,407],{"class":261},[219,1500,1501],{"class":398},"generated",[219,1503,323],{"class":257},[219,1505,1506],{"class":261},"dt.datetime(",[219,1508,810],{"class":326},[219,1510,407],{"class":261},[219,1512,1513],{"class":326},"9",[219,1515,407],{"class":261},[219,1517,1518],{"class":326},"23",[219,1520,407],{"class":261},[219,1522,1523],{"class":326},"7",[219,1525,407],{"class":261},[219,1527,1528],{"class":326},"30",[219,1530,1531],{"class":261},"))\n",[219,1533,1535,1538,1541,1543,1545,1548,1550,1552,1555],{"class":85,"line":1534},36,[219,1536,1537],{"class":326},"print",[219,1539,1540],{"class":261},"(snapshot(",[219,1542,1496],{"class":228},[219,1544,407],{"class":261},[219,1546,1547],{"class":398},"mask",[219,1549,323],{"class":257},[219,1551,617],{"class":261},[219,1553,1554],{"class":228},"\"Summary!D1\"",[219,1556,1557],{"class":261},"}))\n",[209,1559,1563],{"className":1560,"code":1562,"language":73,"meta":214},[1561],"language-text","[Summary] state=visible freeze=A4 widths=A:18 merged=\nA1 'Regional sales' bold\nD1 \u003Cmasked>\nA3 'Region' bold fill=FFDDEBF7\nB3 'Orders' bold fill=FFDDEBF7\nC3 'Revenue' bold fill=FFDDEBF7\nD3 None bold fill=FFDDEBF7\nA4 'North'\nB4 120\nC4 18240.5 fmt=#,##0.00\n…\nC7 '=SUM(C4:C6)' fmt=#,##0.00\n",[216,1564,1562],{"__ignoreMap":214},[10,1566,1567,1568,1571,1572,1575,1576,1578],{},"The very first snapshot found something: ",[216,1569,1570],{},"D3"," — an empty cell — carries the header's bold and fill. The header loop styles every cell in row 3, and the timestamp in ",[216,1573,1574],{},"D1"," makes the sheet four columns wide. No unit test would have asserted on ",[216,1577,1570],{},"; the snapshot shows it because it records everything. Whether to fix it or accept it is a decision you now get to make.",[10,1580,1581,1582,1585],{},"Choose what to include deliberately. Values, formats, bold, fills, freeze panes, widths and merged ranges cover what readers notice; fonts, borders, conditional formatting or data validation can be added to ",[216,1583,1584],{},"describe"," when a report depends on them.",[25,1587,34,1592,34,1595,34,1598,34,1601,34,1607,34,1615,34,1621,34,1627,34,1633,34,1637,34,1641,34,1645,34,1651,34,1654,34,1659,34,1664,34,1668,34,1672,34,1677,34,1680,34,1685,34,1688,34,1693,34,1696],{"viewBox":1588,"role":28,"ariaLabelledBy":1589,"xmlns":32,"style":33},"4 0 752 233",[1590,1591],"gd-compare-t","gd-compare-d",[36,1593,1594],{"id":1590},"Comparing bytes versus comparing a snapshot",[40,1596,1597],{"id":1591},"Comparing xlsx bytes fails on every openpyxl save because of the embedded save time, reports only that the files differ, and breaks when a library reorders XML. A text snapshot ignores the save time, masks cells that change legitimately, and fails with a line-by-line diff naming the cell and the property that changed, which can also be reviewed in a pull request.",[44,1599],{"x":46,"y":47,"width":48,"height":1600,"fill":50},"233",[44,1602],{"x":53,"y":54,"width":1603,"height":1604,"rx":404,"fill":1605,"stroke":1606,"style":60},"351.0","201.0","#fce9e9","var(--danger,#dc2626)",[62,1608,1610],{"transform":1609},"translate(182.5,29.0) scale(1.083)",[66,1611],{"d":1612,"fill":69,"stroke":1613,"style":1614},"M6 3h9l4 4v14H6z M15 3v4h4","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[73,1616,1620],{"x":1617,"y":1618,"style":1619},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Compare .xlsx bytes",[85,1622],{"x1":1623,"y1":1624,"x2":1625,"y2":1624,"stroke":1606,"style":1626},"36.0","91.0","355.0","stroke-width:1px",[73,1628,1632],{"x":1629,"y":1630,"style":1631},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","− save time differs every run",[73,1634,1636],{"x":1629,"y":1635,"style":1631},"137.0","− failure says only 'files differ'",[73,1638,1640],{"x":1629,"y":1639,"style":1631},"163.0","− breaks on library upgrades",[73,1642,1644],{"x":1629,"y":1643,"style":1631},"189.0","reproducible with XlsxWriter",[62,1646,1648],{"transform":1647},"translate(38.5,177.5) scale(0.625)",[66,1649],{"d":162,"fill":69,"stroke":184,"style":1650},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[44,1652],{"x":1653,"y":54,"width":1603,"height":1604,"rx":404,"fill":183,"stroke":184,"style":60},"389.0",[62,1655,1657],{"transform":1656},"translate(551.5,29.0) scale(1.083)",[66,1658],{"d":103,"fill":69,"stroke":191,"style":1614},[73,1660,1663],{"x":1661,"y":1618,"style":1662},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Compare a text snapshot",[85,1665],{"x1":1666,"y1":1624,"x2":1667,"y2":1624,"stroke":184,"style":1626},"405.0","724.0",[73,1669,1671],{"x":1670,"y":1630,"style":1631},"429.0","stable across saves",[62,1673,1675],{"transform":1674},"translate(407.5,99.5) scale(0.625)",[66,1676],{"d":162,"fill":69,"stroke":184,"style":1650},[73,1678,1679],{"x":1670,"y":1635,"style":1631},"volatile cells masked",[62,1681,1683],{"transform":1682},"translate(407.5,125.5) scale(0.625)",[66,1684],{"d":162,"fill":69,"stroke":184,"style":1650},[73,1686,1687],{"x":1670,"y":1639,"style":1631},"diff names cell and property",[62,1689,1691],{"transform":1690},"translate(407.5,151.5) scale(0.625)",[66,1692],{"d":162,"fill":69,"stroke":184,"style":1650},[73,1694,1695],{"x":1670,"y":1643,"style":1631},"reviewable in a pull request",[62,1697,1699],{"transform":1698},"translate(407.5,177.5) scale(0.625)",[66,1700],{"d":162,"fill":69,"stroke":184,"style":1650},[204,1702,1704],{"id":1703},"step-3-a-pytest-fixture-with-an-update-switch","Step 3 — A pytest fixture with an update switch",[10,1706,1707,1708,1711,1712,1715,1716,1718],{},"The ",[216,1709,1710],{},"golden"," fixture compares a snapshot with ",[216,1713,1714],{},"tests\u002Fgolden\u002F\u003Cname>.txt",". When no golden file exists yet it writes one and fails, so a new snapshot is always looked at before it is trusted. ",[216,1717,196],{}," rewrites the files for intended changes:",[209,1720,1722],{"className":241,"code":1721,"language":243,"meta":214,"style":214},"# conftest.py\nimport difflib\nfrom pathlib import Path\n\nimport pytest\n\nGOLDEN_DIR = Path(__file__).parent \u002F \"tests\" \u002F \"golden\"\n\ndef pytest_addoption(parser):\n    parser.addoption(\"--update-golden\", action=\"store_true\",\n                     help=\"rewrite golden snapshots from the current output\")\n\n@pytest.fixture\ndef golden(request):\n    update = request.config.getoption(\"--update-golden\")\n\n    def check(name, actual):\n        path = GOLDEN_DIR \u002F f\"{name}.txt\"\n        if update or not path.exists():\n            path.parent.mkdir(parents=True, exist_ok=True)\n            path.write_text(actual, encoding=\"utf-8\")\n            if not update:\n                pytest.fail(f\"created {path}; review it, commit it and rerun\")\n            return\n        expected = path.read_text(encoding=\"utf-8\")\n        if actual != expected:\n            diff = \"\".join(difflib.unified_diff(\n                expected.splitlines(keepends=True), actual.splitlines(keepends=True),\n                fromfile=f\"golden\u002F{name}.txt\", tofile=\"current output\"))\n            pytest.fail(f\"{name} differs from its golden snapshot:\\n{diff}\", pytrace=False)\n    return check\n",[216,1723,1724,1729,1736,1748,1752,1758,1762,1791,1795,1805,1826,1838,1842,1847,1857,1871,1875,1886,1912,1927,1950,1965,1975,1996,2001,2019,2031,2044,2068,2101,2141],{"__ignoreMap":214},[219,1725,1726],{"class":85,"line":221},[219,1727,1728],{"class":250},"# conftest.py\n",[219,1730,1731,1733],{"class":85,"line":254},[219,1732,258],{"class":257},[219,1734,1735],{"class":261}," difflib\n",[219,1737,1738,1740,1743,1745],{"class":85,"line":271},[219,1739,281],{"class":257},[219,1741,1742],{"class":261}," pathlib ",[219,1744,258],{"class":257},[219,1746,1747],{"class":261}," Path\n",[219,1749,1750],{"class":85,"line":278},[219,1751,275],{"emptyLinePlaceholder":274},[219,1753,1754,1756],{"class":85,"line":292},[219,1755,258],{"class":257},[219,1757,235],{"class":261},[219,1759,1760],{"class":85,"line":305},[219,1761,275],{"emptyLinePlaceholder":274},[219,1763,1764,1767,1770,1773,1776,1779,1782,1785,1788],{"class":85,"line":310},[219,1765,1766],{"class":326},"GOLDEN_DIR",[219,1768,1769],{"class":257}," =",[219,1771,1772],{"class":261}," Path(",[219,1774,1775],{"class":326},"__file__",[219,1777,1778],{"class":261},").parent ",[219,1780,1781],{"class":257},"\u002F",[219,1783,1784],{"class":228}," \"tests\"",[219,1786,1787],{"class":257}," \u002F",[219,1789,1790],{"class":228}," \"golden\"\n",[219,1792,1793],{"class":85,"line":333},[219,1794,275],{"emptyLinePlaceholder":274},[219,1796,1797,1799,1802],{"class":85,"line":344},[219,1798,313],{"class":257},[219,1800,1801],{"class":316}," pytest_addoption",[219,1803,1804],{"class":261},"(parser):\n",[219,1806,1807,1810,1813,1815,1818,1820,1823],{"class":85,"line":355},[219,1808,1809],{"class":261},"    parser.addoption(",[219,1811,1812],{"class":228},"\"--update-golden\"",[219,1814,407],{"class":261},[219,1816,1817],{"class":398},"action",[219,1819,323],{"class":257},[219,1821,1822],{"class":228},"\"store_true\"",[219,1824,1825],{"class":261},",\n",[219,1827,1828,1831,1833,1836],{"class":85,"line":366},[219,1829,1830],{"class":398},"                     help",[219,1832,323],{"class":257},[219,1834,1835],{"class":228},"\"rewrite golden snapshots from the current output\"",[219,1837,418],{"class":261},[219,1839,1840],{"class":85,"line":383},[219,1841,275],{"emptyLinePlaceholder":274},[219,1843,1844],{"class":85,"line":421},[219,1845,1846],{"class":316},"@pytest.fixture\n",[219,1848,1849,1851,1854],{"class":85,"line":442},[219,1850,313],{"class":257},[219,1852,1853],{"class":316}," golden",[219,1855,1856],{"class":261},"(request):\n",[219,1858,1859,1862,1864,1867,1869],{"class":85,"line":448},[219,1860,1861],{"class":261},"    update ",[219,1863,323],{"class":257},[219,1865,1866],{"class":261}," request.config.getoption(",[219,1868,1812],{"class":228},[219,1870,418],{"class":261},[219,1872,1873],{"class":85,"line":470},[219,1874,275],{"emptyLinePlaceholder":274},[219,1876,1877,1880,1883],{"class":85,"line":491},[219,1878,1879],{"class":257},"    def",[219,1881,1882],{"class":316}," check",[219,1884,1885],{"class":261},"(name, actual):\n",[219,1887,1888,1891,1893,1896,1898,1900,1902,1904,1907,1909],{"class":85,"line":509},[219,1889,1890],{"class":261},"        path ",[219,1892,323],{"class":257},[219,1894,1895],{"class":326}," GOLDEN_DIR",[219,1897,1787],{"class":257},[219,1899,1321],{"class":257},[219,1901,913],{"class":228},[219,1903,617],{"class":616},[219,1905,1906],{"class":261},"name",[219,1908,629],{"class":616},[219,1910,1911],{"class":228},".txt\"\n",[219,1913,1914,1917,1920,1922,1924],{"class":85,"line":535},[219,1915,1916],{"class":257},"        if",[219,1918,1919],{"class":261}," update ",[219,1921,436],{"class":257},[219,1923,1304],{"class":257},[219,1925,1926],{"class":261}," path.exists():\n",[219,1928,1929,1932,1935,1937,1939,1941,1944,1946,1948],{"class":85,"line":548},[219,1930,1931],{"class":261},"            path.parent.mkdir(",[219,1933,1934],{"class":398},"parents",[219,1936,323],{"class":257},[219,1938,415],{"class":326},[219,1940,407],{"class":261},[219,1942,1943],{"class":398},"exist_ok",[219,1945,323],{"class":257},[219,1947,415],{"class":326},[219,1949,418],{"class":261},[219,1951,1952,1955,1958,1960,1963],{"class":85,"line":554},[219,1953,1954],{"class":261},"            path.write_text(actual, ",[219,1956,1957],{"class":398},"encoding",[219,1959,323],{"class":257},[219,1961,1962],{"class":228},"\"utf-8\"",[219,1964,418],{"class":261},[219,1966,1967,1970,1972],{"class":85,"line":571},[219,1968,1969],{"class":257},"            if",[219,1971,1304],{"class":257},[219,1973,1974],{"class":261}," update:\n",[219,1976,1977,1980,1982,1985,1987,1989,1991,1994],{"class":85,"line":600},[219,1978,1979],{"class":261},"                pytest.fail(",[219,1981,610],{"class":257},[219,1983,1984],{"class":228},"\"created ",[219,1986,617],{"class":616},[219,1988,66],{"class":261},[219,1990,629],{"class":616},[219,1992,1993],{"class":228},"; review it, commit it and rerun\"",[219,1995,418],{"class":261},[219,1997,1998],{"class":85,"line":637},[219,1999,2000],{"class":257},"            return\n",[219,2002,2003,2006,2008,2011,2013,2015,2017],{"class":85,"line":665},[219,2004,2005],{"class":261},"        expected ",[219,2007,323],{"class":257},[219,2009,2010],{"class":261}," path.read_text(",[219,2012,1957],{"class":398},[219,2014,323],{"class":257},[219,2016,1962],{"class":228},[219,2018,418],{"class":261},[219,2020,2021,2023,2026,2028],{"class":85,"line":705},[219,2022,1916],{"class":257},[219,2024,2025],{"class":261}," actual ",[219,2027,977],{"class":257},[219,2029,2030],{"class":261}," expected:\n",[219,2032,2033,2036,2038,2041],{"class":85,"line":721},[219,2034,2035],{"class":261},"            diff ",[219,2037,323],{"class":257},[219,2039,2040],{"class":228}," \"\"",[219,2042,2043],{"class":261},".join(difflib.unified_diff(\n",[219,2045,2046,2049,2052,2054,2056,2059,2061,2063,2065],{"class":85,"line":738},[219,2047,2048],{"class":261},"                expected.splitlines(",[219,2050,2051],{"class":398},"keepends",[219,2053,323],{"class":257},[219,2055,415],{"class":326},[219,2057,2058],{"class":261},"), actual.splitlines(",[219,2060,2051],{"class":398},[219,2062,323],{"class":257},[219,2064,415],{"class":326},[219,2066,2067],{"class":261},"),\n",[219,2069,2070,2073,2075,2077,2080,2082,2084,2086,2089,2091,2094,2096,2099],{"class":85,"line":749},[219,2071,2072],{"class":398},"                fromfile",[219,2074,323],{"class":257},[219,2076,610],{"class":257},[219,2078,2079],{"class":228},"\"golden\u002F",[219,2081,617],{"class":616},[219,2083,1906],{"class":261},[219,2085,629],{"class":616},[219,2087,2088],{"class":228},".txt\"",[219,2090,407],{"class":261},[219,2092,2093],{"class":398},"tofile",[219,2095,323],{"class":257},[219,2097,2098],{"class":228},"\"current output\"",[219,2100,1531],{"class":261},[219,2102,2103,2106,2108,2110,2112,2114,2116,2119,2122,2125,2127,2129,2131,2134,2136,2139],{"class":85,"line":1388},[219,2104,2105],{"class":261},"            pytest.fail(",[219,2107,610],{"class":257},[219,2109,913],{"class":228},[219,2111,617],{"class":616},[219,2113,1906],{"class":261},[219,2115,629],{"class":616},[219,2117,2118],{"class":228}," differs from its golden snapshot:",[219,2120,2121],{"class":616},"\\n{",[219,2123,2124],{"class":261},"diff",[219,2126,629],{"class":616},[219,2128,913],{"class":228},[219,2130,407],{"class":261},[219,2132,2133],{"class":398},"pytrace",[219,2135,323],{"class":257},[219,2137,2138],{"class":326},"False",[219,2140,418],{"class":261},[219,2142,2143,2145],{"class":85,"line":1411},[219,2144,1052],{"class":257},[219,2146,2147],{"class":261}," check\n",[209,2149,2151],{"className":241,"code":2150,"language":243,"meta":214,"style":214},"# test_report.py\nimport datetime as dt\n\nfrom report import build_report\nfrom snapshot import snapshot\n\nROWS = [(\"North\", 120, 18240.5), (\"South\", 95, 14195.0), (\"East\", 60, 9730.25)]\n\ndef test_summary_report_matches_golden(tmp_path, golden):\n    out = tmp_path \u002F \"summary.xlsx\"\n    build_report(ROWS, out, generated=dt.datetime(2026, 9, 23, 7, 30))\n    golden(\"summary_report\", snapshot(out, mask={\"Summary!D1\"}))\n",[216,2152,2153,2158,2168,2172,2184,2196,2200,2245,2249,2259,2274,2310],{"__ignoreMap":214},[219,2154,2155],{"class":85,"line":221},[219,2156,2157],{"class":250},"# test_report.py\n",[219,2159,2160,2162,2164,2166],{"class":85,"line":254},[219,2161,258],{"class":257},[219,2163,262],{"class":261},[219,2165,265],{"class":257},[219,2167,268],{"class":261},[219,2169,2170],{"class":85,"line":271},[219,2171,275],{"emptyLinePlaceholder":274},[219,2173,2174,2176,2179,2181],{"class":85,"line":278},[219,2175,281],{"class":257},[219,2177,2178],{"class":261}," report ",[219,2180,258],{"class":257},[219,2182,2183],{"class":261}," build_report\n",[219,2185,2186,2188,2191,2193],{"class":85,"line":292},[219,2187,281],{"class":257},[219,2189,2190],{"class":261}," snapshot ",[219,2192,258],{"class":257},[219,2194,2195],{"class":261}," snapshot\n",[219,2197,2198],{"class":85,"line":305},[219,2199,275],{"emptyLinePlaceholder":274},[219,2201,2202,2205,2207,2209,2211,2213,2215,2217,2219,2221,2223,2225,2227,2229,2231,2233,2235,2237,2239,2241,2243],{"class":85,"line":310},[219,2203,2204],{"class":326},"ROWS",[219,2206,1769],{"class":257},[219,2208,1440],{"class":261},[219,2210,1443],{"class":228},[219,2212,407],{"class":261},[219,2214,1448],{"class":326},[219,2216,407],{"class":261},[219,2218,1453],{"class":326},[219,2220,1456],{"class":261},[219,2222,1459],{"class":228},[219,2224,407],{"class":261},[219,2226,1464],{"class":326},[219,2228,407],{"class":261},[219,2230,1469],{"class":326},[219,2232,1456],{"class":261},[219,2234,1474],{"class":228},[219,2236,407],{"class":261},[219,2238,1479],{"class":326},[219,2240,407],{"class":261},[219,2242,1484],{"class":326},[219,2244,1487],{"class":261},[219,2246,2247],{"class":85,"line":333},[219,2248,275],{"emptyLinePlaceholder":274},[219,2250,2251,2253,2256],{"class":85,"line":344},[219,2252,313],{"class":257},[219,2254,2255],{"class":316}," test_summary_report_matches_golden",[219,2257,2258],{"class":261},"(tmp_path, golden):\n",[219,2260,2261,2264,2266,2269,2271],{"class":85,"line":355},[219,2262,2263],{"class":261},"    out ",[219,2265,323],{"class":257},[219,2267,2268],{"class":261}," tmp_path ",[219,2270,1781],{"class":257},[219,2272,2273],{"class":228}," \"summary.xlsx\"\n",[219,2275,2276,2279,2281,2284,2286,2288,2290,2292,2294,2296,2298,2300,2302,2304,2306,2308],{"class":85,"line":366},[219,2277,2278],{"class":261},"    build_report(",[219,2280,2204],{"class":326},[219,2282,2283],{"class":261},", out, ",[219,2285,1501],{"class":398},[219,2287,323],{"class":257},[219,2289,1506],{"class":261},[219,2291,810],{"class":326},[219,2293,407],{"class":261},[219,2295,1513],{"class":326},[219,2297,407],{"class":261},[219,2299,1518],{"class":326},[219,2301,407],{"class":261},[219,2303,1523],{"class":326},[219,2305,407],{"class":261},[219,2307,1528],{"class":326},[219,2309,1531],{"class":261},[219,2311,2312,2315,2318,2321,2323,2325,2327,2329],{"class":85,"line":383},[219,2313,2314],{"class":261},"    golden(",[219,2316,2317],{"class":228},"\"summary_report\"",[219,2319,2320],{"class":261},", snapshot(out, ",[219,2322,1547],{"class":398},[219,2324,323],{"class":257},[219,2326,617],{"class":261},[219,2328,1554],{"class":228},[219,2330,1557],{"class":261},[10,2332,2333,2334,2336,2337,2340,2341,2343],{},"Passing a fixed ",[216,2335,1501],{}," time ",[14,2338,2339],{},"and"," masking ",[216,2342,1574],{}," is belt and braces: the fixed time keeps the report deterministic, and the mask keeps the test stable if someone later drops the parameter.",[204,2345,2347],{"id":2346},"step-4-read-the-diff-when-something-changes","Step 4 — Read the diff when something changes",[10,2349,2350,2351,2354,2355,2358],{},"To see what a regression looks like, the header's bold was removed and the revenue format changed from ",[216,2352,2353],{},"#,##0.00"," to ",[216,2356,2357],{},"#,##0",". The test failed with:",[209,2360,2363],{"className":2361,"code":2362,"language":73,"meta":214},[1561],"--- golden\u002Fsummary_report.txt\n+++ current output\n-A3 'Region' bold fill=FFDDEBF7\n+A3 'Region' fill=FFDDEBF7\n…\n-C4 18240.5 fmt=#,##0.00\n+C4 18240.5 fmt=#,##0\n",[216,2364,2362],{"__ignoreMap":214},[10,2366,2367,2368,2371,2372,2375],{},"Every changed line names the cell and the property. If the change is a bug, fix the code. If it is intended — the new format was requested — run ",[216,2369,2370],{},"pytest --update-golden",", check ",[216,2373,2374],{},"git diff tests\u002Fgolden\u002F",", and commit the snapshot with the code change so the reviewer sees both. That review step is what turns snapshots from \"tests that always pass after updating\" into a record of every visible change to the report.",[25,2377,34,2382,34,2385,34,2388,34,2391,34,2395,34,2399,34,2402,34,2406,34,2410,34,2413,34,2416,34,2425,34,2430,34,2435,34,2439,34,2443,34,2445,34,2448,34,2452,34,2455,34,2458,34,2460,34,2463,34,2465,34,2468,34,2472,34,2475,34,2478,34,2481],{"viewBox":2378,"role":28,"ariaLabelledBy":2379,"xmlns":32,"style":33},"4 0 752 238",[2380,2381],"gd-decide-t","gd-decide-d",[36,2383,2384],{"id":2380},"The golden test failed. Now what?",[40,2386,2387],{"id":2381},"If the diff shows a change nobody intended, it is a regression and the code needs fixing. If it shows an intended change, update the golden file and commit it with the code so it is reviewed. If it shows only noise such as a timestamp or floating-point digits, mask the cell or tighten the rounding in the snapshot.",[44,2389],{"x":46,"y":47,"width":48,"height":2390,"fill":50},"238",[44,2392],{"x":2393,"y":54,"width":2394,"height":107,"rx":57,"fill":58,"stroke":59,"style":60},"240.0","280.0",[62,2396,2397],{"transform":131},[66,2398],{"d":134,"fill":69,"stroke":70,"style":71},[73,2400,2401],{"x":138,"y":107,"style":77},"What does the diff show?",[85,2403],{"x1":138,"y1":2404,"x2":138,"y2":2405,"stroke":59,"style":60},"91.5","111.5",[85,2407],{"x1":2408,"y1":2405,"x2":2409,"y2":2405,"stroke":59,"style":60},"134.7","625.3",[85,2411],{"x1":2408,"y1":2405,"x2":2408,"y2":2412,"stroke":59,"style":60},"157.5",[91,2414],{"points":2415,"fill":94},"134.7,161.5 129.7,152.5 139.7,152.5",[44,2417],{"x":2418,"y":2419,"width":2420,"height":2421,"rx":2422,"fill":50,"stroke":2423,"style":2424},"59.8","121.5","149.6","21.0","10","var(--line,#cdd5e6)","stroke-width:1.2px",[73,2426,2429],{"x":2408,"y":2427,"style":2428},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","an unintended change",[44,2431],{"x":53,"y":2432,"width":2433,"height":2434,"rx":57,"fill":1605,"stroke":1606,"style":60},"163.5","229.3","59.0",[73,2436,2438],{"x":2408,"y":1643,"style":2437},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","fix the code",[73,2440,2442],{"x":2408,"y":2441,"style":82},"208.5","the test did its job",[85,2444],{"x1":138,"y1":2405,"x2":138,"y2":2412,"stroke":59,"style":60},[91,2446],{"points":2447,"fill":94},"380.0,161.5 375.0,152.5 385.0,152.5",[44,2449],{"x":2450,"y":2419,"width":2451,"height":2421,"rx":2422,"fill":50,"stroke":2423,"style":2424},"312.7","134.6",[73,2453,2454],{"x":138,"y":2427,"style":2428},"an intended change",[44,2456],{"x":2457,"y":2432,"width":2433,"height":2434,"rx":57,"fill":183,"stroke":184,"style":60},"265.3",[73,2459,196],{"x":138,"y":1643,"style":195},[73,2461,2462],{"x":138,"y":2441,"style":82},"commit and review the diff",[85,2464],{"x1":2409,"y1":2405,"x2":2409,"y2":2412,"stroke":59,"style":60},[91,2466],{"points":2467,"fill":94},"625.3,161.5 620.3,152.5 630.3,152.5",[44,2469],{"x":2470,"y":2419,"width":2471,"height":2421,"rx":2422,"fill":50,"stroke":2423,"style":2424},"600.3","50.2",[73,2473,2474],{"x":2409,"y":2427,"style":2428},"noise",[44,2476],{"x":2477,"y":2432,"width":2433,"height":2434,"rx":57,"fill":127,"stroke":128,"style":60},"510.7",[73,2479,2480],{"x":2409,"y":1643,"style":139},"mask or round",[73,2482,2483],{"x":2409,"y":2441,"style":82},"in snapshot()",[204,2485,2487],{"id":2486},"common-pitfalls","Common pitfalls",[2489,2490,2491,2506,2512,2522],"ul",{},[2492,2493,2494,2498,2499,2501,2502,2505],"li",{},[2495,2496,2497],"strong",{},"Updating without reading."," ",[216,2500,196],{}," makes any failure pass. Always read the resulting ",[216,2503,2504],{},"git diff"," before committing.",[2492,2507,2508,2511],{},[2495,2509,2510],{},"Snapshots that are too big."," A 10,000-row report makes a 10,000-line snapshot that nobody reviews. Snapshot a small, representative input, and test large inputs with targeted assertions.",[2492,2513,2514,2517,2518,2521],{},[2495,2515,2516],{},"Library upgrades."," A new pandas or openpyxl version can change defaults — pandas versions differ, for example, in whether ",[216,2519,2520],{},"to_excel"," makes the header bold. Expect snapshot diffs after upgrades and review them like any other change.",[2492,2523,2524,2527],{},[2495,2525,2526],{},"Platform differences."," Keep the snapshot free of absolute paths and locale-dependent formatting, or tests pass locally and fail in CI.",[204,2529,2531],{"id":2530},"conclusion","Conclusion",[10,2533,2534,2535,2537],{},"Compare generated workbooks through a text snapshot, not their bytes: one line per cell with value, number format, bold and fill, floats rounded, volatile cells masked. A pytest fixture compares it with a committed golden file, fails with a unified diff that names each changed cell and property, and rewrites the golden file only on ",[216,2536,196],{}," — after which the diff is reviewed with the code.",[204,2539,2541],{"id":2540},"frequently-asked-questions","Frequently asked questions",[10,2543,2544,2547],{},[2495,2545,2546],{},"Why can't I compare a generated .xlsx file with an expected file byte for byte?","\nopenpyxl writes the save time into docProps\u002Fcore.xml on every save, so two runs of the same code never produce identical bytes. Compare a description of the contents instead — values, formats and styles — which stays the same when the report does.",[10,2549,2550,2553],{},[2495,2551,2552],{},"What is a golden file test?","\nA test that compares the program's output with an approved copy stored in the repository. When the output changes, the test fails with a diff; if the change was intended, the golden copy is regenerated and the diff is reviewed like any other code change.",[10,2555,2556,2559,2560],{},[2495,2557,2558],{},"How do I handle cells that change every run, like a generated-at timestamp?","\nReplace them with a placeholder such as ",[2561,2562,2563],"masked",{}," in the snapshot, or pass a fixed value into the report function during tests. Masking keeps the rest of the sheet under test while ignoring the one cell that legitimately varies.",[10,2565,2566,2569],{},[2495,2567,2568],{},"Can XlsxWriter produce identical files on every run?","\nYes, if the creation date is fixed with workbook.set_properties({\"created\": ...}). In a test, two builds a second apart were then byte-identical. A text snapshot is still easier to review, because a byte difference does not say what changed.",[204,2571,2573],{"id":2572},"related","Related",[2489,2575,2576,2582,2589,2596],{},[2492,2577,2578,2579,2581],{},"Up one level: ",[19,2580,22],{"href":21}," — making report code dependable.",[2492,2583,2584,2588],{},[19,2585,2587],{"href":2586},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — targeted assertions for the numbers that matter most.",[2492,2590,2591,2595],{},[19,2592,2594],{"href":2593},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory\u002F","Create Excel Test Fixtures in Memory for pytest"," — small, representative inputs for snapshot tests.",[2492,2597,2598,2602],{},[19,2599,2601],{"href":2600},"\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"," — the same idea applied to data files.",[2604,2605,2606],"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}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":214,"searchDepth":254,"depth":254,"links":2608},[2609,2610,2611,2612,2613,2614,2615,2616,2617],{"id":206,"depth":254,"text":207},{"id":755,"depth":254,"text":756},{"id":830,"depth":254,"text":831},{"id":1703,"depth":254,"text":1704},{"id":2346,"depth":254,"text":2347},{"id":2486,"depth":254,"text":2487},{"id":2530,"depth":254,"text":2531},{"id":2540,"depth":254,"text":2541},{"id":2572,"depth":254,"text":2573},"2026-09-23","Test a whole generated workbook at once by comparing it with an approved \"golden\" snapshot — not the .xlsx bytes, which change on every save, but a readable text description of every cell's value, number format and style. Includes masking volatile cells, a clear diff when something changes and a --update-golden option for intended changes.","md",[2622,2624,2626,2628],{"q":2546,"a":2623},"openpyxl writes the save time into docProps\u002Fcore.xml on every save, so two runs of the same code never produce identical bytes. Compare a description of the contents instead — values, formats and styles — which stays the same when the report does.",{"q":2552,"a":2625},"A test that compares the program's output with an approved copy stored in the repository. When the output changes, the test fails with a diff; if the change was intended, the golden copy is regenerated and the diff is reviewed like any other code change.",{"q":2558,"a":2627},"Replace them with a placeholder such as \u003Cmasked> in the snapshot, or pass a fixed value into the report function during tests. Masking keeps the rest of the sheet under test while ignoring the one cell that legitimately varies.",{"q":2568,"a":2629},"Yes, if the creation date is fixed with workbook.set_properties({\"created\": ...}). In a test, two builds a second apart were then byte-identical. A text snapshot is still easier to review, because a byte difference does not say what changed.","compare excel output to a golden file in tests",null,{"breadcrumb":2633},[2634,2636,2639],{"name":2635,"item":1781},"Home",{"name":2637,"item":2638},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests",{"title":2642,"description":2643},"Golden File (Snapshot) Tests for Excel Output in pytest","Snapshot-test Excel reports in Python — why comparing xlsx bytes fails, a line-per-cell text snapshot with values, formats, bold and fills, masking timestamps, unified diffs in pytest failures, a --update-golden pytest option, reviewing snapshot diffs in code review and XlsxWriter's reproducible output.","compare-excel-output-to-a-golden-file-in-tests","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests\u002Findex",[2647,2648],"reliable-jobs","openpyxl","how-to","FKZnu9n34RqNthqWDGLAomhZhVg1JVZlJYr_T1zXCck",[2652,2656],{"title":2653,"path":2654,"stem":2655,"children":-1},"Build a Command-Line Tool for Excel Reports with argparse","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse\u002Findex",{"title":2594,"path":2657,"stem":2658,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory\u002Findex",[2660,2662],{"path":2661,"title":2637},"\u002Fautomating-reporting-workflows",{"path":2663,"title":22},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2666,2670,2673,2676,2679,2682],{"path":2667,"title":2668,"kind":2669},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":2671,"title":2672,"kind":2649},"\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":2674,"title":2675,"kind":2649},"\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":2677,"title":2678,"kind":2649},"\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":2680,"title":2681,"kind":2649},"\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":2683,"title":2684,"kind":2649},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972140650]