[{"data":1,"prerenderedAt":2202},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python":2193},{"id":4,"title":5,"body":6,"dateModified":2167,"datePublished":2167,"description":2168,"extension":2169,"faq":2170,"meta":2184,"navigation":236,"path":2185,"seo":2186,"slug":2189,"stem":2190,"type":2191,"__hash__":2192},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python\u002Findex.md","Fix \"Excel Found Unreadable Content\" After Writing with Python",{"type":7,"value":8,"toc":2153},"minimark",[9,29,165,170,200,203,207,214,513,516,520,523,815,840,844,851,1004,1011,1096,1100,1122,1332,1340,1344,1355,1446,1449,1453,1456,1885,1996,2003,2007,2046,2050,2069,2073,2076,2080,2086,2092,2098,2104,2110,2114,2149],[10,11,12,13,17,18,22,23,28],"p",{},"A script finishes without an error, writes ",[14,15,16],"code",{},"report.xlsx",", and the recipient sees: ",[19,20,21],"em",{},"\"We found a problem with some content in report.xlsx. Do you want us to try to recover as much as we can?\""," Nothing in Python reported a fault, because openpyxl and xlsxwriter validate the XML they produce, not the extra application-level rules Excel enforces on top of it. This guide lists those rules, shows the code that breaks each one, and ends with a pre-flight check that keeps a repair prompt from ever reaching a stakeholder. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[30,31,39,40,39,44,39,48,39,55,39,62,39,71,39,77,39,82,39,86,39,90,39,95,39,103,39,108,39,112,39,117,39,122,39,126,39,130,39,135,39,139,39,143,39,148,39,151,39,154,39,157,39,161],"svg",{"viewBox":32,"role":33,"ariaLabelledBy":34,"xmlns":37,"style":38},"0 0 760 240","img",[35,36],"rep-t","rep-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[41,42,43],"title",{"id":35},"Two layers of validation, and the gap between them",[45,46,47],"desc",{"id":36},"Python libraries check that the XML is well formed, while Excel additionally checks names, references and value types; anything that passes the first check but fails the second produces a repair prompt.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","240","#ffffff",[56,57,61],"text",{"x":58,"y":59,"style":60},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","What each layer actually checks",[49,63],{"x":64,"y":65,"width":66,"height":67,"rx":68,"fill":69,"stroke":70},"34","44","212","150","12","#d9f4f1","var(--line,#cdd5e6)",[56,72,76],{"x":73,"y":74,"style":75},"140","74","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","openpyxl \u002F xlsxwriter",[56,78,81],{"x":73,"y":79,"style":80},"104","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","well-formed XML",[56,83,85],{"x":73,"y":84,"style":80},"128","valid cell coordinates",[56,87,89],{"x":73,"y":88,"style":80},"152","legal characters in text",[56,91,94],{"x":73,"y":92,"style":93},"176","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","raises if broken",[96,97],"line",{"x1":98,"y1":99,"x2":100,"y2":99,"stroke":101,"style":102},"246","119","288","var(--brand,#5b5cf0)","stroke-width:2px",[104,105],"polygon",{"points":106,"fill":107},"288,119 278,114 278,124","#5b5cf0",[49,109],{"x":110,"y":65,"width":92,"height":67,"rx":68,"fill":111,"stroke":70},"292","#fdefd8",[56,113,116],{"x":58,"y":114,"style":115},"82","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","the gap",[56,118,121],{"x":58,"y":119,"style":120},"112","font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","names, references,",[56,123,125],{"x":58,"y":124,"style":120},"136","duplicate tables,",[56,127,129],{"x":58,"y":128,"style":120},"160","unsupported values",[96,131],{"x1":132,"y1":99,"x2":133,"y2":99,"stroke":134,"style":102},"468","510","var(--accent,#f43f8f)",[104,136],{"points":137,"fill":138},"510,119 500,114 500,124","#be185d",[49,140],{"x":141,"y":65,"width":66,"height":67,"rx":68,"fill":142,"stroke":70},"514","#fee8f2",[56,144,147],{"x":145,"y":74,"style":146},"620","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Excel on open",[56,149,150],{"x":145,"y":79,"style":80},"checks every name",[56,152,153],{"x":145,"y":84,"style":80},"resolves every reference",[56,155,156],{"x":145,"y":88,"style":80},"rewrites what it cannot",[56,158,160],{"x":145,"y":92,"style":159},"font-size:11.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","shows the repair prompt",[56,162,164],{"x":58,"y":163,"style":93},"226","Everything in the middle box writes cleanly from Python and fails in Excel",[166,167,169],"h2",{"id":168},"prerequisites","Prerequisites",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl xlsxwriter pandas\n","bash","",[14,178,179],{"__ignoreMap":176},[180,181,183,187,191,194,197],"span",{"class":96,"line":182},1,[180,184,186],{"class":185},"sMTad","pip",[180,188,190],{"class":189},"srMev"," install",[180,192,193],{"class":189}," openpyxl",[180,195,196],{"class":189}," xlsxwriter",[180,198,199],{"class":189}," pandas\n",[10,201,202],{},"The checks at the end use only the standard library, so they run anywhere the report job runs.",[166,204,206],{"id":205},"rule-1-sheet-names","Rule 1: sheet names",[10,208,209,210,213],{},"Excel accepts at most 31 characters and forbids ",[14,211,212],{},"\\ \u002F ? * [ ] :",". A sheet name built from data — a customer, a region, a report title — breaks both rules eventually. openpyxl truncates silently in some versions and writes the name as given in others; xlsxwriter raises. Sanitise before creating the sheet:",[171,215,219],{"className":216,"code":217,"language":218,"meta":176,"style":176},"language-python shiki shiki-themes github-light github-dark-high-contrast","import re\n\nINVALID = re.compile(r\"[\\\\\u002F?*\\[\\]:]\")\n\ndef safe_sheet_name(name: str, used: set[str] | None = None) -> str:\n    \"\"\"A name Excel accepts, unique within the workbook.\"\"\"\n    cleaned = INVALID.sub(\"-\", \" \".join(str(name).split()))[:31] or \"Sheet\"\n    if used is None:\n        return cleaned\n    candidate, n = cleaned, 1\n    while candidate.casefold() in used:\n        suffix = f\"_{n}\"\n        candidate = cleaned[: 31 - len(suffix)] + suffix\n        n += 1\n    used.add(candidate.casefold())\n    return candidate\n","python",[14,220,221,231,238,282,287,329,335,378,394,403,417,432,458,486,498,504],{"__ignoreMap":176},[180,222,223,227],{"class":96,"line":182},[180,224,226],{"class":225},"s-kum","import",[180,228,230],{"class":229},"skGVy"," re\n",[180,232,234],{"class":96,"line":233},2,[180,235,237],{"emptyLinePlaceholder":236},true,"\n",[180,239,241,245,248,251,254,257,260,264,268,271,274,277,279],{"class":96,"line":240},3,[180,242,244],{"class":243},"sP0c6","INVALID",[180,246,247],{"class":225}," =",[180,249,250],{"class":229}," re.compile(",[180,252,253],{"class":225},"r",[180,255,256],{"class":189},"\"",[180,258,259],{"class":243},"[",[180,261,263],{"class":262},"s_b0D","\\\\",[180,265,267],{"class":266},"sSjpA","\u002F?*",[180,269,270],{"class":262},"\\[\\]",[180,272,273],{"class":266},":",[180,275,276],{"class":243},"]",[180,278,256],{"class":189},[180,280,281],{"class":229},")\n",[180,283,285],{"class":96,"line":284},4,[180,286,237],{"emptyLinePlaceholder":236},[180,288,290,293,297,300,303,306,308,311,314,317,319,321,324,326],{"class":96,"line":289},5,[180,291,292],{"class":225},"def",[180,294,296],{"class":295},"s_Opv"," safe_sheet_name",[180,298,299],{"class":229},"(name: ",[180,301,302],{"class":243},"str",[180,304,305],{"class":229},", used: set[",[180,307,302],{"class":243},[180,309,310],{"class":229},"] ",[180,312,313],{"class":225},"|",[180,315,316],{"class":243}," None",[180,318,247],{"class":225},[180,320,316],{"class":243},[180,322,323],{"class":229},") -> ",[180,325,302],{"class":243},[180,327,328],{"class":229},":\n",[180,330,332],{"class":96,"line":331},6,[180,333,334],{"class":189},"    \"\"\"A name Excel accepts, unique within the workbook.\"\"\"\n",[180,336,338,341,344,347,350,353,356,359,362,364,367,370,372,375],{"class":96,"line":337},7,[180,339,340],{"class":229},"    cleaned ",[180,342,343],{"class":225},"=",[180,345,346],{"class":243}," INVALID",[180,348,349],{"class":229},".sub(",[180,351,352],{"class":189},"\"-\"",[180,354,355],{"class":229},", ",[180,357,358],{"class":189},"\" \"",[180,360,361],{"class":229},".join(",[180,363,302],{"class":243},[180,365,366],{"class":229},"(name).split()))[:",[180,368,369],{"class":243},"31",[180,371,310],{"class":229},[180,373,374],{"class":225},"or",[180,376,377],{"class":189}," \"Sheet\"\n",[180,379,381,384,387,390,392],{"class":96,"line":380},8,[180,382,383],{"class":225},"    if",[180,385,386],{"class":229}," used ",[180,388,389],{"class":225},"is",[180,391,316],{"class":243},[180,393,328],{"class":229},[180,395,397,400],{"class":96,"line":396},9,[180,398,399],{"class":225},"        return",[180,401,402],{"class":229}," cleaned\n",[180,404,406,409,411,414],{"class":96,"line":405},10,[180,407,408],{"class":229},"    candidate, n ",[180,410,343],{"class":225},[180,412,413],{"class":229}," cleaned, ",[180,415,416],{"class":243},"1\n",[180,418,420,423,426,429],{"class":96,"line":419},11,[180,421,422],{"class":225},"    while",[180,424,425],{"class":229}," candidate.casefold() ",[180,427,428],{"class":225},"in",[180,430,431],{"class":229}," used:\n",[180,433,435,438,440,443,446,449,452,455],{"class":96,"line":434},12,[180,436,437],{"class":229},"        suffix ",[180,439,343],{"class":225},[180,441,442],{"class":225}," f",[180,444,445],{"class":189},"\"_",[180,447,448],{"class":266},"{",[180,450,451],{"class":229},"n",[180,453,454],{"class":266},"}",[180,456,457],{"class":189},"\"\n",[180,459,461,464,466,469,471,474,477,480,483],{"class":96,"line":460},13,[180,462,463],{"class":229},"        candidate ",[180,465,343],{"class":225},[180,467,468],{"class":229}," cleaned[: ",[180,470,369],{"class":243},[180,472,473],{"class":225}," -",[180,475,476],{"class":243}," len",[180,478,479],{"class":229},"(suffix)] ",[180,481,482],{"class":225},"+",[180,484,485],{"class":229}," suffix\n",[180,487,489,492,495],{"class":96,"line":488},14,[180,490,491],{"class":229},"        n ",[180,493,494],{"class":225},"+=",[180,496,497],{"class":243}," 1\n",[180,499,501],{"class":96,"line":500},15,[180,502,503],{"class":229},"    used.add(candidate.casefold())\n",[180,505,507,510],{"class":96,"line":506},16,[180,508,509],{"class":225},"    return",[180,511,512],{"class":229}," candidate\n",[10,514,515],{},"Uniqueness matters as much as legality: two sheets differing only in case are legal in the file format and illegal in Excel.",[166,517,519],{"id":518},"rule-2-table-names-and-ranges","Rule 2: table names and ranges",[10,521,522],{},"Excel tables are the most common cause of a repair prompt in generated reports, because two rules apply at once. Table names must be unique across the whole workbook — not per sheet — and must not look like a cell reference. A table's range must also contain a header row plus at least one data row that actually exists.",[171,524,526],{"className":216,"code":525,"language":218,"meta":176,"style":176},"\"\"\"One table per sheet, each with a unique, legal name.\"\"\"\nfrom openpyxl import Workbook\nfrom openpyxl.worksheet.table import Table, TableStyleInfo\n\nwb = Workbook()\nwb.remove(wb.active)\nrows = {\"North\": [(\"Widget\", 120)], \"South\": [(\"Widget\", 90)]}\n\nfor i, (region, data) in enumerate(rows.items(), start=1):\n    ws = wb.create_sheet(region)\n    ws.append([\"Product\", \"Units\"])\n    for row in data:\n        ws.append(list(row))\n    table = Table(displayName=f\"tbl_{region}_{i}\", ref=f\"A1:B{ws.max_row}\")\n    table.tableStyleInfo = TableStyleInfo(name=\"TableStyleMedium9\", showRowStripes=True)\n    ws.add_table(table)\n\nwb.save(\"regions.xlsx\")\n",[14,527,528,533,546,558,562,572,577,619,623,651,661,677,690,701,764,794,799,804],{"__ignoreMap":176},[180,529,530],{"class":96,"line":182},[180,531,532],{"class":189},"\"\"\"One table per sheet, each with a unique, legal name.\"\"\"\n",[180,534,535,538,541,543],{"class":96,"line":233},[180,536,537],{"class":225},"from",[180,539,540],{"class":229}," openpyxl ",[180,542,226],{"class":225},[180,544,545],{"class":229}," Workbook\n",[180,547,548,550,553,555],{"class":96,"line":240},[180,549,537],{"class":225},[180,551,552],{"class":229}," openpyxl.worksheet.table ",[180,554,226],{"class":225},[180,556,557],{"class":229}," Table, TableStyleInfo\n",[180,559,560],{"class":96,"line":284},[180,561,237],{"emptyLinePlaceholder":236},[180,563,564,567,569],{"class":96,"line":289},[180,565,566],{"class":229},"wb ",[180,568,343],{"class":225},[180,570,571],{"class":229}," Workbook()\n",[180,573,574],{"class":96,"line":331},[180,575,576],{"class":229},"wb.remove(wb.active)\n",[180,578,579,582,584,587,590,593,596,598,601,604,607,609,611,613,616],{"class":96,"line":337},[180,580,581],{"class":229},"rows ",[180,583,343],{"class":225},[180,585,586],{"class":229}," {",[180,588,589],{"class":189},"\"North\"",[180,591,592],{"class":229},": [(",[180,594,595],{"class":189},"\"Widget\"",[180,597,355],{"class":229},[180,599,600],{"class":243},"120",[180,602,603],{"class":229},")], ",[180,605,606],{"class":189},"\"South\"",[180,608,592],{"class":229},[180,610,595],{"class":189},[180,612,355],{"class":229},[180,614,615],{"class":243},"90",[180,617,618],{"class":229},")]}\n",[180,620,621],{"class":96,"line":380},[180,622,237],{"emptyLinePlaceholder":236},[180,624,625,628,631,633,636,639,643,645,648],{"class":96,"line":396},[180,626,627],{"class":225},"for",[180,629,630],{"class":229}," i, (region, data) ",[180,632,428],{"class":225},[180,634,635],{"class":243}," enumerate",[180,637,638],{"class":229},"(rows.items(), ",[180,640,642],{"class":641},"sa561","start",[180,644,343],{"class":225},[180,646,647],{"class":243},"1",[180,649,650],{"class":229},"):\n",[180,652,653,656,658],{"class":96,"line":405},[180,654,655],{"class":229},"    ws ",[180,657,343],{"class":225},[180,659,660],{"class":229}," wb.create_sheet(region)\n",[180,662,663,666,669,671,674],{"class":96,"line":419},[180,664,665],{"class":229},"    ws.append([",[180,667,668],{"class":189},"\"Product\"",[180,670,355],{"class":229},[180,672,673],{"class":189},"\"Units\"",[180,675,676],{"class":229},"])\n",[180,678,679,682,685,687],{"class":96,"line":434},[180,680,681],{"class":225},"    for",[180,683,684],{"class":229}," row ",[180,686,428],{"class":225},[180,688,689],{"class":229}," data:\n",[180,691,692,695,698],{"class":96,"line":460},[180,693,694],{"class":229},"        ws.append(",[180,696,697],{"class":243},"list",[180,699,700],{"class":229},"(row))\n",[180,702,703,706,708,711,714,716,719,722,724,727,729,732,734,737,739,741,743,746,748,750,753,755,758,760,762],{"class":96,"line":488},[180,704,705],{"class":229},"    table ",[180,707,343],{"class":225},[180,709,710],{"class":229}," Table(",[180,712,713],{"class":641},"displayName",[180,715,343],{"class":225},[180,717,718],{"class":225},"f",[180,720,721],{"class":189},"\"tbl_",[180,723,448],{"class":266},[180,725,726],{"class":229},"region",[180,728,454],{"class":266},[180,730,731],{"class":189},"_",[180,733,448],{"class":266},[180,735,736],{"class":229},"i",[180,738,454],{"class":266},[180,740,256],{"class":189},[180,742,355],{"class":229},[180,744,745],{"class":641},"ref",[180,747,343],{"class":225},[180,749,718],{"class":225},[180,751,752],{"class":189},"\"A1:B",[180,754,448],{"class":266},[180,756,757],{"class":229},"ws.max_row",[180,759,454],{"class":266},[180,761,256],{"class":189},[180,763,281],{"class":229},[180,765,766,769,771,774,777,779,782,784,787,789,792],{"class":96,"line":500},[180,767,768],{"class":229},"    table.tableStyleInfo ",[180,770,343],{"class":225},[180,772,773],{"class":229}," TableStyleInfo(",[180,775,776],{"class":641},"name",[180,778,343],{"class":225},[180,780,781],{"class":189},"\"TableStyleMedium9\"",[180,783,355],{"class":229},[180,785,786],{"class":641},"showRowStripes",[180,788,343],{"class":225},[180,790,791],{"class":243},"True",[180,793,281],{"class":229},[180,795,796],{"class":96,"line":506},[180,797,798],{"class":229},"    ws.add_table(table)\n",[180,800,802],{"class":96,"line":801},17,[180,803,237],{"emptyLinePlaceholder":236},[180,805,807,810,813],{"class":96,"line":806},18,[180,808,809],{"class":229},"wb.save(",[180,811,812],{"class":189},"\"regions.xlsx\"",[180,814,281],{"class":229},[10,816,817,818,821,822,824,825,827,828,831,832,835,836,28],{},"Three details make that safe: the name is prefixed so it can never resemble a cell address like ",[14,819,820],{},"B2",", the counter guarantees uniqueness across sheets, and ",[14,823,745],{}," is computed from ",[14,826,757],{}," so it always covers real data. Writing a table over an empty range is the version of this bug that ships most often — an empty group produces ",[14,829,830],{},"A1:B1",", a header with no rows, which Excel rejects. Guard with ",[14,833,834],{},"if ws.max_row > 1:"," before adding the table. More on tables in ",[24,837,839],{"href":838},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python\u002F","Create an Excel table with Python",[166,841,843],{"id":842},"rule-3-defined-names-and-stale-references","Rule 3: defined names and stale references",[10,845,846,847,850],{},"A defined name pointing at a deleted or renamed sheet leaves ",[14,848,849],{},"#REF!"," inside the workbook, and Excel repairs the file to remove it. This happens most often when a template is filled and sheets are then dropped:",[171,852,854],{"className":216,"code":853,"language":218,"meta":176,"style":176},"from openpyxl import load_workbook\n\nwb = load_workbook(\"template.xlsx\")\ndel wb[\"Scratch\"]                     # a sheet other names may reference\n\nstale = [name for name, dn in wb.defined_names.items() if \"#REF\" in str(dn.value)\n         or \"Scratch\" in str(dn.value)]\nfor name in stale:\n    del wb.defined_names[name]\n    print(\"removed stale name:\", name)\n\nwb.save(\"filled.xlsx\")\n",[14,855,856,867,871,885,903,907,942,957,969,977,991,995],{"__ignoreMap":176},[180,857,858,860,862,864],{"class":96,"line":182},[180,859,537],{"class":225},[180,861,540],{"class":229},[180,863,226],{"class":225},[180,865,866],{"class":229}," load_workbook\n",[180,868,869],{"class":96,"line":233},[180,870,237],{"emptyLinePlaceholder":236},[180,872,873,875,877,880,883],{"class":96,"line":240},[180,874,566],{"class":229},[180,876,343],{"class":225},[180,878,879],{"class":229}," load_workbook(",[180,881,882],{"class":189},"\"template.xlsx\"",[180,884,281],{"class":229},[180,886,887,890,893,896,899],{"class":96,"line":284},[180,888,889],{"class":225},"del",[180,891,892],{"class":229}," wb[",[180,894,895],{"class":189},"\"Scratch\"",[180,897,898],{"class":229},"]                     ",[180,900,902],{"class":901},"s-wDw","# a sheet other names may reference\n",[180,904,905],{"class":96,"line":289},[180,906,237],{"emptyLinePlaceholder":236},[180,908,909,912,914,917,919,922,924,927,930,933,936,939],{"class":96,"line":331},[180,910,911],{"class":229},"stale ",[180,913,343],{"class":225},[180,915,916],{"class":229}," [name ",[180,918,627],{"class":225},[180,920,921],{"class":229}," name, dn ",[180,923,428],{"class":225},[180,925,926],{"class":229}," wb.defined_names.items() ",[180,928,929],{"class":225},"if",[180,931,932],{"class":189}," \"#REF\"",[180,934,935],{"class":225}," in",[180,937,938],{"class":243}," str",[180,940,941],{"class":229},"(dn.value)\n",[180,943,944,947,950,952,954],{"class":96,"line":337},[180,945,946],{"class":225},"         or",[180,948,949],{"class":189}," \"Scratch\"",[180,951,935],{"class":225},[180,953,938],{"class":243},[180,955,956],{"class":229},"(dn.value)]\n",[180,958,959,961,964,966],{"class":96,"line":380},[180,960,627],{"class":225},[180,962,963],{"class":229}," name ",[180,965,428],{"class":225},[180,967,968],{"class":229}," stale:\n",[180,970,971,974],{"class":96,"line":396},[180,972,973],{"class":225},"    del",[180,975,976],{"class":229}," wb.defined_names[name]\n",[180,978,979,982,985,988],{"class":96,"line":405},[180,980,981],{"class":243},"    print",[180,983,984],{"class":229},"(",[180,986,987],{"class":189},"\"removed stale name:\"",[180,989,990],{"class":229},", name)\n",[180,992,993],{"class":96,"line":419},[180,994,237],{"emptyLinePlaceholder":236},[180,996,997,999,1002],{"class":96,"line":434},[180,998,809],{"class":229},[180,1000,1001],{"class":189},"\"filled.xlsx\"",[180,1003,281],{"class":229},[10,1005,1006,1007,28],{},"The same applies to charts and conditional-formatting rules whose ranges point at removed sheets — see ",[24,1008,1010],{"href":1009},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template\u002F","Keep charts and images when filling an Excel template",[30,1012,39,1017,39,1020,39,1023,39,1026,39,1032,39,1037,39,1042,39,1047,39,1051,39,1056,39,1059,39,1063,39,1065,39,1068,39,1071,39,1075,39,1077,39,1080,39,1083,39,1087,39,1089,39,1092],{"viewBox":1013,"role":33,"ariaLabelledBy":1014,"xmlns":37,"style":38},"0 0 760 236",[1015,1016],"rep2-t","rep2-d",[41,1018,1019],{"id":1015},"The five rules a generated workbook has to satisfy",[45,1021,1022],{"id":1016},"Sheet names, table names, defined names, cell values and formula strings each have an Excel-level rule that Python libraries do not enforce, and each has a small guard that prevents it.",[49,1024],{"x":51,"y":51,"width":52,"height":1025,"fill":54},"236",[56,1027,1031],{"x":1028,"y":1029,"style":1030},"220","28","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","What Excel rejects",[56,1033,1036],{"x":1034,"y":1029,"style":1035},"566","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","The guard",[49,1038],{"x":1039,"y":1040,"width":58,"height":64,"rx":1041,"fill":142,"stroke":70},"30","42","8",[56,1043,1046],{"x":1028,"y":1044,"style":1045},"64","font-size:12px;fill:var(--accent-ink,#be185d);text-anchor:middle","sheet name over 31 chars or containing \u002F \\\\ ? * [ ]",[49,1048],{"x":1049,"y":1040,"width":1050,"height":64,"rx":1041,"fill":69,"stroke":70},"424","306",[56,1052,1055],{"x":1053,"y":1044,"style":1054},"577","font-size:12px;fill:var(--teal-ink,#0b6157);text-anchor:middle","sanitise and truncate on creation",[49,1057],{"x":1039,"y":1058,"width":58,"height":64,"rx":1041,"fill":142,"stroke":70},"84",[56,1060,1062],{"x":1028,"y":1061,"style":1045},"106","two tables sharing a name, or a header-only range",[49,1064],{"x":1049,"y":1058,"width":1050,"height":64,"rx":1041,"fill":69,"stroke":70},[56,1066,1067],{"x":1053,"y":1061,"style":1054},"prefix and count; require max_row > 1",[49,1069],{"x":1039,"y":1070,"width":58,"height":64,"rx":1041,"fill":142,"stroke":70},"126",[56,1072,1074],{"x":1028,"y":1073,"style":1045},"148","defined name pointing at a deleted sheet",[49,1076],{"x":1049,"y":1070,"width":1050,"height":64,"rx":1041,"fill":69,"stroke":70},[56,1078,1079],{"x":1053,"y":1073,"style":1054},"drop names containing #REF before saving",[49,1081],{"x":1039,"y":1082,"width":58,"height":64,"rx":1041,"fill":142,"stroke":70},"168",[56,1084,1086],{"x":1028,"y":1085,"style":1045},"190","timezone-aware datetime or NaN in a cell",[49,1088],{"x":1049,"y":1082,"width":1050,"height":64,"rx":1041,"fill":69,"stroke":70},[56,1090,1091],{"x":1053,"y":1085,"style":1054},"strip tzinfo; write None instead of NaN",[56,1093,1095],{"x":58,"y":1094,"style":93},"222","Each guard is two lines, and each prevents a prompt the recipient would see first",[166,1097,1099],{"id":1098},"rule-4-values-excel-cannot-store","Rule 4: values Excel cannot store",[10,1101,1102,1103,1106,1107,1110,1111,1114,1115,1117,1118,1121],{},"Three value types cause trouble. A timezone-aware ",[14,1104,1105],{},"datetime"," has no representation in the file format and openpyxl raises on it. A float ",[14,1108,1109],{},"NaN"," writes as the text ",[14,1112,1113],{},"nan",", which is not an error but poisons every downstream calculation. And a string beginning with ",[14,1116,343],{}," is stored as a formula, so a product code like ",[14,1119,1120],{},"=X12"," becomes a broken formula.",[171,1123,1125],{"className":216,"code":1124,"language":218,"meta":176,"style":176},"\"\"\"Coerce a DataFrame into values a workbook can hold.\"\"\"\nimport math\n\nimport pandas as pd\n\ndef excel_safe(value):\n    if isinstance(value, pd.Timestamp) or hasattr(value, \"tzinfo\"):\n        value = value.tz_localize(None) if getattr(value, \"tzinfo\", None) else value\n        return value.to_pydatetime() if isinstance(value, pd.Timestamp) else value\n    if isinstance(value, float) and math.isnan(value):\n        return None\n    if isinstance(value, str) and value.startswith((\"=\", \"+\", \"-\", \"@\")):\n        return \"'\" + value          # leading apostrophe forces text\n    return value\n",[14,1126,1127,1132,1139,1143,1156,1160,1170,1193,1230,1247,1266,1273,1310,1326],{"__ignoreMap":176},[180,1128,1129],{"class":96,"line":182},[180,1130,1131],{"class":189},"\"\"\"Coerce a DataFrame into values a workbook can hold.\"\"\"\n",[180,1133,1134,1136],{"class":96,"line":233},[180,1135,226],{"class":225},[180,1137,1138],{"class":229}," math\n",[180,1140,1141],{"class":96,"line":240},[180,1142,237],{"emptyLinePlaceholder":236},[180,1144,1145,1147,1150,1153],{"class":96,"line":284},[180,1146,226],{"class":225},[180,1148,1149],{"class":229}," pandas ",[180,1151,1152],{"class":225},"as",[180,1154,1155],{"class":229}," pd\n",[180,1157,1158],{"class":96,"line":289},[180,1159,237],{"emptyLinePlaceholder":236},[180,1161,1162,1164,1167],{"class":96,"line":331},[180,1163,292],{"class":225},[180,1165,1166],{"class":295}," excel_safe",[180,1168,1169],{"class":229},"(value):\n",[180,1171,1172,1174,1177,1180,1182,1185,1188,1191],{"class":96,"line":337},[180,1173,383],{"class":225},[180,1175,1176],{"class":243}," isinstance",[180,1178,1179],{"class":229},"(value, pd.Timestamp) ",[180,1181,374],{"class":225},[180,1183,1184],{"class":243}," hasattr",[180,1186,1187],{"class":229},"(value, ",[180,1189,1190],{"class":189},"\"tzinfo\"",[180,1192,650],{"class":229},[180,1194,1195,1198,1200,1203,1206,1209,1211,1214,1216,1218,1220,1222,1224,1227],{"class":96,"line":380},[180,1196,1197],{"class":229},"        value ",[180,1199,343],{"class":225},[180,1201,1202],{"class":229}," value.tz_localize(",[180,1204,1205],{"class":243},"None",[180,1207,1208],{"class":229},") ",[180,1210,929],{"class":225},[180,1212,1213],{"class":243}," getattr",[180,1215,1187],{"class":229},[180,1217,1190],{"class":189},[180,1219,355],{"class":229},[180,1221,1205],{"class":243},[180,1223,1208],{"class":229},[180,1225,1226],{"class":225},"else",[180,1228,1229],{"class":229}," value\n",[180,1231,1232,1234,1237,1239,1241,1243,1245],{"class":96,"line":396},[180,1233,399],{"class":225},[180,1235,1236],{"class":229}," value.to_pydatetime() ",[180,1238,929],{"class":225},[180,1240,1176],{"class":243},[180,1242,1179],{"class":229},[180,1244,1226],{"class":225},[180,1246,1229],{"class":229},[180,1248,1249,1251,1253,1255,1258,1260,1263],{"class":96,"line":405},[180,1250,383],{"class":225},[180,1252,1176],{"class":243},[180,1254,1187],{"class":229},[180,1256,1257],{"class":243},"float",[180,1259,1208],{"class":229},[180,1261,1262],{"class":225},"and",[180,1264,1265],{"class":229}," math.isnan(value):\n",[180,1267,1268,1270],{"class":96,"line":419},[180,1269,399],{"class":225},[180,1271,1272],{"class":243}," None\n",[180,1274,1275,1277,1279,1281,1283,1285,1287,1290,1293,1295,1298,1300,1302,1304,1307],{"class":96,"line":434},[180,1276,383],{"class":225},[180,1278,1176],{"class":243},[180,1280,1187],{"class":229},[180,1282,302],{"class":243},[180,1284,1208],{"class":229},[180,1286,1262],{"class":225},[180,1288,1289],{"class":229}," value.startswith((",[180,1291,1292],{"class":189},"\"=\"",[180,1294,355],{"class":229},[180,1296,1297],{"class":189},"\"+\"",[180,1299,355],{"class":229},[180,1301,352],{"class":189},[180,1303,355],{"class":229},[180,1305,1306],{"class":189},"\"@\"",[180,1308,1309],{"class":229},")):\n",[180,1311,1312,1314,1317,1320,1323],{"class":96,"line":460},[180,1313,399],{"class":225},[180,1315,1316],{"class":189}," \"'\"",[180,1318,1319],{"class":225}," +",[180,1321,1322],{"class":229}," value          ",[180,1324,1325],{"class":901},"# leading apostrophe forces text\n",[180,1327,1328,1330],{"class":96,"line":488},[180,1329,509],{"class":225},[180,1331,1229],{"class":229},[10,1333,1334,1335,1339],{},"Timezone handling in spreadsheet data is a topic of its own — ",[24,1336,1338],{"href":1337},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-timezones-in-excel-timestamps-with-python\u002F","Handle timezones in Excel timestamps with Python"," covers what to store instead of an offset.",[166,1341,1343],{"id":1342},"rule-5-control-characters-in-text","Rule 5: control characters in text",[10,1345,1346,1347,1350,1351,1354],{},"XML forbids most control characters, and openpyxl raises ",[14,1348,1349],{},"IllegalCharacterError"," when it meets one. Database exports and scraped text carry them regularly — a ",[14,1352,1353],{},"\\x00"," from a fixed-width field, a vertical tab from a pasted address:",[171,1356,1358],{"className":216,"code":1357,"language":218,"meta":176,"style":176},"import re\n\nCONTROL = re.compile(r\"[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f]\")\n\ndef clean_text(value):\n    return CONTROL.sub(\"\", value) if isinstance(value, str) else value\n",[14,1359,1360,1366,1370,1404,1408,1417],{"__ignoreMap":176},[180,1361,1362,1364],{"class":96,"line":182},[180,1363,226],{"class":225},[180,1365,230],{"class":229},[180,1367,1368],{"class":96,"line":233},[180,1369,237],{"emptyLinePlaceholder":236},[180,1371,1372,1375,1377,1379,1381,1383,1385,1387,1390,1393,1395,1398,1400,1402],{"class":96,"line":240},[180,1373,1374],{"class":243},"CONTROL",[180,1376,247],{"class":225},[180,1378,250],{"class":229},[180,1380,253],{"class":225},[180,1382,256],{"class":189},[180,1384,259],{"class":243},[180,1386,1353],{"class":262},[180,1388,1389],{"class":266},"-",[180,1391,1392],{"class":262},"\\x08\\x0b\\x0c\\x0e",[180,1394,1389],{"class":266},[180,1396,1397],{"class":262},"\\x1f",[180,1399,276],{"class":243},[180,1401,256],{"class":189},[180,1403,281],{"class":229},[180,1405,1406],{"class":96,"line":284},[180,1407,237],{"emptyLinePlaceholder":236},[180,1409,1410,1412,1415],{"class":96,"line":289},[180,1411,292],{"class":225},[180,1413,1414],{"class":295}," clean_text",[180,1416,1169],{"class":229},[180,1418,1419,1421,1424,1426,1429,1432,1434,1436,1438,1440,1442,1444],{"class":96,"line":331},[180,1420,509],{"class":225},[180,1422,1423],{"class":243}," CONTROL",[180,1425,349],{"class":229},[180,1427,1428],{"class":189},"\"\"",[180,1430,1431],{"class":229},", value) ",[180,1433,929],{"class":225},[180,1435,1176],{"class":243},[180,1437,1187],{"class":229},[180,1439,302],{"class":243},[180,1441,1208],{"class":229},[180,1443,1226],{"class":225},[180,1445,1229],{"class":229},[10,1447,1448],{},"Apply it at the boundary where data enters the report, not at the cell level, so the cleaning happens once per value rather than once per write.",[166,1450,1452],{"id":1451},"a-pre-flight-check-before-the-file-leaves-your-script","A pre-flight check before the file leaves your script",[10,1454,1455],{},"Rather than trusting each guard individually, assert the invariants on the finished workbook. This runs in well under a second and catches everything above:",[171,1457,1459],{"className":216,"code":1458,"language":218,"meta":176,"style":176},"\"\"\"Refuse to ship a workbook Excel would offer to repair.\"\"\"\nimport re\n\nfrom openpyxl import load_workbook\n\nINVALID = re.compile(r\"[\\\\\u002F?*\\[\\]:]\")\n\ndef preflight(path: str) -> None:\n    wb = load_workbook(path)\n    problems, table_names = [], set()\n\n    for ws in wb.worksheets:\n        if len(ws.title) > 31 or INVALID.search(ws.title):\n            problems.append(f\"illegal sheet name: {ws.title!r}\")\n        for name, table in getattr(ws, \"tables\", {}).items():\n            key = name.casefold()\n            if key in table_names:\n                problems.append(f\"duplicate table name: {name}\")\n            table_names.add(key)\n            if table.ref.split(\":\")[0][1:] == table.ref.split(\":\")[1][1:]:\n                problems.append(f\"table {name} has no data rows ({table.ref})\")\n\n    for name, dn in wb.defined_names.items():\n        if \"#REF\" in str(dn.value):\n            problems.append(f\"stale defined name: {name}\")\n\n    if problems:\n        raise ValueError(\"workbook would prompt a repair:\\n  \" + \"\\n  \".join(problems))\n\npreflight(\"report.xlsx\")\n",[14,1460,1461,1466,1472,1476,1486,1490,1518,1522,1540,1550,1566,1570,1582,1606,1630,1651,1661,1674,1694,1700,1742,1773,1778,1790,1804,1824,1829,1837,1869,1874],{"__ignoreMap":176},[180,1462,1463],{"class":96,"line":182},[180,1464,1465],{"class":189},"\"\"\"Refuse to ship a workbook Excel would offer to repair.\"\"\"\n",[180,1467,1468,1470],{"class":96,"line":233},[180,1469,226],{"class":225},[180,1471,230],{"class":229},[180,1473,1474],{"class":96,"line":240},[180,1475,237],{"emptyLinePlaceholder":236},[180,1477,1478,1480,1482,1484],{"class":96,"line":284},[180,1479,537],{"class":225},[180,1481,540],{"class":229},[180,1483,226],{"class":225},[180,1485,866],{"class":229},[180,1487,1488],{"class":96,"line":289},[180,1489,237],{"emptyLinePlaceholder":236},[180,1491,1492,1494,1496,1498,1500,1502,1504,1506,1508,1510,1512,1514,1516],{"class":96,"line":331},[180,1493,244],{"class":243},[180,1495,247],{"class":225},[180,1497,250],{"class":229},[180,1499,253],{"class":225},[180,1501,256],{"class":189},[180,1503,259],{"class":243},[180,1505,263],{"class":262},[180,1507,267],{"class":266},[180,1509,270],{"class":262},[180,1511,273],{"class":266},[180,1513,276],{"class":243},[180,1515,256],{"class":189},[180,1517,281],{"class":229},[180,1519,1520],{"class":96,"line":337},[180,1521,237],{"emptyLinePlaceholder":236},[180,1523,1524,1526,1529,1532,1534,1536,1538],{"class":96,"line":380},[180,1525,292],{"class":225},[180,1527,1528],{"class":295}," preflight",[180,1530,1531],{"class":229},"(path: ",[180,1533,302],{"class":243},[180,1535,323],{"class":229},[180,1537,1205],{"class":243},[180,1539,328],{"class":229},[180,1541,1542,1545,1547],{"class":96,"line":396},[180,1543,1544],{"class":229},"    wb ",[180,1546,343],{"class":225},[180,1548,1549],{"class":229}," load_workbook(path)\n",[180,1551,1552,1555,1557,1560,1563],{"class":96,"line":405},[180,1553,1554],{"class":229},"    problems, table_names ",[180,1556,343],{"class":225},[180,1558,1559],{"class":229}," [], ",[180,1561,1562],{"class":243},"set",[180,1564,1565],{"class":229},"()\n",[180,1567,1568],{"class":96,"line":419},[180,1569,237],{"emptyLinePlaceholder":236},[180,1571,1572,1574,1577,1579],{"class":96,"line":434},[180,1573,681],{"class":225},[180,1575,1576],{"class":229}," ws ",[180,1578,428],{"class":225},[180,1580,1581],{"class":229}," wb.worksheets:\n",[180,1583,1584,1587,1589,1592,1595,1598,1601,1603],{"class":96,"line":460},[180,1585,1586],{"class":225},"        if",[180,1588,476],{"class":243},[180,1590,1591],{"class":229},"(ws.title) ",[180,1593,1594],{"class":225},">",[180,1596,1597],{"class":243}," 31",[180,1599,1600],{"class":225}," or",[180,1602,346],{"class":243},[180,1604,1605],{"class":229},".search(ws.title):\n",[180,1607,1608,1611,1613,1616,1618,1621,1624,1626,1628],{"class":96,"line":488},[180,1609,1610],{"class":229},"            problems.append(",[180,1612,718],{"class":225},[180,1614,1615],{"class":189},"\"illegal sheet name: ",[180,1617,448],{"class":266},[180,1619,1620],{"class":229},"ws.title",[180,1622,1623],{"class":225},"!r",[180,1625,454],{"class":266},[180,1627,256],{"class":189},[180,1629,281],{"class":229},[180,1631,1632,1635,1638,1640,1642,1645,1648],{"class":96,"line":500},[180,1633,1634],{"class":225},"        for",[180,1636,1637],{"class":229}," name, table ",[180,1639,428],{"class":225},[180,1641,1213],{"class":243},[180,1643,1644],{"class":229},"(ws, ",[180,1646,1647],{"class":189},"\"tables\"",[180,1649,1650],{"class":229},", {}).items():\n",[180,1652,1653,1656,1658],{"class":96,"line":506},[180,1654,1655],{"class":229},"            key ",[180,1657,343],{"class":225},[180,1659,1660],{"class":229}," name.casefold()\n",[180,1662,1663,1666,1669,1671],{"class":96,"line":801},[180,1664,1665],{"class":225},"            if",[180,1667,1668],{"class":229}," key ",[180,1670,428],{"class":225},[180,1672,1673],{"class":229}," table_names:\n",[180,1675,1676,1679,1681,1684,1686,1688,1690,1692],{"class":96,"line":806},[180,1677,1678],{"class":229},"                problems.append(",[180,1680,718],{"class":225},[180,1682,1683],{"class":189},"\"duplicate table name: ",[180,1685,448],{"class":266},[180,1687,776],{"class":229},[180,1689,454],{"class":266},[180,1691,256],{"class":189},[180,1693,281],{"class":229},[180,1695,1697],{"class":96,"line":1696},19,[180,1698,1699],{"class":229},"            table_names.add(key)\n",[180,1701,1703,1705,1708,1711,1714,1716,1719,1721,1724,1727,1729,1731,1733,1735,1737,1739],{"class":96,"line":1702},20,[180,1704,1665],{"class":225},[180,1706,1707],{"class":229}," table.ref.split(",[180,1709,1710],{"class":189},"\":\"",[180,1712,1713],{"class":229},")[",[180,1715,51],{"class":243},[180,1717,1718],{"class":229},"][",[180,1720,647],{"class":243},[180,1722,1723],{"class":229},":] ",[180,1725,1726],{"class":225},"==",[180,1728,1707],{"class":229},[180,1730,1710],{"class":189},[180,1732,1713],{"class":229},[180,1734,647],{"class":243},[180,1736,1718],{"class":229},[180,1738,647],{"class":243},[180,1740,1741],{"class":229},":]:\n",[180,1743,1745,1747,1749,1752,1754,1756,1758,1761,1763,1766,1768,1771],{"class":96,"line":1744},21,[180,1746,1678],{"class":229},[180,1748,718],{"class":225},[180,1750,1751],{"class":189},"\"table ",[180,1753,448],{"class":266},[180,1755,776],{"class":229},[180,1757,454],{"class":266},[180,1759,1760],{"class":189}," has no data rows (",[180,1762,448],{"class":266},[180,1764,1765],{"class":229},"table.ref",[180,1767,454],{"class":266},[180,1769,1770],{"class":189},")\"",[180,1772,281],{"class":229},[180,1774,1776],{"class":96,"line":1775},22,[180,1777,237],{"emptyLinePlaceholder":236},[180,1779,1781,1783,1785,1787],{"class":96,"line":1780},23,[180,1782,681],{"class":225},[180,1784,921],{"class":229},[180,1786,428],{"class":225},[180,1788,1789],{"class":229}," wb.defined_names.items():\n",[180,1791,1793,1795,1797,1799,1801],{"class":96,"line":1792},24,[180,1794,1586],{"class":225},[180,1796,932],{"class":189},[180,1798,935],{"class":225},[180,1800,938],{"class":243},[180,1802,1803],{"class":229},"(dn.value):\n",[180,1805,1807,1809,1811,1814,1816,1818,1820,1822],{"class":96,"line":1806},25,[180,1808,1610],{"class":229},[180,1810,718],{"class":225},[180,1812,1813],{"class":189},"\"stale defined name: ",[180,1815,448],{"class":266},[180,1817,776],{"class":229},[180,1819,454],{"class":266},[180,1821,256],{"class":189},[180,1823,281],{"class":229},[180,1825,1827],{"class":96,"line":1826},26,[180,1828,237],{"emptyLinePlaceholder":236},[180,1830,1832,1834],{"class":96,"line":1831},27,[180,1833,383],{"class":225},[180,1835,1836],{"class":229}," problems:\n",[180,1838,1840,1843,1846,1848,1851,1854,1857,1859,1862,1864,1866],{"class":96,"line":1839},28,[180,1841,1842],{"class":225},"        raise",[180,1844,1845],{"class":243}," ValueError",[180,1847,984],{"class":229},[180,1849,1850],{"class":189},"\"workbook would prompt a repair:",[180,1852,1853],{"class":266},"\\n",[180,1855,1856],{"class":189},"  \"",[180,1858,1319],{"class":225},[180,1860,1861],{"class":189}," \"",[180,1863,1853],{"class":266},[180,1865,1856],{"class":189},[180,1867,1868],{"class":229},".join(problems))\n",[180,1870,1872],{"class":96,"line":1871},29,[180,1873,237],{"emptyLinePlaceholder":236},[180,1875,1877,1880,1883],{"class":96,"line":1876},30,[180,1878,1879],{"class":229},"preflight(",[180,1881,1882],{"class":189},"\"report.xlsx\"",[180,1884,281],{"class":229},[30,1886,39,1891,39,1894,39,1897,39,1900,39,1903,39,1909,39,1914,39,1917,39,1920,39,1923,39,1928,39,1934,39,1938,39,1942,39,1945,39,1951,39,1955,39,1959,39,1964,39,1966,39,1969,39,1973,39,1976,39,1980,39,1985,39,1988,39,1991,39,1993],{"viewBox":1887,"role":33,"ariaLabelledBy":1888,"xmlns":37,"style":38},"0 0 760 208",[1889,1890],"rep3-t","rep3-d",[41,1892,1893],{"id":1889},"Where the pre-flight check sits in a report job",[45,1895,1896],{"id":1890},"The finished workbook is validated between the write step and the delivery step, so a file that would prompt a repair fails the job instead of reaching the recipient.",[49,1898],{"x":51,"y":51,"width":52,"height":1899,"fill":54},"208",[56,1901,1902],{"x":58,"y":59,"style":60},"Validate between writing and delivering",[49,1904],{"x":59,"y":1905,"width":67,"height":1906,"rx":1907,"fill":1908,"stroke":70},"48","56","11","#ebebfd",[56,1910,1913],{"x":1911,"y":114,"style":1912},"101","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","build workbook",[96,1915],{"x1":92,"y1":1916,"x2":66,"y2":1916,"stroke":101,"style":102},"76",[104,1918],{"points":1919,"fill":107},"212,76 202,71 202,81",[49,1921],{"x":1922,"y":1905,"width":67,"height":1906,"rx":1907,"fill":111,"stroke":70},"216",[56,1924,1927],{"x":1925,"y":114,"style":1926},"291","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","preflight()",[96,1929],{"x1":1930,"y1":1931,"x2":1932,"y2":1931,"stroke":1933,"style":102},"366","66","402","var(--teal,#0f9488)",[104,1935],{"points":1936,"fill":1937},"402,66 392,61 392,71","#0f766e",[96,1939],{"x1":1930,"y1":1940,"x2":1932,"y2":1941,"stroke":134,"style":102},"92","130",[104,1943],{"points":1944,"fill":138},"402,130 390,126 396,118",[56,1946,1950],{"x":1947,"y":1948,"style":1949},"374","58","font-size:11px;fill:var(--muted,#5b6780);text-anchor:start","clean",[56,1952,1954],{"x":1947,"y":1953,"style":1949},"118","problems",[49,1956],{"x":1957,"y":1958,"width":67,"height":1906,"rx":1907,"fill":69,"stroke":70},"406","38",[56,1960,1963],{"x":1961,"y":1962,"style":1035},"481","72","deliver",[49,1965],{"x":1957,"y":1953,"width":67,"height":1906,"rx":1907,"fill":142,"stroke":70},[56,1967,1968],{"x":1961,"y":88,"style":1030},"fail the job",[96,1970],{"x1":1971,"y1":1931,"x2":1972,"y2":1931,"stroke":1933,"style":102},"556","592",[104,1974],{"points":1975,"fill":1937},"592,66 582,61 582,71",[49,1977],{"x":1978,"y":1958,"width":73,"height":1906,"rx":1907,"fill":1979,"stroke":70},"596","#f0f2f5",[56,1981,1984],{"x":1982,"y":1962,"style":1983},"666","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","inbox",[96,1986],{"x1":1971,"y1":1987,"x2":1972,"y2":1987,"stroke":134,"style":102},"146",[104,1989],{"points":1990,"fill":138},"592,146 582,141 582,151",[49,1992],{"x":1978,"y":1953,"width":73,"height":1906,"rx":1907,"fill":1979,"stroke":70},[56,1994,1995],{"x":1982,"y":88,"style":1983},"alert, not send",[10,1997,1998,1999,28],{},"Run it as the last step of the report job, before delivery. A failure there costs a rerun; a failure in the recipient's inbox costs their confidence in the report. Wiring it into the delivery step is covered in ",[24,2000,2002],{"href":2001},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel report before sending it",[166,2004,2006],{"id":2005},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[2008,2009,2010,2018,2028,2034,2040],"ul",{},[2011,2012,2013,2017],"li",{},[2014,2015,2016],"strong",{},"Editing a file Excel is holding open."," Save to a temporary name and replace, or the write may land half-complete.",[2011,2019,2020,2023,2024,2027],{},[2014,2021,2022],{},"Copying a worksheet between workbooks."," openpyxl's ",[14,2025,2026],{},"copy_worksheet"," works within one workbook only; a cross-workbook copy carries broken style references and prompts a repair.",[2011,2029,2030,2033],{},[2014,2031,2032],{},"Images with no anchor",", or an anchor beyond the sheet's used range, drop out during the repair.",[2011,2035,2036,2039],{},[2014,2037,2038],{},"A chart referencing a sheet by an old name."," Rename before you create charts, never after.",[2011,2041,2042,2045],{},[2014,2043,2044],{},"Assuming the prompt is cosmetic."," Excel silently deletes the offending object during repair, so a \"recovered\" report can be missing a whole table.",[166,2047,2049],{"id":2048},"performance-and-scale-notes","Performance and scale notes",[10,2051,2052,2053,2056,2057,2060,2061,2064,2065,28],{},"Every guard here is cheap relative to writing the file: name checks are string operations, and the pre-flight reload of a typical report costs well under a second. The exception is very large workbooks, where ",[14,2054,2055],{},"load_workbook"," for the pre-flight is genuinely expensive — use ",[14,2058,2059],{},"read_only=True"," for the sheet-name and defined-name checks (table objects are unavailable in that mode, so validate table names at construction time instead). For the write side, xlsxwriter's ",[14,2062,2063],{},"constant_memory"," mode changes the trade-off further; see ",[24,2066,2068],{"href":2067},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-million-rows-to-excel-with-xlsxwriter-constant-memory\u002F","Write a million rows to Excel with xlsxwriter constant memory",[166,2070,2072],{"id":2071},"conclusion","Conclusion",[10,2074,2075],{},"The repair prompt lives in the gap between \"valid XML\" and \"valid Excel\". Five rules cover almost every occurrence: legal and unique sheet names, unique table names over ranges that hold data, defined names without stale references, values the format can store, and text without control characters. Enforce them where the data enters, then assert them on the finished file, and the workbook your job delivers opens cleanly every time.",[166,2077,2079],{"id":2078},"frequently-asked-questions","Frequently asked questions",[10,2081,2082,2085],{},[2014,2083,2084],{},"Why does Python not raise an error if the file is invalid?","\nopenpyxl and xlsxwriter validate the XML they generate, not Excel's additional application-level rules. A duplicate table name or a defined name pointing at a deleted sheet is well-formed XML, so it writes cleanly and only Excel objects.",[10,2087,2088,2091],{},[2014,2089,2090],{},"How do I see what Excel actually removed?","\nLet Excel repair the file, then save the repaired copy under a new name and unzip both. Diffing the two sets of XML parts shows exactly which part was rewritten or dropped.",[10,2093,2094,2097],{},[2014,2095,2096],{},"Can a timezone-aware datetime really break a workbook?","\nIt raises an error in openpyxl rather than producing a repair prompt, because the file format has no way to store an offset. Convert to naive local time or UTC before writing, and put the zone in a separate column or the header.",[10,2099,2100,2103],{},[2014,2101,2102],{},"Does the repair prompt mean data was lost?","\nUsually only the offending object — a table definition, a chart, a defined name. Cell values normally survive. Never ship a file that prompts, though: recipients read it as a sign the report is untrustworthy.",[10,2105,2106,2109],{},[2014,2107,2108],{},"Which is stricter, openpyxl or xlsxwriter?","\nxlsxwriter validates more aggressively at write time and will raise on several things openpyxl allows, such as an over-long sheet name. Neither implements every Excel rule, so a pre-flight check of your own is still worth having.",[166,2111,2113],{"id":2112},"related","Related",[2008,2115,2116,2125,2130,2135,2142],{},[2011,2117,2118,2121,2122,2124],{},[2014,2119,2120],{},"Up:"," ",[24,2123,27],{"href":26}," — the reading-side errors that precede this writing-side one.",[2011,2126,2127,2129],{},[24,2128,2002],{"href":2001}," — where the pre-flight check belongs in a scheduled job.",[2011,2131,2132,2134],{},[24,2133,839],{"href":838}," — table names and ranges done correctly from the start.",[2011,2136,2137,2141],{},[24,2138,2140],{"href":2139},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle \"Permission denied\" when writing Excel in Python"," — the other failure that happens at save time.",[2011,2143,2144,2148],{},[24,2145,2147],{"href":2146},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel template without losing formatting"," — the template workflow where stale references most often appear.",[2150,2151,2152],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":176,"searchDepth":233,"depth":233,"links":2154},[2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166],{"id":168,"depth":233,"text":169},{"id":205,"depth":233,"text":206},{"id":518,"depth":233,"text":519},{"id":842,"depth":233,"text":843},{"id":1098,"depth":233,"text":1099},{"id":1342,"depth":233,"text":1343},{"id":1451,"depth":233,"text":1452},{"id":2005,"depth":233,"text":2006},{"id":2048,"depth":233,"text":2049},{"id":2071,"depth":233,"text":2072},{"id":2078,"depth":233,"text":2079},{"id":2112,"depth":233,"text":2113},"2026-08-27","Your script saved a workbook and Excel offers to repair it. The rules Excel enforces — sheet names, table names, defined names, timezones — and how to satisfy them.","md",[2171,2173,2175,2177,2182],{"q":2084,"a":2172},"openpyxl and xlsxwriter validate the XML they generate, not Excel's additional application-level rules. A duplicate table name or a defined name pointing at a deleted sheet is well-formed XML, so it writes cleanly and only Excel objects.",{"q":2090,"a":2174},"Let Excel repair the file, then save the repaired copy under a new name and unzip both. Diffing the two sets of XML parts shows exactly which part was rewritten or dropped.",{"q":2096,"a":2176},"It raises an error in openpyxl rather than producing a repair prompt, because the file format has no way to store an offset. Convert to naive local time or UTC before writing, and put the zone in a separate column or the header.",{"q":2102,"a":2178},{"Usually only the offending object — a table definition, a chart, a defined name":2179},{" Cell values normally survive":2180},{" Never ship a file that prompts, though":2181},"recipients read it as a sign the report is untrustworthy.",{"q":2108,"a":2183},"xlsxwriter validates more aggressively at write time and will raise on several things openpyxl allows, such as an over-long sheet name. Neither implements every Excel rule, so a pre-flight check of your own is still worth having.",{},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python",{"title":2187,"description":2188},"Fix Excel Repaired Your Python Workbook","Stop Excel repairing files written by openpyxl or xlsxwriter: legal sheet and table names, valid defined names, naive datetimes, safe strings and a pre-flight check.","fix-excel-repaired-content-after-writing-with-python","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python\u002Findex","how-to","aPyn_fLcH7fMER0VWajWDHOpR__JUYgc_gGrR-ImD9E",[2194,2198],{"title":2195,"path":2196,"stem":2197,"children":-1},"Fix \"Excel File Format Cannot Be Determined\" in pandas","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-file-format-cannot-be-determined-in-pandas","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-file-format-cannot-be-determined-in-pandas\u002Findex",{"title":2199,"path":2200,"stem":2201,"children":-1},"Fix \"openpyxl does not support the old .xls file format\"","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format\u002Findex",1788710155135]