[{"data":1,"prerenderedAt":2050},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl":2041},{"id":4,"title":5,"body":6,"dateModified":2016,"datePublished":2016,"description":2017,"extension":2018,"faq":2019,"meta":2032,"navigation":244,"path":2033,"seo":2034,"slug":2037,"stem":2038,"type":2039,"__hash__":2040},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002Findex.md","Work with Macro-Enabled .xlsm Files in openpyxl",{"type":7,"value":8,"toc":2004},"minimark",[9,19,176,181,206,209,213,216,360,367,370,663,667,672,805,825,898,902,905,1182,1189,1193,1296,1303,1406,1409,1511,1519,1523,1635,1639,1648,1663,1669,1863,1866,1870,1893,1897,1909,1930,1941,1947,1959,1963,2000],[10,11,12,13,18],"p",{},"Macro-enabled workbooks are common in finance and operations: a template with buttons, a refresh routine, some validation VBA — and a monthly data drop that somebody currently pastes in by hand. Automating that paste with openpyxl works, but there is one trap that catches nearly everyone the first time. Load the file, change a cell, save it, and the macros are gone. No exception, no warning, just a workbook whose buttons no longer do anything. This guide shows the two-part rule that prevents it, what openpyxl still cannot preserve, and how to verify before you ship. It is part of ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[20,21,30,31,30,35,30,39,30,46,30,56,30,63,30,72,30,78,30,83,30,85,30,89,30,92,30,95,30,99,30,103,30,105,30,108,30,111,30,118,30,123,30,128,30,134,30,139,30,146,30,151,30,154,30,157,30,161,30,167,30,172],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 268","img","Anatomy of an xlsm file as a zip archive: worksheets, styles and shared strings alongside the vbaProject binary part, with the vba part shown as the piece a default openpyxl save drops.",[26,27],"xlsm-anat-t","xlsm-anat-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"What is inside an .xlsm, and which part gets dropped",[36,37,38],"desc",{"id":27},"An xlsm file is a zip archive. Inside it are the workbook definition, the worksheet XML parts, the shared strings table, the styles part, and a vbaProject dot bin binary holding the macros. openpyxl parses and rewrites the four XML parts on every save. The vbaProject part is opaque to it and is only carried through when keep_vba is set to True.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","268","#ffffff",[40,47],{"x":48,"y":49,"width":50,"height":51,"rx":52,"fill":53,"stroke":54,"style":55},"20","34","470","216","16","#f0f2f5","var(--line,#cdd5e6)","stroke-width:2px",[57,58,62],"text",{"x":59,"y":60,"style":61},"255","60","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","report.xlsm — a zip archive",[40,64],{"x":65,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":55},"42","76","200","46","9","#ebebfd","var(--brand,#5b5cf0)",[57,73,77],{"x":74,"y":75,"style":76},"142","97","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","xl\u002Fworkbook.xml",[57,79,82],{"x":74,"y":80,"style":81},"113","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","sheets, names",[40,84],{"x":44,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":55},[57,86,88],{"x":87,"y":75,"style":76},"368","xl\u002Fworksheets\u002F",[57,90,91],{"x":87,"y":80,"style":81},"the cell data",[40,93],{"x":65,"y":94,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":55},"132",[57,96,98],{"x":74,"y":97,"style":76},"153","xl\u002Fstyles.xml",[57,100,102],{"x":74,"y":101,"style":81},"169","fonts, fills, formats",[40,104],{"x":44,"y":94,"width":67,"height":68,"rx":69,"fill":70,"stroke":71,"style":55},[57,106,107],{"x":87,"y":97,"style":76},"sharedStrings.xml",[57,109,110],{"x":87,"y":101,"style":81},"deduplicated text",[40,112],{"x":65,"y":113,"width":114,"height":115,"rx":69,"fill":116,"stroke":117,"style":55},"188","426","48","#fdefd8","var(--gold,#b4740a)",[57,119,122],{"x":59,"y":120,"style":121},"210","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xl\u002FvbaProject.bin",[57,124,127],{"x":59,"y":125,"style":126},"227","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","the macros — opaque binary, not XML",[129,130],"line",{"x1":131,"y1":132,"x2":133,"y2":132,"stroke":71,"style":55},"490","99","536",[135,136],"polygon",{"points":137,"fill":138},"544,99 532,93 532,105","#5b5cf0",[40,140],{"x":141,"y":66,"width":142,"height":68,"rx":143,"fill":144,"stroke":145,"style":55},"552","232","10","#d9f4f1","var(--teal,#0f9488)",[57,147,150],{"x":148,"y":75,"style":149},"668","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","parsed and rewritten",[57,152,153],{"x":148,"y":80,"style":81},"on every wb.save()",[129,155],{"x1":131,"y1":156,"x2":133,"y2":156,"stroke":117,"style":55},"212",[135,158],{"points":159,"fill":160},"544,212 532,206 532,218","#b4740a",[40,162],{"x":141,"y":163,"width":142,"height":164,"rx":143,"fill":165,"stroke":166,"style":55},"186","52","#fee8f2","var(--accent,#f43f8f)",[57,168,171],{"x":148,"y":169,"style":170},"208","font-size:11.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","carried through only if",[57,173,175],{"x":148,"y":174,"style":170},"226","keep_vba=True",[177,178,180],"h2",{"id":179},"prerequisites","Prerequisites",[182,183,188],"pre",{"className":184,"code":185,"language":186,"meta":187,"style":187},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[189,190,191],"code",{"__ignoreMap":187},[192,193,195,199,203],"span",{"class":129,"line":194},1,[192,196,198],{"class":197},"sMTad","pip",[192,200,202],{"class":201},"srMev"," install",[192,204,205],{"class":201}," openpyxl\n",[10,207,208],{},"You also need a macro-enabled workbook. To make one for testing, open Excel, record any trivial macro (Developer → Record Macro), and save as \"Excel Macro-Enabled Workbook (*.xlsm)\". You cannot create the VBA project from Python — openpyxl can carry one but not author one.",[177,210,212],{"id":211},"step-1-the-two-part-rule","Step 1 — The two-part rule",[10,214,215],{},"Both halves are required. Missing either one loses the macros:",[182,217,221],{"className":218,"code":219,"language":220,"meta":187,"style":187},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\n# 1. keep_vba=True — hold the vbaProject.bin part in memory.\nwb = load_workbook(\"template.xlsm\", keep_vba=True)\n\nws = wb[\"Data\"]\nws[\"B2\"] = 4821\nws[\"B3\"] = \"2026-08-15\"\n\n# 2. Save to an .xlsm path. An .xlsx path drops the VBA even with keep_vba.\nwb.save(\"template_filled.xlsm\")\n","python",[189,222,223,239,246,253,284,289,306,323,338,343,349],{"__ignoreMap":187},[192,224,225,229,233,236],{"class":129,"line":194},[192,226,228],{"class":227},"s-kum","from",[192,230,232],{"class":231},"skGVy"," openpyxl ",[192,234,235],{"class":227},"import",[192,237,238],{"class":231}," load_workbook\n",[192,240,242],{"class":129,"line":241},2,[192,243,245],{"emptyLinePlaceholder":244},true,"\n",[192,247,249],{"class":129,"line":248},3,[192,250,252],{"class":251},"s-wDw","# 1. keep_vba=True — hold the vbaProject.bin part in memory.\n",[192,254,256,259,262,265,268,271,275,277,281],{"class":129,"line":255},4,[192,257,258],{"class":231},"wb ",[192,260,261],{"class":227},"=",[192,263,264],{"class":231}," load_workbook(",[192,266,267],{"class":201},"\"template.xlsm\"",[192,269,270],{"class":231},", ",[192,272,274],{"class":273},"sa561","keep_vba",[192,276,261],{"class":227},[192,278,280],{"class":279},"sP0c6","True",[192,282,283],{"class":231},")\n",[192,285,287],{"class":129,"line":286},5,[192,288,245],{"emptyLinePlaceholder":244},[192,290,292,295,297,300,303],{"class":129,"line":291},6,[192,293,294],{"class":231},"ws ",[192,296,261],{"class":227},[192,298,299],{"class":231}," wb[",[192,301,302],{"class":201},"\"Data\"",[192,304,305],{"class":231},"]\n",[192,307,309,312,315,318,320],{"class":129,"line":308},7,[192,310,311],{"class":231},"ws[",[192,313,314],{"class":201},"\"B2\"",[192,316,317],{"class":231},"] ",[192,319,261],{"class":227},[192,321,322],{"class":279}," 4821\n",[192,324,326,328,331,333,335],{"class":129,"line":325},8,[192,327,311],{"class":231},[192,329,330],{"class":201},"\"B3\"",[192,332,317],{"class":231},[192,334,261],{"class":227},[192,336,337],{"class":201}," \"2026-08-15\"\n",[192,339,341],{"class":129,"line":340},9,[192,342,245],{"emptyLinePlaceholder":244},[192,344,346],{"class":129,"line":345},10,[192,347,348],{"class":251},"# 2. Save to an .xlsm path. An .xlsx path drops the VBA even with keep_vba.\n",[192,350,352,355,358],{"class":129,"line":351},11,[192,353,354],{"class":231},"wb.save(",[192,356,357],{"class":201},"\"template_filled.xlsm\"",[192,359,283],{"class":231},[10,361,362,363,366],{},"The reason the filename matters is structural, not stylistic. openpyxl picks the output content type from the extension, and the ",[189,364,365],{},".xlsx"," content type has no slot for a VBA part — so the part is simply not written. There is no error because, from the format's point of view, nothing is wrong.",[10,368,369],{},"Guard against it in code rather than trusting yourself to remember:",[182,371,373],{"className":218,"code":372,"language":220,"meta":187,"style":187},"from pathlib import Path\nfrom openpyxl import load_workbook\n\ndef edit_macro_workbook(src, dest, mutate):\n    \"\"\"Open a macro-enabled workbook, apply `mutate(wb)`, save it back safely.\"\"\"\n    src, dest = Path(src), Path(dest)\n    if src.suffix.lower() != \".xlsm\":\n        raise ValueError(f\"{src.name} is not macro-enabled\")\n    if dest.suffix.lower() != \".xlsm\":\n        raise ValueError(\n            f\"Refusing to write {dest.name}: saving a macro workbook to \"\n            f\"'{dest.suffix}' would discard the VBA project.\"\n        )\n\n    wb = load_workbook(src, keep_vba=True)\n    mutate(wb)\n    wb.save(dest)\n    return dest\n\nedit_macro_workbook(\n    \"template.xlsm\",\n    \"august.xlsm\",\n    lambda wb: wb[\"Data\"].cell(row=2, column=2, value=4821),\n)\n",[189,374,375,387,397,401,413,418,428,445,477,490,499,517,535,541,546,565,571,577,586,591,597,606,614,658],{"__ignoreMap":187},[192,376,377,379,382,384],{"class":129,"line":194},[192,378,228],{"class":227},[192,380,381],{"class":231}," pathlib ",[192,383,235],{"class":227},[192,385,386],{"class":231}," Path\n",[192,388,389,391,393,395],{"class":129,"line":241},[192,390,228],{"class":227},[192,392,232],{"class":231},[192,394,235],{"class":227},[192,396,238],{"class":231},[192,398,399],{"class":129,"line":248},[192,400,245],{"emptyLinePlaceholder":244},[192,402,403,406,410],{"class":129,"line":255},[192,404,405],{"class":227},"def",[192,407,409],{"class":408},"s_Opv"," edit_macro_workbook",[192,411,412],{"class":231},"(src, dest, mutate):\n",[192,414,415],{"class":129,"line":286},[192,416,417],{"class":201},"    \"\"\"Open a macro-enabled workbook, apply `mutate(wb)`, save it back safely.\"\"\"\n",[192,419,420,423,425],{"class":129,"line":291},[192,421,422],{"class":231},"    src, dest ",[192,424,261],{"class":227},[192,426,427],{"class":231}," Path(src), Path(dest)\n",[192,429,430,433,436,439,442],{"class":129,"line":308},[192,431,432],{"class":227},"    if",[192,434,435],{"class":231}," src.suffix.lower() ",[192,437,438],{"class":227},"!=",[192,440,441],{"class":201}," \".xlsm\"",[192,443,444],{"class":231},":\n",[192,446,447,450,453,456,459,462,466,469,472,475],{"class":129,"line":325},[192,448,449],{"class":227},"        raise",[192,451,452],{"class":279}," ValueError",[192,454,455],{"class":231},"(",[192,457,458],{"class":227},"f",[192,460,461],{"class":201},"\"",[192,463,465],{"class":464},"sSjpA","{",[192,467,468],{"class":231},"src.name",[192,470,471],{"class":464},"}",[192,473,474],{"class":201}," is not macro-enabled\"",[192,476,283],{"class":231},[192,478,479,481,484,486,488],{"class":129,"line":340},[192,480,432],{"class":227},[192,482,483],{"class":231}," dest.suffix.lower() ",[192,485,438],{"class":227},[192,487,441],{"class":201},[192,489,444],{"class":231},[192,491,492,494,496],{"class":129,"line":345},[192,493,449],{"class":227},[192,495,452],{"class":279},[192,497,498],{"class":231},"(\n",[192,500,501,504,507,509,512,514],{"class":129,"line":351},[192,502,503],{"class":227},"            f",[192,505,506],{"class":201},"\"Refusing to write ",[192,508,465],{"class":464},[192,510,511],{"class":231},"dest.name",[192,513,471],{"class":464},[192,515,516],{"class":201},": saving a macro workbook to \"\n",[192,518,520,522,525,527,530,532],{"class":129,"line":519},12,[192,521,503],{"class":227},[192,523,524],{"class":201},"\"'",[192,526,465],{"class":464},[192,528,529],{"class":231},"dest.suffix",[192,531,471],{"class":464},[192,533,534],{"class":201},"' would discard the VBA project.\"\n",[192,536,538],{"class":129,"line":537},13,[192,539,540],{"class":231},"        )\n",[192,542,544],{"class":129,"line":543},14,[192,545,245],{"emptyLinePlaceholder":244},[192,547,549,552,554,557,559,561,563],{"class":129,"line":548},15,[192,550,551],{"class":231},"    wb ",[192,553,261],{"class":227},[192,555,556],{"class":231}," load_workbook(src, ",[192,558,274],{"class":273},[192,560,261],{"class":227},[192,562,280],{"class":279},[192,564,283],{"class":231},[192,566,568],{"class":129,"line":567},16,[192,569,570],{"class":231},"    mutate(wb)\n",[192,572,574],{"class":129,"line":573},17,[192,575,576],{"class":231},"    wb.save(dest)\n",[192,578,580,583],{"class":129,"line":579},18,[192,581,582],{"class":227},"    return",[192,584,585],{"class":231}," dest\n",[192,587,589],{"class":129,"line":588},19,[192,590,245],{"emptyLinePlaceholder":244},[192,592,594],{"class":129,"line":593},20,[192,595,596],{"class":231},"edit_macro_workbook(\n",[192,598,600,603],{"class":129,"line":599},21,[192,601,602],{"class":201},"    \"template.xlsm\"",[192,604,605],{"class":231},",\n",[192,607,609,612],{"class":129,"line":608},22,[192,610,611],{"class":201},"    \"august.xlsm\"",[192,613,605],{"class":231},[192,615,617,620,623,625,628,631,633,636,638,641,643,645,647,650,652,655],{"class":129,"line":616},23,[192,618,619],{"class":227},"    lambda",[192,621,622],{"class":231}," wb: wb[",[192,624,302],{"class":201},[192,626,627],{"class":231},"].cell(",[192,629,630],{"class":273},"row",[192,632,261],{"class":227},[192,634,635],{"class":279},"2",[192,637,270],{"class":231},[192,639,640],{"class":273},"column",[192,642,261],{"class":227},[192,644,635],{"class":279},[192,646,270],{"class":231},[192,648,649],{"class":273},"value",[192,651,261],{"class":227},[192,653,654],{"class":279},"4821",[192,656,657],{"class":231},"),\n",[192,659,661],{"class":129,"line":660},24,[192,662,283],{"class":231},[177,664,666],{"id":665},"step-2-know-what-openpyxl-does-not-preserve","Step 2 — Know what openpyxl does not preserve",[10,668,669,671],{},[189,670,274],{}," covers the VBA project. It does not make openpyxl a lossless round-tripper for everything else. Anything openpyxl cannot model is rebuilt from its own understanding of the file, and what it does not understand is dropped.",[673,674,675,691],"table",{},[676,677,678],"thead",{},[679,680,681,685,688],"tr",{},[682,683,684],"th",{},"Feature",[682,686,687],{},"Survives a round trip?",[682,689,690],{},"Notes",[692,693,694,708,722,734,746,757,768,777,786,795],"tbody",{},[679,695,696,700,705],{},[697,698,699],"td",{},"VBA macros",[697,701,702,703],{},"Yes, with ",[189,704,175],{},[697,706,707],{},"Carried as opaque bytes.",[679,709,710,713,719],{},[697,711,712],{},"Form controls, ActiveX buttons",[697,714,715],{},[716,717,718],"strong",{},"No",[697,720,721],{},"The buttons that call the macros disappear.",[679,723,724,727,731],{},[697,725,726],{},"Charts in the source file",[697,728,729],{},[716,730,718],{},[697,732,733],{},"openpyxl drops charts it did not create itself.",[679,735,736,739,743],{},[697,737,738],{},"Images already in the file",[697,740,741],{},[716,742,718],{},[697,744,745],{},"Re-add them after loading if needed.",[679,747,748,751,754],{},[697,749,750],{},"Pivot tables",[697,752,753],{},"Partially",[697,755,756],{},"Definition kept; the cache may not refresh.",[679,758,759,762,765],{},[697,760,761],{},"Conditional formatting",[697,763,764],{},"Yes",[697,766,767],{},"Read and rewritten.",[679,769,770,773,775],{},[697,771,772],{},"Data validation",[697,774,764],{},[697,776,767],{},[679,778,779,782,784],{},[697,780,781],{},"Cell styles, number formats",[697,783,764],{},[697,785,767],{},[679,787,788,791,793],{},[697,789,790],{},"Comments",[697,792,764],{},[697,794,767],{},[679,796,797,800,802],{},[697,798,799],{},"Defined names",[697,801,764],{},[697,803,804],{},"Important — macros often reference them.",[10,806,807,808,812,813,816,817,820,821,18],{},"That third and fourth row are the ones that surprise people: a template with a \"Refresh\" button loses the ",[809,810,811],"em",{},"button"," while keeping the ",[809,814,815],{},"macro it called",". If your template has controls or charts, the safe pattern is to treat the workbook as ",[716,818,819],{},"fill-only"," — write values into cells and never restructure — which is exactly the discipline described in ",[14,822,824],{"href":823},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","populating an Excel template without losing formatting",[20,826,30,833,30,836,30,839,30,843,30,849,30,854,30,859,30,863,30,867,30,871,30,875,30,878,30,883,30,886,30,889,30,892,30,895],{"viewBox":827,"role":23,"ariaLabel":828,"ariaLabelledBy":829,"xmlns":28,"style":832},"0 0 780 224","Safe versus unsafe operations on a macro template: writing cell values and setting defined-name targets are safe, while adding or deleting sheets, renaming sheets and inserting rows can break the macros that reference them.",[830,831],"safe-t","safe-d","width:100%;max-width:780px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[32,834,835],{"id":830},"Which edits a macro template tolerates",[36,837,838],{"id":831},"Two columns. Safe edits, on the left, are writing values into existing cells, updating number formats, and filling a range a defined name already points at. Risky edits, on the right, are renaming a sheet, deleting a sheet, inserting or deleting rows and columns, and removing a defined name — each of which can leave VBA code referring to something that no longer exists.",[40,840],{"x":42,"y":42,"width":841,"height":842,"fill":45},"780","224",[40,844],{"x":52,"y":845,"width":846,"height":847,"rx":848,"fill":144,"stroke":145,"style":55},"18","366","192","14",[57,850,853],{"x":851,"y":68,"style":852},"199","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","safe — the template still works",[57,855,858],{"x":851,"y":856,"style":857},"78","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","write values into existing cells",[57,860,862],{"x":851,"y":861,"style":857},"104","change number formats and styles",[57,864,866],{"x":851,"y":865,"style":857},"130","fill a range a defined name covers",[57,868,870],{"x":851,"y":869,"style":857},"156","add rows below the last used row",[57,872,874],{"x":851,"y":873,"style":857},"182","update data validation lists",[40,876],{"x":877,"y":845,"width":846,"height":847,"rx":848,"fill":165,"stroke":166,"style":55},"398",[57,879,882],{"x":880,"y":68,"style":881},"581","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","risky — VBA may break",[57,884,885],{"x":880,"y":856,"style":857},"rename or reorder sheets",[57,887,888],{"x":880,"y":861,"style":857},"delete a sheet the macro names",[57,890,891],{"x":880,"y":865,"style":857},"insert or delete rows and columns",[57,893,894],{"x":880,"y":869,"style":857},"remove or move a defined name",[57,896,897],{"x":880,"y":873,"style":857},"rebuild a sheet from scratch",[177,899,901],{"id":900},"step-3-write-through-defined-names-not-coordinates","Step 3 — Write through defined names, not coordinates",[10,903,904],{},"Macros usually address ranges by name, not by cell reference, precisely so a template can be reorganised without breaking. Writing through the same names makes your Python code equally robust — and it fails loudly if somebody removes one.",[182,906,908],{"className":218,"code":907,"language":220,"meta":187,"style":187},"from openpyxl import load_workbook\nfrom openpyxl.utils import range_boundaries\n\ndef write_named(wb, name, value):\n    \"\"\"Set the value of a single-cell defined name.\"\"\"\n    if name not in wb.defined_names:\n        raise KeyError(f\"Defined name '{name}' is missing from the template\")\n\n    destinations = list(wb.defined_names[name].destinations)\n    if len(destinations) != 1:\n        raise ValueError(f\"'{name}' does not resolve to one range\")\n\n    sheet_title, ref = destinations[0]\n    min_col, min_row, *_ = range_boundaries(ref.replace(\"$\", \"\"))\n    wb[sheet_title].cell(row=min_row, column=min_col, value=value)\n\nwb = load_workbook(\"template.xlsm\", keep_vba=True)\nwrite_named(wb, \"ReportMonth\", \"2026-08\")\nwrite_named(wb, \"TotalUnits\", 4821)\nwb.save(\"august.xlsm\")\n",[189,909,910,920,932,936,946,951,967,993,997,1010,1027,1050,1054,1068,1095,1121,1125,1145,1160,1173],{"__ignoreMap":187},[192,911,912,914,916,918],{"class":129,"line":194},[192,913,228],{"class":227},[192,915,232],{"class":231},[192,917,235],{"class":227},[192,919,238],{"class":231},[192,921,922,924,927,929],{"class":129,"line":241},[192,923,228],{"class":227},[192,925,926],{"class":231}," openpyxl.utils ",[192,928,235],{"class":227},[192,930,931],{"class":231}," range_boundaries\n",[192,933,934],{"class":129,"line":248},[192,935,245],{"emptyLinePlaceholder":244},[192,937,938,940,943],{"class":129,"line":255},[192,939,405],{"class":227},[192,941,942],{"class":408}," write_named",[192,944,945],{"class":231},"(wb, name, value):\n",[192,947,948],{"class":129,"line":286},[192,949,950],{"class":201},"    \"\"\"Set the value of a single-cell defined name.\"\"\"\n",[192,952,953,955,958,961,964],{"class":129,"line":291},[192,954,432],{"class":227},[192,956,957],{"class":231}," name ",[192,959,960],{"class":227},"not",[192,962,963],{"class":227}," in",[192,965,966],{"class":231}," wb.defined_names:\n",[192,968,969,971,974,976,978,981,983,986,988,991],{"class":129,"line":308},[192,970,449],{"class":227},[192,972,973],{"class":279}," KeyError",[192,975,455],{"class":231},[192,977,458],{"class":227},[192,979,980],{"class":201},"\"Defined name '",[192,982,465],{"class":464},[192,984,985],{"class":231},"name",[192,987,471],{"class":464},[192,989,990],{"class":201},"' is missing from the template\"",[192,992,283],{"class":231},[192,994,995],{"class":129,"line":325},[192,996,245],{"emptyLinePlaceholder":244},[192,998,999,1002,1004,1007],{"class":129,"line":340},[192,1000,1001],{"class":231},"    destinations ",[192,1003,261],{"class":227},[192,1005,1006],{"class":279}," list",[192,1008,1009],{"class":231},"(wb.defined_names[name].destinations)\n",[192,1011,1012,1014,1017,1020,1022,1025],{"class":129,"line":345},[192,1013,432],{"class":227},[192,1015,1016],{"class":279}," len",[192,1018,1019],{"class":231},"(destinations) ",[192,1021,438],{"class":227},[192,1023,1024],{"class":279}," 1",[192,1026,444],{"class":231},[192,1028,1029,1031,1033,1035,1037,1039,1041,1043,1045,1048],{"class":129,"line":351},[192,1030,449],{"class":227},[192,1032,452],{"class":279},[192,1034,455],{"class":231},[192,1036,458],{"class":227},[192,1038,524],{"class":201},[192,1040,465],{"class":464},[192,1042,985],{"class":231},[192,1044,471],{"class":464},[192,1046,1047],{"class":201},"' does not resolve to one range\"",[192,1049,283],{"class":231},[192,1051,1052],{"class":129,"line":519},[192,1053,245],{"emptyLinePlaceholder":244},[192,1055,1056,1059,1061,1064,1066],{"class":129,"line":537},[192,1057,1058],{"class":231},"    sheet_title, ref ",[192,1060,261],{"class":227},[192,1062,1063],{"class":231}," destinations[",[192,1065,42],{"class":279},[192,1067,305],{"class":231},[192,1069,1070,1073,1076,1079,1081,1084,1087,1089,1092],{"class":129,"line":543},[192,1071,1072],{"class":231},"    min_col, min_row, ",[192,1074,1075],{"class":227},"*",[192,1077,1078],{"class":231},"_ ",[192,1080,261],{"class":227},[192,1082,1083],{"class":231}," range_boundaries(ref.replace(",[192,1085,1086],{"class":201},"\"$\"",[192,1088,270],{"class":231},[192,1090,1091],{"class":201},"\"\"",[192,1093,1094],{"class":231},"))\n",[192,1096,1097,1100,1102,1104,1107,1109,1111,1114,1116,1118],{"class":129,"line":548},[192,1098,1099],{"class":231},"    wb[sheet_title].cell(",[192,1101,630],{"class":273},[192,1103,261],{"class":227},[192,1105,1106],{"class":231},"min_row, ",[192,1108,640],{"class":273},[192,1110,261],{"class":227},[192,1112,1113],{"class":231},"min_col, ",[192,1115,649],{"class":273},[192,1117,261],{"class":227},[192,1119,1120],{"class":231},"value)\n",[192,1122,1123],{"class":129,"line":567},[192,1124,245],{"emptyLinePlaceholder":244},[192,1126,1127,1129,1131,1133,1135,1137,1139,1141,1143],{"class":129,"line":573},[192,1128,258],{"class":231},[192,1130,261],{"class":227},[192,1132,264],{"class":231},[192,1134,267],{"class":201},[192,1136,270],{"class":231},[192,1138,274],{"class":273},[192,1140,261],{"class":227},[192,1142,280],{"class":279},[192,1144,283],{"class":231},[192,1146,1147,1150,1153,1155,1158],{"class":129,"line":579},[192,1148,1149],{"class":231},"write_named(wb, ",[192,1151,1152],{"class":201},"\"ReportMonth\"",[192,1154,270],{"class":231},[192,1156,1157],{"class":201},"\"2026-08\"",[192,1159,283],{"class":231},[192,1161,1162,1164,1167,1169,1171],{"class":129,"line":588},[192,1163,1149],{"class":231},[192,1165,1166],{"class":201},"\"TotalUnits\"",[192,1168,270],{"class":231},[192,1170,654],{"class":279},[192,1172,283],{"class":231},[192,1174,1175,1177,1180],{"class":129,"line":593},[192,1176,354],{"class":231},[192,1178,1179],{"class":201},"\"august.xlsm\"",[192,1181,283],{"class":231},[10,1183,1184,1185,18],{},"Creating and inspecting names is covered in ",[14,1186,1188],{"href":1187},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fcreate-named-range-in-excel-with-openpyxl\u002F","creating a named range in Excel with openpyxl",[177,1190,1192],{"id":1191},"step-4-verify-the-macros-survived","Step 4 — Verify the macros survived",[20,1194,30,1200,30,1203,30,1206,30,1208,30,1213,30,1218,30,1222,30,1226,30,1229,30,1233,30,1238,30,1242,30,1246,30,1249,30,1261,30,1265,30,1269,30,1275,30,1280,30,1284,30,1287,30,1292],{"viewBox":1195,"role":23,"ariaLabel":1196,"ariaLabelledBy":1197,"xmlns":28,"style":29},"0 0 800 216","Verification gate in a pipeline: after the script writes the workbook, a zip inspection confirms the vbaProject part is present and its digest matches the source before the file is released to users.",[1198,1199],"verify-t","verify-d",[32,1201,1202],{"id":1198},"A macro-survival gate between writing and shipping",[36,1204,1205],{"id":1199},"The script writes the filled workbook, then a verification step opens it as a zip archive and checks two things: that the vbaProject binary part exists, and that its SHA-256 digest matches the digest taken from the source template. Only if both hold does the file move on to delivery. A mismatch or a missing part fails the job loudly instead of shipping a workbook whose buttons do nothing.",[40,1207],{"x":42,"y":42,"width":43,"height":51,"fill":45},[40,1209],{"x":848,"y":66,"width":1210,"height":1211,"rx":1212,"fill":70,"stroke":71,"style":55},"146","66","12",[57,1214,1217],{"x":1215,"y":861,"style":1216},"87","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","wb.save()",[57,1219,1221],{"x":1215,"y":1220,"style":126},"124","august.xlsm",[129,1223],{"x1":1224,"y1":1225,"x2":67,"y2":1225,"stroke":71,"style":55},"160","109",[135,1227],{"points":1228,"fill":138},"208,109 196,103 196,115",[40,1230],{"x":51,"y":164,"width":51,"height":1231,"rx":1232,"fill":116,"stroke":117,"style":55},"114","13",[57,1234,1237],{"x":1235,"y":856,"style":1236},"324","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","open as a zip",[57,1239,1241],{"x":1235,"y":861,"style":1240},"font-size:11px;fill:var(--text,#172033);text-anchor:middle","is xl\u002FvbaProject.bin",[57,1243,1245],{"x":1235,"y":1244,"style":1240},"122","present?",[57,1247,1248],{"x":1235,"y":1210,"style":1240},"does its digest match?",[1250,1251,1253,1254,1253,1258,30],"g",{"stroke":54,"style":55,"fill":1252},"none","\n    ",[1255,1256],"path",{"d":1257},"M432 88 H 476 V 56 H 512",[1255,1259],{"d":1260},"M432 130 H 476 V 162 H 512",[135,1262],{"points":1263,"fill":1264},"520,56 508,50 508,62","#0f9488",[135,1266],{"points":1267,"fill":1268},"520,162 508,156 508,168","#f43f8f",[40,1270],{"x":1271,"y":1272,"width":1273,"height":1274,"rx":1212,"fill":144,"stroke":145,"style":55},"528","30","256","54",[57,1276,1279],{"x":1277,"y":164,"style":1278},"656","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","both hold → deliver",[57,1281,1283],{"x":1277,"y":1282,"style":126},"71","email, upload, archive",[40,1285],{"x":1271,"y":1286,"width":1273,"height":1274,"rx":1212,"fill":165,"stroke":166,"style":55},"136",[57,1288,1291],{"x":1277,"y":1289,"style":1290},"158","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","either fails → raise",[57,1293,1295],{"x":1277,"y":1294,"style":126},"177","never ship a dead template",[10,1297,1298,1299,1302],{},"Do not ship on faith. Because ",[189,1300,1301],{},".xlsm"," is a zip, you can confirm the VBA part is present in two lines — no Excel required, which means this check runs in CI on Linux:",[182,1304,1306],{"className":218,"code":1305,"language":220,"meta":187,"style":187},"import zipfile\n\ndef has_macros(path):\n    \"\"\"True if the workbook still contains a VBA project.\"\"\"\n    with zipfile.ZipFile(path) as zf:\n        return \"xl\u002FvbaProject.bin\" in zf.namelist()\n\nassert has_macros(\"template.xlsm\"), \"source has no macros — check the fixture\"\nassert has_macros(\"august.xlsm\"), \"macros were dropped during the write\"\nprint(\"VBA project intact\")\n",[189,1307,1308,1315,1319,1329,1334,1348,1361,1365,1381,1394],{"__ignoreMap":187},[192,1309,1310,1312],{"class":129,"line":194},[192,1311,235],{"class":227},[192,1313,1314],{"class":231}," zipfile\n",[192,1316,1317],{"class":129,"line":241},[192,1318,245],{"emptyLinePlaceholder":244},[192,1320,1321,1323,1326],{"class":129,"line":248},[192,1322,405],{"class":227},[192,1324,1325],{"class":408}," has_macros",[192,1327,1328],{"class":231},"(path):\n",[192,1330,1331],{"class":129,"line":255},[192,1332,1333],{"class":201},"    \"\"\"True if the workbook still contains a VBA project.\"\"\"\n",[192,1335,1336,1339,1342,1345],{"class":129,"line":286},[192,1337,1338],{"class":227},"    with",[192,1340,1341],{"class":231}," zipfile.ZipFile(path) ",[192,1343,1344],{"class":227},"as",[192,1346,1347],{"class":231}," zf:\n",[192,1349,1350,1353,1356,1358],{"class":129,"line":291},[192,1351,1352],{"class":227},"        return",[192,1354,1355],{"class":201}," \"xl\u002FvbaProject.bin\"",[192,1357,963],{"class":227},[192,1359,1360],{"class":231}," zf.namelist()\n",[192,1362,1363],{"class":129,"line":308},[192,1364,245],{"emptyLinePlaceholder":244},[192,1366,1367,1370,1373,1375,1378],{"class":129,"line":325},[192,1368,1369],{"class":227},"assert",[192,1371,1372],{"class":231}," has_macros(",[192,1374,267],{"class":201},[192,1376,1377],{"class":231},"), ",[192,1379,1380],{"class":201},"\"source has no macros — check the fixture\"\n",[192,1382,1383,1385,1387,1389,1391],{"class":129,"line":340},[192,1384,1369],{"class":227},[192,1386,1372],{"class":231},[192,1388,1179],{"class":201},[192,1390,1377],{"class":231},[192,1392,1393],{"class":201},"\"macros were dropped during the write\"\n",[192,1395,1396,1399,1401,1404],{"class":129,"line":345},[192,1397,1398],{"class":279},"print",[192,1400,455],{"class":231},[192,1402,1403],{"class":201},"\"VBA project intact\"",[192,1405,283],{"class":231},[10,1407,1408],{},"A stricter version compares the bytes, catching a corrupted carry-through as well as a missing one:",[182,1410,1412],{"className":218,"code":1411,"language":220,"meta":187,"style":187},"import zipfile, hashlib\n\ndef vba_digest(path):\n    with zipfile.ZipFile(path) as zf:\n        if \"xl\u002FvbaProject.bin\" not in zf.namelist():\n            return None\n        return hashlib.sha256(zf.read(\"xl\u002FvbaProject.bin\")).hexdigest()\n\nassert vba_digest(\"template.xlsm\") == vba_digest(\"august.xlsm\"), \\\n    \"the VBA project changed during the round trip\"\n",[189,1413,1414,1421,1425,1434,1444,1459,1467,1480,1484,1506],{"__ignoreMap":187},[192,1415,1416,1418],{"class":129,"line":194},[192,1417,235],{"class":227},[192,1419,1420],{"class":231}," zipfile, hashlib\n",[192,1422,1423],{"class":129,"line":241},[192,1424,245],{"emptyLinePlaceholder":244},[192,1426,1427,1429,1432],{"class":129,"line":248},[192,1428,405],{"class":227},[192,1430,1431],{"class":408}," vba_digest",[192,1433,1328],{"class":231},[192,1435,1436,1438,1440,1442],{"class":129,"line":255},[192,1437,1338],{"class":227},[192,1439,1341],{"class":231},[192,1441,1344],{"class":227},[192,1443,1347],{"class":231},[192,1445,1446,1449,1451,1454,1456],{"class":129,"line":286},[192,1447,1448],{"class":227},"        if",[192,1450,1355],{"class":201},[192,1452,1453],{"class":227}," not",[192,1455,963],{"class":227},[192,1457,1458],{"class":231}," zf.namelist():\n",[192,1460,1461,1464],{"class":129,"line":291},[192,1462,1463],{"class":227},"            return",[192,1465,1466],{"class":279}," None\n",[192,1468,1469,1471,1474,1477],{"class":129,"line":308},[192,1470,1352],{"class":227},[192,1472,1473],{"class":231}," hashlib.sha256(zf.read(",[192,1475,1476],{"class":201},"\"xl\u002FvbaProject.bin\"",[192,1478,1479],{"class":231},")).hexdigest()\n",[192,1481,1482],{"class":129,"line":325},[192,1483,245],{"emptyLinePlaceholder":244},[192,1485,1486,1488,1491,1493,1496,1499,1501,1503],{"class":129,"line":340},[192,1487,1369],{"class":227},[192,1489,1490],{"class":231}," vba_digest(",[192,1492,267],{"class":201},[192,1494,1495],{"class":231},") ",[192,1497,1498],{"class":227},"==",[192,1500,1490],{"class":231},[192,1502,1179],{"class":201},[192,1504,1505],{"class":231},"), \\\n",[192,1507,1508],{"class":129,"line":345},[192,1509,1510],{"class":201},"    \"the VBA project changed during the round trip\"\n",[10,1512,1513,1514,1518],{},"Wire that into a test alongside the output assertions in ",[14,1515,1517],{"href":1516},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","testing Excel output with pytest"," and a regression can never reach users silently.",[177,1520,1522],{"id":1521},"common-pitfalls-and-fixes","Common pitfalls and fixes",[673,1524,1525,1538],{},[676,1526,1527],{},[679,1528,1529,1532,1535],{},[682,1530,1531],{},"Symptom",[682,1533,1534],{},"Cause",[682,1536,1537],{},"Fix",[692,1539,1540,1555,1573,1584,1602,1613,1624],{},[679,1541,1542,1545,1550],{},[697,1543,1544],{},"Macros gone, no error",[697,1546,1547,1549],{},[189,1548,274],{}," omitted",[697,1551,1552,1553,18],{},"Load with ",[189,1554,175],{},[679,1556,1557,1562,1568],{},[697,1558,1559,1560],{},"Macros gone despite ",[189,1561,274],{},[697,1563,1564,1565,1567],{},"Saved to an ",[189,1566,365],{}," filename",[697,1569,1570,1571,18],{},"Save to ",[189,1572,1301],{},[679,1574,1575,1578,1581],{},[697,1576,1577],{},"Buttons vanished, macros present",[697,1579,1580],{},"openpyxl drops form and ActiveX controls",[697,1582,1583],{},"Keep the controls in a template you only fill; do not rebuild the sheet.",[679,1585,1586,1589,1599],{},[697,1587,1588],{},"Excel says the file is corrupt",[697,1590,1591,1592,1594,1595,1598],{},"Mixing ",[189,1593,274],{}," with ",[189,1596,1597],{},"write_only"," mode",[697,1600,1601],{},"The two are incompatible; use normal mode for macro workbooks.",[679,1603,1604,1607,1610],{},[697,1605,1606],{},"Macro errors on \"subscript out of range\"",[697,1608,1609],{},"A sheet the VBA names was renamed or deleted",[697,1611,1612],{},"Restore the original sheet titles.",[679,1614,1615,1618,1621],{},[697,1616,1617],{},"Chart disappeared",[697,1619,1620],{},"Chart existed in the source file",[697,1622,1623],{},"Re-create it with openpyxl after loading, or use the LibreOffice route.",[679,1625,1626,1629,1632],{},[697,1627,1628],{},"Digital signature warning",[697,1630,1631],{},"Any edit invalidates the signature",[697,1633,1634],{},"Re-sign in Excel after the automated step, or drop signing.",[177,1636,1638],{"id":1637},"performance-and-scale-notes","Performance and scale notes",[10,1640,1641,1643,1644,1647],{},[189,1642,175],{}," costs almost nothing at run time. The VBA part is read as bytes and written back as bytes with no parsing, so the overhead is roughly the size of ",[189,1645,1646],{},"vbaProject.bin"," in memory — typically tens of kilobytes even for substantial macro projects.",[10,1649,1650,1651,1654,1655,1657,1658,1662],{},"The real cost is that macro workbooks force you into openpyxl's ",[716,1652,1653],{},"normal"," mode. The ",[189,1656,1597],{}," streaming mode, which is what makes ",[14,1659,1661],{"href":1660},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fwrite-large-dataframes-to-excel-with-write-only-mode\u002F","writing large DataFrames memory-efficient",", cannot carry a VBA project — it builds a new workbook from nothing. So a macro template holding hundreds of thousands of rows will be memory-hungry to edit.",[10,1664,1665,1666,1668],{},"The way out is to split the roles. Keep the macro workbook small — the template, the controls, the summary sheet — and put bulk data in a separate ",[189,1667,365],{}," the template links to or the macro imports:",[182,1670,1672],{"className":218,"code":1671,"language":220,"meta":187,"style":187},"import pandas as pd\nfrom openpyxl import load_workbook\n\n# Bulk data goes to a plain .xlsx, written the fast way.\nbig = pd.DataFrame({\"id\": range(500_000), \"amount\": range(500_000)})\nbig.to_excel(\"detail.xlsx\", index=False, engine=\"xlsxwriter\")\n\n# The macro template only receives the summary figures.\nwb = load_workbook(\"dashboard.xlsm\", keep_vba=True)\nws = wb[\"Summary\"]\nws[\"B2\"] = len(big)\nws[\"B3\"] = float(big[\"amount\"].sum())\nwb.save(\"dashboard_august.xlsm\")\n",[189,1673,1674,1686,1696,1700,1705,1745,1775,1779,1784,1805,1818,1833,1854],{"__ignoreMap":187},[192,1675,1676,1678,1681,1683],{"class":129,"line":194},[192,1677,235],{"class":227},[192,1679,1680],{"class":231}," pandas ",[192,1682,1344],{"class":227},[192,1684,1685],{"class":231}," pd\n",[192,1687,1688,1690,1692,1694],{"class":129,"line":241},[192,1689,228],{"class":227},[192,1691,232],{"class":231},[192,1693,235],{"class":227},[192,1695,238],{"class":231},[192,1697,1698],{"class":129,"line":248},[192,1699,245],{"emptyLinePlaceholder":244},[192,1701,1702],{"class":129,"line":255},[192,1703,1704],{"class":251},"# Bulk data goes to a plain .xlsx, written the fast way.\n",[192,1706,1707,1710,1712,1715,1718,1721,1724,1726,1729,1731,1734,1736,1738,1740,1742],{"class":129,"line":286},[192,1708,1709],{"class":231},"big ",[192,1711,261],{"class":227},[192,1713,1714],{"class":231}," pd.DataFrame({",[192,1716,1717],{"class":201},"\"id\"",[192,1719,1720],{"class":231},": ",[192,1722,1723],{"class":279},"range",[192,1725,455],{"class":231},[192,1727,1728],{"class":279},"500_000",[192,1730,1377],{"class":231},[192,1732,1733],{"class":201},"\"amount\"",[192,1735,1720],{"class":231},[192,1737,1723],{"class":279},[192,1739,455],{"class":231},[192,1741,1728],{"class":279},[192,1743,1744],{"class":231},")})\n",[192,1746,1747,1750,1753,1755,1758,1760,1763,1765,1768,1770,1773],{"class":129,"line":291},[192,1748,1749],{"class":231},"big.to_excel(",[192,1751,1752],{"class":201},"\"detail.xlsx\"",[192,1754,270],{"class":231},[192,1756,1757],{"class":273},"index",[192,1759,261],{"class":227},[192,1761,1762],{"class":279},"False",[192,1764,270],{"class":231},[192,1766,1767],{"class":273},"engine",[192,1769,261],{"class":227},[192,1771,1772],{"class":201},"\"xlsxwriter\"",[192,1774,283],{"class":231},[192,1776,1777],{"class":129,"line":308},[192,1778,245],{"emptyLinePlaceholder":244},[192,1780,1781],{"class":129,"line":325},[192,1782,1783],{"class":251},"# The macro template only receives the summary figures.\n",[192,1785,1786,1788,1790,1792,1795,1797,1799,1801,1803],{"class":129,"line":340},[192,1787,258],{"class":231},[192,1789,261],{"class":227},[192,1791,264],{"class":231},[192,1793,1794],{"class":201},"\"dashboard.xlsm\"",[192,1796,270],{"class":231},[192,1798,274],{"class":273},[192,1800,261],{"class":227},[192,1802,280],{"class":279},[192,1804,283],{"class":231},[192,1806,1807,1809,1811,1813,1816],{"class":129,"line":345},[192,1808,294],{"class":231},[192,1810,261],{"class":227},[192,1812,299],{"class":231},[192,1814,1815],{"class":201},"\"Summary\"",[192,1817,305],{"class":231},[192,1819,1820,1822,1824,1826,1828,1830],{"class":129,"line":351},[192,1821,311],{"class":231},[192,1823,314],{"class":201},[192,1825,317],{"class":231},[192,1827,261],{"class":227},[192,1829,1016],{"class":279},[192,1831,1832],{"class":231},"(big)\n",[192,1834,1835,1837,1839,1841,1843,1846,1849,1851],{"class":129,"line":519},[192,1836,311],{"class":231},[192,1838,330],{"class":201},[192,1840,317],{"class":231},[192,1842,261],{"class":227},[192,1844,1845],{"class":279}," float",[192,1847,1848],{"class":231},"(big[",[192,1850,1733],{"class":201},[192,1852,1853],{"class":231},"].sum())\n",[192,1855,1856,1858,1861],{"class":129,"line":537},[192,1857,354],{"class":231},[192,1859,1860],{"class":201},"\"dashboard_august.xlsm\"",[192,1862,283],{"class":231},[10,1864,1865],{},"That split keeps the memory profile flat and keeps the fragile, hand-built part of the workbook untouched by the bulk write.",[177,1867,1869],{"id":1868},"conclusion","Conclusion",[10,1871,1872,1873,1875,1876,1878,1879,1881,1882,1885,1886,1889,1890,1892],{},"Editing ",[189,1874,1301],{}," files from Python is safe once you internalise the two-part rule: ",[189,1877,175],{}," on load, an ",[189,1880,1301],{}," extension on save. Beyond that, treat the workbook as something you ",[809,1883,1884],{},"fill",", not something you ",[809,1887,1888],{},"restructure"," — openpyxl silently drops form controls and pre-existing charts, and VBA that names a sheet you renamed will fail at run time inside Excel. Write through defined names where you can, check for ",[189,1891,122],{}," in the output before shipping, and keep bulk data out of the macro workbook entirely.",[177,1894,1896],{"id":1895},"frequently-asked-questions","Frequently asked questions",[10,1898,1899,1905,1906,1908],{},[716,1900,1901,1902,1904],{},"Does ",[189,1903,175],{}," let me read or edit the macro code?","\nNo. openpyxl treats ",[189,1907,1646],{}," as an opaque blob — it carries the bytes through unchanged but cannot parse, read or modify the VBA source. To change macro code you need Excel itself, or a dedicated VBA tooling library.",[10,1910,1911,1917,1918,1920,1921,1923,1924,1926,1927,1929],{},[716,1912,1913,1914,1916],{},"I set ",[189,1915,175],{}," and the macros are still gone. Why?","\nAlmost always the output filename. Saving to a ",[189,1919,365],{}," path discards the VBA project regardless of ",[189,1922,274],{},", because ",[189,1925,365],{}," has no place to store it. Save to a ",[189,1928,1301],{}," path.",[10,1931,1932,1935,1936,1940],{},[716,1933,1934],{},"Can openpyxl run a macro?","\nNo. openpyxl only reads and writes files; it never starts Excel. To execute a macro you need ",[14,1937,1939],{"href":1938},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example\u002F","xlwings"," driving a real Excel instance on Windows or macOS.",[10,1942,1943,1946],{},[716,1944,1945],{},"Will Excel warn users about the file my script produced?","\nYes, the usual macro security prompt appears, exactly as it would for the original. If the workbook was digitally signed, the signature will no longer match after any edit and the file will be treated as unsigned.",[10,1948,1949,1952,1953,1955,1956,1958],{},[716,1950,1951],{},"Is it safe to run this on a server?","\nYes. openpyxl never executes the VBA, so processing an untrusted ",[189,1954,1301],{}," carries no more risk than processing an ",[189,1957,365],{},". The macros only run when someone opens the file in Excel and allows them.",[177,1960,1962],{"id":1961},"related","Related",[1964,1965,1966,1976,1982,1988,1994],"ul",{},[1967,1968,1969,1970,1972,1973,1975],"li",{},"Up to the parent: ",[14,1971,17],{"href":16}," — where ",[189,1974,1301],{}," sits among the other formats.",[1967,1977,1978,1981],{},[14,1979,1980],{"href":823},"Populate an Excel Template Without Losing Formatting"," — the fill-only discipline this guide recommends.",[1967,1983,1984,1987],{},[14,1985,1986],{"href":1938},"xlwings: Run a Macro from Python"," — actually executing the VBA you preserved.",[1967,1989,1990,1993],{},[14,1991,1992],{"href":1187},"Create a Named Range in Excel with openpyxl"," — the names to write through.",[1967,1995,1996,1999],{},[14,1997,1998],{"href":1516},"Test Excel Output with pytest"," — where the macro-survival assertion belongs.",[2001,2002,2003],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":187,"searchDepth":241,"depth":241,"links":2005},[2006,2007,2008,2009,2010,2011,2012,2013,2014,2015],{"id":179,"depth":241,"text":180},{"id":211,"depth":241,"text":212},{"id":665,"depth":241,"text":666},{"id":900,"depth":241,"text":901},{"id":1191,"depth":241,"text":1192},{"id":1521,"depth":241,"text":1522},{"id":1637,"depth":241,"text":1638},{"id":1868,"depth":241,"text":1869},{"id":1895,"depth":241,"text":1896},{"id":1961,"depth":241,"text":1962},"2026-08-15","Edit .xlsm workbooks from Python without destroying the VBA project: the keep_vba flag, why the filename matters, what openpyxl still drops, and how to verify macros survived.","md",[2020,2023,2026,2028,2030],{"q":2021,"a":2022},"Does keep_vba=True let me read or edit the macro code?","No. openpyxl treats vbaProject.bin as an opaque blob — it carries the bytes through unchanged but cannot parse, read or modify the VBA source. To change macro code you need Excel itself, or a dedicated VBA tooling library.",{"q":2024,"a":2025},"I set keep_vba=True and the macros are still gone. Why?","Almost always the output filename. Saving to a .xlsx path discards the VBA project regardless of keep_vba, because .xlsx has no place to store it. Save to a .xlsm path.",{"q":1934,"a":2027},"No. openpyxl only reads and writes files; it never starts Excel. To execute a macro you need xlwings driving a real Excel instance on Windows or macOS.",{"q":1945,"a":2029},"Yes, the usual macro security prompt appears, exactly as it would for the original. If the workbook was digitally signed, the signature will no longer match after any edit and the file will be treated as unsigned.",{"q":1951,"a":2031},"Yes. openpyxl never executes the VBA, so processing an untrusted .xlsm carries no more risk than processing an .xlsx. The macros only run when someone opens the file in Excel and allows them.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl",{"title":2035,"description":2036},"Edit .xlsm Files in openpyxl Without Losing Macros","How to load, modify and save macro-enabled Excel workbooks in Python — keep_vba=True, the .xlsm filename rule, chart and image caveats, and a verification check.","work-with-macro-enabled-xlsm-files-in-openpyxl","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002Findex","how-to","HVLPWvBsiQunMEyR8nB6UtRp2PRWZmhQ7J3RF5wBCFI",[2042,2046],{"title":2043,"path":2044,"stem":2045,"children":-1},"Read .xls Files in Python with xlrd and pandas","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002Findex",{"title":2047,"path":2048,"stem":2049,"children":-1},"Reading Excel Files with Pandas","\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas","getting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Findex",1786800027102]