[{"data":1,"prerenderedAt":2069},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python":2061},{"id":4,"title":5,"body":6,"dateModified":2038,"datePublished":2038,"description":2039,"extension":2040,"faq":2041,"meta":2052,"navigation":264,"path":2053,"seo":2054,"slug":2057,"stem":2058,"type":2059,"__hash__":2060},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002Findex.md","Fix BadZipFile When Reading an Excel File in Python",{"type":7,"value":8,"toc":2025},"minimark",[9,27,165,170,177,204,215,219,222,583,590,594,700,713,800,820,842,852,856,866,1013,1027,1031,1093,1097,1108,1222,1226,1229,1582,1585,1719,1727,1731,1734,1908,1914,1918,1929,1933,1948,1958,1964,1977,1983,1987,2021],[10,11,12,16,17,20,21,26],"p",{},[13,14,15],"code",{},"zipfile.BadZipFile: File is not a zip file"," is the first error most Python developers hit when automating spreadsheets, and its wording sends people looking in the wrong place. Nothing is wrong with your pandas call or your openpyxl version: a modern ",[13,18,19],{},".xlsx"," is a zip archive containing XML parts, so the read fails in the zip layer before a single cell is parsed. This guide identifies what the file really is, fixes each of the causes, and shows how to recover data from an archive that is genuinely damaged. It is one of the failures mapped in ",[22,23,25],"a",{"href":24},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[28,29,37,38,37,42,37,46,37,53,37,60,37,69,37,75,37,80,37,85,37,88,37,92,37,100,37,105,37,112,37,118,37,121,37,125,37,127,37,131,37,136,37,140,37,147,37,152,37,157,37,160],"svg",{"viewBox":30,"role":31,"ariaLabelledBy":32,"xmlns":35,"style":36},"0 0 760 236","img",[33,34],"bzf-t","bzf-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  ",[39,40,41],"title",{"id":33},"What an .xlsx file really contains",[43,44,45],"desc",{"id":34},"A valid xlsx is a zip container holding an XML part per sheet plus shared strings and styles; openpyxl opens the zip first, so a file that is not a zip fails before any sheet is read.",[47,48],"rect",{"x":49,"y":49,"width":50,"height":51,"fill":52},"0","760","236","#ffffff",[54,55,59],"text",{"x":56,"y":57,"style":58},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","report.xlsx, opened as bytes",[47,61],{"x":62,"y":63,"width":64,"height":65,"rx":66,"fill":67,"stroke":68},"40","46","230","150","14","#ebebfd","var(--line,#cdd5e6)",[54,70,74],{"x":71,"y":72,"style":73},"155","78","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","zip container",[54,76,79],{"x":71,"y":77,"style":78},"104","font-size:12px;fill:var(--text,#172033);text-anchor:middle","first two bytes: PK",[54,81,84],{"x":71,"y":82,"style":83},"130","font-size:12px;fill:var(--muted,#5b6780);text-anchor:middle","central directory at",[54,86,87],{"x":71,"y":65,"style":83},"the end of the file",[54,89,91],{"x":71,"y":90,"style":83},"176","truncation breaks it",[93,94],"line",{"x1":95,"y1":96,"x2":97,"y2":96,"stroke":98,"style":99},"270","121","306","var(--brand,#5b5cf0)","stroke-width:2px",[101,102],"polygon",{"points":103,"fill":104},"306,121 296,116 296,126","#5b5cf0",[47,106],{"x":107,"y":63,"width":108,"height":109,"rx":110,"fill":111,"stroke":68},"310","200","44","9","#d9f4f1",[54,113,117],{"x":114,"y":115,"style":116},"410","74","font-size:12px;fill:var(--teal-ink,#0b6157);text-anchor:middle","xl\u002Fworksheets\u002Fsheet1.xml",[47,119],{"x":107,"y":120,"width":108,"height":109,"rx":110,"fill":111,"stroke":68},"98",[54,122,124],{"x":114,"y":123,"style":116},"126","xl\u002FsharedStrings.xml",[47,126],{"x":107,"y":65,"width":108,"height":63,"rx":110,"fill":111,"stroke":68},[54,128,130],{"x":114,"y":129,"style":116},"179","xl\u002Fstyles.xml",[93,132],{"x1":133,"y1":96,"x2":134,"y2":96,"stroke":135,"style":99},"510","546","var(--teal,#0f9488)",[101,137],{"points":138,"fill":139},"546,121 536,116 536,126","#0f766e",[47,141],{"x":142,"y":143,"width":90,"height":144,"rx":145,"fill":146,"stroke":68},"550","70","102","12","#fdefd8",[54,148,151],{"x":149,"y":77,"style":150},"638","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","openpyxl",[54,153,156],{"x":149,"y":154,"style":155},"128","font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Workbook object",[54,158,159],{"x":149,"y":65,"style":155},"cells, styles, names",[54,161,164],{"x":56,"y":162,"style":163},"222","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","BadZipFile is raised at the first arrow — the parser never runs",[166,167,169],"h2",{"id":168},"prerequisites","Prerequisites",[10,171,172,173,176],{},"Nothing beyond a standard install. ",[13,174,175],{},"zipfile"," is in the standard library, and the checks below need no third-party package:",[178,179,184],"pre",{"className":180,"code":181,"language":182,"meta":183,"style":183},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[13,185,186],{"__ignoreMap":183},[187,188,190,194,198,201],"span",{"class":93,"line":189},1,[187,191,193],{"class":192},"sMTad","pip",[187,195,197],{"class":196},"srMev"," install",[187,199,200],{"class":196}," pandas",[187,202,203],{"class":196}," openpyxl\n",[10,205,206,207,210,211,214],{},"The diagnosis works the same whether the error came from ",[13,208,209],{},"pd.read_excel()",", ",[13,212,213],{},"openpyxl.load_workbook()",", or a library that wraps either.",[166,216,218],{"id":217},"step-1-confirm-the-file-is-not-a-zip","Step 1: confirm the file is not a zip",[10,220,221],{},"Do not guess from the extension. Read the first four bytes and compare them against the signatures Excel-adjacent files actually use:",[178,223,227],{"className":224,"code":225,"language":226,"meta":183,"style":183},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Report what a supposedly-xlsx file really is.\"\"\"\nfrom pathlib import Path\nimport zipfile\n\ndef diagnose(path: str) -> str:\n    p = Path(path)\n    if not p.is_file():\n        return f\"missing: {p.resolve()}\"\n    size = p.stat().st_size\n    if size == 0:\n        return \"zero bytes — the upstream write or download failed\"\n\n    head = p.read_bytes()[:8]\n    if head.startswith(b\"PK\\x03\\x04\"):\n        return \"valid zip container\" if zipfile.is_zipfile(p) else \"zip header but broken archive\"\n    if head.startswith(b\"\\xd0\\xcf\\x11\\xe0\"):\n        return \"OLE2 file — a legacy .xls, or an encrypted workbook\"\n    if head[:5].lower() in (b\"\u003Chtml\", b\"\u003C!doc\"):\n        return \"HTML — a web export renamed to .xlsx\"\n    if head.startswith(b\"\u003C?xml\"):\n        return \"XML — SpreadsheetML 2003, not a real .xlsx\"\n    return f\"text or unknown ({size} bytes): {head!r}\"\n\nprint(diagnose(\"report.xlsx\"))\n","python",[13,228,229,234,251,259,266,291,303,315,340,351,367,375,380,397,420,440,458,466,500,508,522,530,563,568],{"__ignoreMap":183},[187,230,231],{"class":93,"line":189},[187,232,233],{"class":196},"\"\"\"Report what a supposedly-xlsx file really is.\"\"\"\n",[187,235,237,241,245,248],{"class":93,"line":236},2,[187,238,240],{"class":239},"s-kum","from",[187,242,244],{"class":243},"skGVy"," pathlib ",[187,246,247],{"class":239},"import",[187,249,250],{"class":243}," Path\n",[187,252,254,256],{"class":93,"line":253},3,[187,255,247],{"class":239},[187,257,258],{"class":243}," zipfile\n",[187,260,262],{"class":93,"line":261},4,[187,263,265],{"emptyLinePlaceholder":264},true,"\n",[187,267,269,272,276,279,283,286,288],{"class":93,"line":268},5,[187,270,271],{"class":239},"def",[187,273,275],{"class":274},"s_Opv"," diagnose",[187,277,278],{"class":243},"(path: ",[187,280,282],{"class":281},"sP0c6","str",[187,284,285],{"class":243},") -> ",[187,287,282],{"class":281},[187,289,290],{"class":243},":\n",[187,292,294,297,300],{"class":93,"line":293},6,[187,295,296],{"class":243},"    p ",[187,298,299],{"class":239},"=",[187,301,302],{"class":243}," Path(path)\n",[187,304,306,309,312],{"class":93,"line":305},7,[187,307,308],{"class":239},"    if",[187,310,311],{"class":239}," not",[187,313,314],{"class":243}," p.is_file():\n",[187,316,318,321,324,327,331,334,337],{"class":93,"line":317},8,[187,319,320],{"class":239},"        return",[187,322,323],{"class":239}," f",[187,325,326],{"class":196},"\"missing: ",[187,328,330],{"class":329},"sSjpA","{",[187,332,333],{"class":243},"p.resolve()",[187,335,336],{"class":329},"}",[187,338,339],{"class":196},"\"\n",[187,341,343,346,348],{"class":93,"line":342},9,[187,344,345],{"class":243},"    size ",[187,347,299],{"class":239},[187,349,350],{"class":243}," p.stat().st_size\n",[187,352,354,356,359,362,365],{"class":93,"line":353},10,[187,355,308],{"class":239},[187,357,358],{"class":243}," size ",[187,360,361],{"class":239},"==",[187,363,364],{"class":281}," 0",[187,366,290],{"class":243},[187,368,370,372],{"class":93,"line":369},11,[187,371,320],{"class":239},[187,373,374],{"class":196}," \"zero bytes — the upstream write or download failed\"\n",[187,376,378],{"class":93,"line":377},12,[187,379,265],{"emptyLinePlaceholder":264},[187,381,383,386,388,391,394],{"class":93,"line":382},13,[187,384,385],{"class":243},"    head ",[187,387,299],{"class":239},[187,389,390],{"class":243}," p.read_bytes()[:",[187,392,393],{"class":281},"8",[187,395,396],{"class":243},"]\n",[187,398,400,402,405,408,411,414,417],{"class":93,"line":399},14,[187,401,308],{"class":239},[187,403,404],{"class":243}," head.startswith(",[187,406,407],{"class":239},"b",[187,409,410],{"class":196},"\"PK",[187,412,413],{"class":329},"\\x03\\x04",[187,415,416],{"class":196},"\"",[187,418,419],{"class":243},"):\n",[187,421,423,425,428,431,434,437],{"class":93,"line":422},15,[187,424,320],{"class":239},[187,426,427],{"class":196}," \"valid zip container\"",[187,429,430],{"class":239}," if",[187,432,433],{"class":243}," zipfile.is_zipfile(p) ",[187,435,436],{"class":239},"else",[187,438,439],{"class":196}," \"zip header but broken archive\"\n",[187,441,443,445,447,449,451,454,456],{"class":93,"line":442},16,[187,444,308],{"class":239},[187,446,404],{"class":243},[187,448,407],{"class":239},[187,450,416],{"class":196},[187,452,453],{"class":329},"\\xd0\\xcf\\x11\\xe0",[187,455,416],{"class":196},[187,457,419],{"class":243},[187,459,461,463],{"class":93,"line":460},17,[187,462,320],{"class":239},[187,464,465],{"class":196}," \"OLE2 file — a legacy .xls, or an encrypted workbook\"\n",[187,467,469,471,474,477,480,483,486,488,491,493,495,498],{"class":93,"line":468},18,[187,470,308],{"class":239},[187,472,473],{"class":243}," head[:",[187,475,476],{"class":281},"5",[187,478,479],{"class":243},"].lower() ",[187,481,482],{"class":239},"in",[187,484,485],{"class":243}," (",[187,487,407],{"class":239},[187,489,490],{"class":196},"\"\u003Chtml\"",[187,492,210],{"class":243},[187,494,407],{"class":239},[187,496,497],{"class":196},"\"\u003C!doc\"",[187,499,419],{"class":243},[187,501,503,505],{"class":93,"line":502},19,[187,504,320],{"class":239},[187,506,507],{"class":196}," \"HTML — a web export renamed to .xlsx\"\n",[187,509,511,513,515,517,520],{"class":93,"line":510},20,[187,512,308],{"class":239},[187,514,404],{"class":243},[187,516,407],{"class":239},[187,518,519],{"class":196},"\"\u003C?xml\"",[187,521,419],{"class":243},[187,523,525,527],{"class":93,"line":524},21,[187,526,320],{"class":239},[187,528,529],{"class":196}," \"XML — SpreadsheetML 2003, not a real .xlsx\"\n",[187,531,533,536,538,541,543,546,548,551,553,556,559,561],{"class":93,"line":532},22,[187,534,535],{"class":239},"    return",[187,537,323],{"class":239},[187,539,540],{"class":196},"\"text or unknown (",[187,542,330],{"class":329},[187,544,545],{"class":243},"size",[187,547,336],{"class":329},[187,549,550],{"class":196}," bytes): ",[187,552,330],{"class":329},[187,554,555],{"class":243},"head",[187,557,558],{"class":239},"!r",[187,560,336],{"class":329},[187,562,339],{"class":196},[187,564,566],{"class":93,"line":565},23,[187,567,265],{"emptyLinePlaceholder":264},[187,569,571,574,577,580],{"class":93,"line":570},24,[187,572,573],{"class":281},"print",[187,575,576],{"class":243},"(diagnose(",[187,578,579],{"class":196},"\"report.xlsx\"",[187,581,582],{"class":243},"))\n",[10,584,585,586,589],{},"The result names your fix. ",[13,587,588],{},"PK"," plus a broken archive means truncation or damage. OLE2 means a legacy or encrypted file. HTML, XML or plain text means the producer never wrote a real workbook — the most common outcome by far when the file came from a reporting portal, a CRM export, or an emailed \"Excel\" attachment.",[166,591,593],{"id":592},"step-2-apply-the-fix-for-that-cause","Step 2: apply the fix for that cause",[28,595,37,600,37,603,37,606,37,609,37,615,37,620,37,626,37,630,37,634,37,638,37,641,37,645,37,647,37,650,37,653,37,657,37,659,37,662,37,665,37,669,37,671,37,674,37,679,37,682,37,685,37,688,37,691,37,694,37,697],{"viewBox":596,"role":31,"ariaLabelledBy":597,"xmlns":35,"style":36},"0 0 760 250",[598,599],"bzf2-t","bzf2-d",[39,601,602],{"id":598},"Four causes of BadZipFile and the fix for each",[43,604,605],{"id":599},"A file that is really HTML or CSV needs a different reader, a legacy xls needs a conversion, a truncated download needs re-fetching, and a file still being written needs a stability check before reading.",[47,607],{"x":49,"y":49,"width":50,"height":608,"fill":52},"250",[54,610,614],{"x":611,"y":612,"style":613},"196","28","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","What the bytes say",[54,616,619],{"x":617,"y":612,"style":618},"566","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","What to do",[47,621],{"x":622,"y":623,"width":624,"height":109,"rx":110,"fill":625,"stroke":68},"30","42","332","#fee8f2",[54,627,629],{"x":611,"y":143,"style":628},"font-size:12px;fill:var(--accent-ink,#be185d);text-anchor:middle","starts with \u003Chtml or a comma",[47,631],{"x":632,"y":623,"width":633,"height":109,"rx":110,"fill":111,"stroke":68},"400","330",[54,635,637],{"x":636,"y":143,"style":116},"565","read_html() or read_csv(), then re-export",[47,639],{"x":622,"y":640,"width":624,"height":109,"rx":110,"fill":625,"stroke":68},"94",[54,642,644],{"x":611,"y":643,"style":628},"122","OLE2 signature D0 CF 11 E0",[47,646],{"x":632,"y":640,"width":633,"height":109,"rx":110,"fill":111,"stroke":68},[54,648,649],{"x":636,"y":643,"style":116},"engine=\"xlrd\", or decrypt if protected",[47,651],{"x":622,"y":652,"width":624,"height":109,"rx":110,"fill":625,"stroke":68},"146",[54,654,656],{"x":611,"y":655,"style":628},"174","PK header, archive still broken",[47,658],{"x":632,"y":652,"width":633,"height":109,"rx":110,"fill":111,"stroke":68},[54,660,661],{"x":636,"y":655,"style":116},"re-download; verify length or checksum",[47,663],{"x":622,"y":664,"width":624,"height":109,"rx":110,"fill":625,"stroke":68},"198",[54,666,668],{"x":611,"y":667,"style":628},"226","size changes between two reads",[47,670],{"x":632,"y":664,"width":633,"height":109,"rx":110,"fill":111,"stroke":68},[54,672,673],{"x":636,"y":667,"style":116},"wait for a stable size, then open",[93,675],{"x1":676,"y1":677,"x2":678,"y2":677,"stroke":98,"style":99},"362","64","396",[101,680],{"points":681,"fill":104},"396,64 386,59 386,69",[93,683],{"x1":676,"y1":684,"x2":678,"y2":684,"stroke":98,"style":99},"116",[101,686],{"points":687,"fill":104},"396,116 386,111 386,121",[93,689],{"x1":676,"y1":690,"x2":678,"y2":690,"stroke":98,"style":99},"168",[101,692],{"points":693,"fill":104},"396,168 386,163 386,173",[93,695],{"x1":676,"y1":696,"x2":678,"y2":696,"stroke":98,"style":99},"220",[101,698],{"points":699,"fill":104},"396,220 386,215 386,225",[10,701,702,708,709,712],{},[703,704,705,706,26],"strong",{},"An HTML table renamed ",[13,707,19],{}," Reporting portals export an HTML ",[13,710,711],{},"\u003Ctable>"," with a spreadsheet extension because Excel renders it. pandas can read it directly, and you can write a genuine workbook from the result:",[178,714,716],{"className":224,"code":715,"language":226,"meta":183,"style":183},"import pandas as pd\n\ntables = pd.read_html(\"report.xlsx\")     # needs lxml or html5lib\ndf = tables[0]\ndf.to_excel(\"report_fixed.xlsx\", index=False, engine=\"openpyxl\")\n",[13,717,718,731,735,754,768],{"__ignoreMap":183},[187,719,720,722,725,728],{"class":93,"line":189},[187,721,247],{"class":239},[187,723,724],{"class":243}," pandas ",[187,726,727],{"class":239},"as",[187,729,730],{"class":243}," pd\n",[187,732,733],{"class":93,"line":236},[187,734,265],{"emptyLinePlaceholder":264},[187,736,737,740,742,745,747,750],{"class":93,"line":253},[187,738,739],{"class":243},"tables ",[187,741,299],{"class":239},[187,743,744],{"class":243}," pd.read_html(",[187,746,579],{"class":196},[187,748,749],{"class":243},")     ",[187,751,753],{"class":752},"s-wDw","# needs lxml or html5lib\n",[187,755,756,759,761,764,766],{"class":93,"line":261},[187,757,758],{"class":243},"df ",[187,760,299],{"class":239},[187,762,763],{"class":243}," tables[",[187,765,49],{"class":281},[187,767,396],{"class":243},[187,769,770,773,776,778,782,784,787,789,792,794,797],{"class":93,"line":268},[187,771,772],{"class":243},"df.to_excel(",[187,774,775],{"class":196},"\"report_fixed.xlsx\"",[187,777,210],{"class":243},[187,779,781],{"class":780},"sa561","index",[187,783,299],{"class":239},[187,785,786],{"class":281},"False",[187,788,210],{"class":243},[187,790,791],{"class":780},"engine",[187,793,299],{"class":239},[187,795,796],{"class":196},"\"openpyxl\"",[187,798,799],{"class":243},")\n",[10,801,802,807,808,811,812,815,816,819],{},[703,803,804,805,26],{},"A CSV renamed ",[13,806,19],{}," Same idea with ",[13,809,810],{},"pd.read_csv()","; pass ",[13,813,814],{},"sep=None, engine=\"python\""," if the delimiter is unknown, and ",[13,817,818],{},"encoding=\"utf-8-sig\""," when a byte-order mark is present.",[10,821,822,828,829,832,833,837,838,26],{},[703,823,824,825,26],{},"A legacy ",[13,826,827],{},".xls"," The file is an OLE2 compound document, which openpyxl has never supported. Read it with ",[13,830,831],{},"engine=\"xlrd\""," and convert once — see ",[22,834,836],{"href":835},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format\u002F","Fix \"openpyxl does not support the old .xls format\""," and ",[22,839,841],{"href":840},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-xls-to-xlsx-with-python\u002F","Convert xls to xlsx with Python",[10,843,844,847,848,26],{},[703,845,846],{},"An encrypted workbook."," A password-protected file is also OLE2, so it reports the same way. Decrypt it in memory before reading, as shown in ",[22,849,851],{"href":850},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fopen-a-password-protected-excel-file-with-python\u002F","Open a password-protected Excel file with Python",[166,853,855],{"id":854},"step-3-recover-data-from-a-damaged-archive","Step 3: recover data from a damaged archive",[10,857,858,859,861,862,865],{},"If the header says ",[13,860,588],{}," but the archive still will not open, the zip itself is damaged — usually a partial download. ",[13,863,864],{},"testzip()"," names the first bad member, and a member-by-member copy can often rescue everything else:",[178,867,869],{"className":224,"code":868,"language":226,"meta":183,"style":183},"\"\"\"Salvage the readable parts of a damaged .xlsx.\"\"\"\nimport shutil\nimport zipfile\n\nsrc, dst = \"broken.xlsx\", \"salvaged.xlsx\"\n\nwith zipfile.ZipFile(src) as zf:          # raises BadZipFile if the directory is gone\n    print(\"first damaged member:\", zf.testzip())\n    with zipfile.ZipFile(dst, \"w\", zipfile.ZIP_DEFLATED) as out:\n        for info in zf.infolist():\n            try:\n                out.writestr(info, zf.read(info.filename))\n            except zipfile.BadZipFile:\n                print(\"skipping unreadable part:\", info.filename)\n",[13,870,871,876,883,889,893,908,912,928,942,967,980,987,992,1000],{"__ignoreMap":183},[187,872,873],{"class":93,"line":189},[187,874,875],{"class":196},"\"\"\"Salvage the readable parts of a damaged .xlsx.\"\"\"\n",[187,877,878,880],{"class":93,"line":236},[187,879,247],{"class":239},[187,881,882],{"class":243}," shutil\n",[187,884,885,887],{"class":93,"line":253},[187,886,247],{"class":239},[187,888,258],{"class":243},[187,890,891],{"class":93,"line":261},[187,892,265],{"emptyLinePlaceholder":264},[187,894,895,898,900,903,905],{"class":93,"line":268},[187,896,897],{"class":243},"src, dst ",[187,899,299],{"class":239},[187,901,902],{"class":196}," \"broken.xlsx\"",[187,904,210],{"class":243},[187,906,907],{"class":196},"\"salvaged.xlsx\"\n",[187,909,910],{"class":93,"line":293},[187,911,265],{"emptyLinePlaceholder":264},[187,913,914,917,920,922,925],{"class":93,"line":305},[187,915,916],{"class":239},"with",[187,918,919],{"class":243}," zipfile.ZipFile(src) ",[187,921,727],{"class":239},[187,923,924],{"class":243}," zf:          ",[187,926,927],{"class":752},"# raises BadZipFile if the directory is gone\n",[187,929,930,933,936,939],{"class":93,"line":317},[187,931,932],{"class":281},"    print",[187,934,935],{"class":243},"(",[187,937,938],{"class":196},"\"first damaged member:\"",[187,940,941],{"class":243},", zf.testzip())\n",[187,943,944,947,950,953,956,959,962,964],{"class":93,"line":342},[187,945,946],{"class":239},"    with",[187,948,949],{"class":243}," zipfile.ZipFile(dst, ",[187,951,952],{"class":196},"\"w\"",[187,954,955],{"class":243},", zipfile.",[187,957,958],{"class":281},"ZIP_DEFLATED",[187,960,961],{"class":243},") ",[187,963,727],{"class":239},[187,965,966],{"class":243}," out:\n",[187,968,969,972,975,977],{"class":93,"line":353},[187,970,971],{"class":239},"        for",[187,973,974],{"class":243}," info ",[187,976,482],{"class":239},[187,978,979],{"class":243}," zf.infolist():\n",[187,981,982,985],{"class":93,"line":369},[187,983,984],{"class":239},"            try",[187,986,290],{"class":243},[187,988,989],{"class":93,"line":377},[187,990,991],{"class":243},"                out.writestr(info, zf.read(info.filename))\n",[187,993,994,997],{"class":93,"line":382},[187,995,996],{"class":239},"            except",[187,998,999],{"class":243}," zipfile.BadZipFile:\n",[187,1001,1002,1005,1007,1010],{"class":93,"line":399},[187,1003,1004],{"class":281},"                print",[187,1006,935],{"class":243},[187,1008,1009],{"class":196},"\"skipping unreadable part:\"",[187,1011,1012],{"class":243},", info.filename)\n",[10,1014,1015,1016,1018,1019,1022,1023,1026],{},"A workbook that loses ",[13,1017,130],{}," still opens with its data intact; one that loses a sheet part does not. If ",[13,1020,1021],{},"zipfile.ZipFile(src)"," itself raises, the central directory at the end of the file is missing — the download stopped early — and no pure-Python repair exists. Re-fetch the file and compare the byte length against the ",[13,1024,1025],{},"Content-Length"," header before trusting it.",[166,1028,1030],{"id":1029},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1032,1033,1034,1053,1069,1079,1087],"ul",{},[1035,1036,1037,1040,1041,1044,1045,1048,1049,1052],"li",{},[703,1038,1039],{},"Catching the wrong exception."," ",[13,1042,1043],{},"BadZipFile"," is ",[13,1046,1047],{},"zipfile.BadZipFile","; openpyxl re-raises some cases as ",[13,1050,1051],{},"InvalidFileException",". Catch both if you are writing a guard.",[1035,1054,1055,1040,1058,1061,1062,837,1065,1068],{},[703,1056,1057],{},"Reading straight from a download stream.",[13,1059,1060],{},"pd.read_excel(response.content)"," on a failed request happily parses an HTML error page. Check ",[13,1063,1064],{},"response.status_code",[13,1066,1067],{},"Content-Type"," first.",[1035,1070,1071,1074,1075,1078],{},[703,1072,1073],{},"Assuming a network share is atomic."," It is not. Have the producer write ",[13,1076,1077],{},"name.tmp"," and rename, or wait until the size stops changing.",[1035,1080,1081,1040,1084,1086],{},[703,1082,1083],{},"Version confusion.",[13,1085,1043],{}," never indicates an outdated openpyxl. Upgrading the library cannot fix a file that is not a zip.",[1035,1088,1089,1092],{},[703,1090,1091],{},"Antivirus and DLP tools"," occasionally quarantine and replace an attachment with a text stub of the same name. The magic-byte check catches that instantly.",[166,1094,1096],{"id":1095},"performance-and-scale-notes","Performance and scale notes",[10,1098,1099,1100,1103,1104,26],{},"The diagnosis costs nothing: reading eight bytes is a single seek, so you can run the check on every file in a batch without measurable overhead. ",[13,1101,1102],{},"zipfile.is_zipfile()"," is heavier — it seeks to the end of the file to find the central directory — but still trivial next to parsing a workbook. In a pipeline that ingests hundreds of files, validate them all first and report every bad file at once, rather than failing on the first one after twenty minutes of work. That pattern pairs well with ",[22,1105,1107],{"href":1106},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fprocess-multiple-excel-files-in-parallel-with-python\u002F","Process multiple Excel files in parallel with Python",[28,1109,37,1114,37,1117,37,1120,37,1123,37,1126,37,1132,37,1137,37,1141,37,1144,37,1147,37,1150,37,1155,37,1158,37,1162,37,1165,37,1168,37,1173,37,1177,37,1180,37,1185,37,1190,37,1195,37,1199,37,1203,37,1206,37,1210,37,1215,37,1218],{"viewBox":1110,"role":31,"ariaLabelledBy":1111,"xmlns":35,"style":36},"0 0 760 216",[1112,1113],"bzf3-t","bzf3-d",[39,1115,1116],{"id":1112},"Validate every input file before parsing any of them",[43,1118,1119],{"id":1113},"A batch job that checks magic bytes for all files first reports every bad input in one pass, instead of failing on the first bad file after parsing the earlier ones.",[47,1121],{"x":49,"y":49,"width":50,"height":1122,"fill":52},"216",[54,1124,1125],{"x":56,"y":57,"style":58},"Batch ingest with a cheap pre-flight",[47,1127],{"x":1128,"y":109,"width":65,"height":1129,"rx":1130,"fill":1131,"stroke":68},"34","60","11","#f0f2f5",[54,1133,1136],{"x":1134,"y":143,"style":1135},"109","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","120 files",[54,1138,1140],{"x":1134,"y":1139,"style":163},"90","from a share",[93,1142],{"x1":1143,"y1":115,"x2":1122,"y2":115,"stroke":98,"style":99},"184",[101,1145],{"points":1146,"fill":104},"216,74 206,69 206,79",[47,1148],{"x":696,"y":109,"width":1149,"height":1129,"rx":1130,"fill":67,"stroke":68},"170",[54,1151,1154],{"x":1152,"y":143,"style":1153},"305","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","read 8 bytes each",[54,1156,1157],{"x":1152,"y":1139,"style":163},"under a second total",[93,1159],{"x1":1160,"y1":115,"x2":1161,"y2":115,"stroke":98,"style":99},"390","422",[101,1163],{"points":1164,"fill":104},"422,74 412,69 412,79",[47,1166],{"x":1167,"y":109,"width":65,"height":1129,"rx":1130,"fill":111,"stroke":68},"426",[54,1169,1172],{"x":1170,"y":143,"style":1171},"501","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","114 valid",[54,1174,1176],{"x":1170,"y":1139,"style":1175},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","parse these",[47,1178],{"x":1167,"y":1179,"width":65,"height":1129,"rx":1130,"fill":625,"stroke":68},"118",[54,1181,1184],{"x":1170,"y":1182,"style":1183},"144","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","6 rejected",[54,1186,1189],{"x":1170,"y":1187,"style":1188},"164","font-size:11.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","reported together",[93,1191],{"x1":1160,"y1":1192,"x2":1161,"y2":1193,"stroke":1194,"style":99},"86","140","var(--accent,#f43f8f)",[101,1196],{"points":1197,"fill":1198},"422,140 410,136 416,128","#be185d",[93,1200],{"x1":1201,"y1":115,"x2":1202,"y2":115,"stroke":135,"style":99},"576","608",[101,1204],{"points":1205,"fill":139},"608,74 598,69 598,79",[47,1207],{"x":1208,"y":109,"width":1209,"height":1129,"rx":1130,"fill":146,"stroke":68},"612","124",[54,1211,1214],{"x":1212,"y":143,"style":1213},"674","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","one run,",[54,1216,1217],{"x":1212,"y":1139,"style":1213},"one report",[54,1219,1221],{"x":56,"y":1220,"style":163},"202","Failing fast on file 1 hides the other five bad inputs until tomorrow's run",[166,1223,1225],{"id":1224},"step-4-stop-it-recurring-in-an-unattended-job","Step 4: stop it recurring in an unattended job",[10,1227,1228],{},"A one-off fix on your laptop does not help the 06:00 run. Fold the diagnosis into the ingest itself so a bad input is rejected with a readable message and the rest of the batch still completes. The helper below returns a DataFrame for a genuine workbook, transparently handles the two \"renamed export\" cases, and raises a message a colleague can act on for anything else:",[178,1230,1232],{"className":224,"code":1231,"language":226,"meta":183,"style":183},"\"\"\"Read a spreadsheet whatever the producer actually sent.\"\"\"\nfrom pathlib import Path\nimport zipfile\n\nimport pandas as pd\n\nclass UnreadableSpreadsheet(RuntimeError):\n    pass\n\ndef read_any(path: str) -> pd.DataFrame:\n    p = Path(path).resolve()\n    head = p.read_bytes()[:8] if p.is_file() else b\"\"\n\n    if head.startswith(b\"PK\"):\n        if not zipfile.is_zipfile(p):\n            raise UnreadableSpreadsheet(f\"{p}: zip header but damaged archive — re-download\")\n        return pd.read_excel(p, engine=\"openpyxl\")\n    if head.startswith(b\"\\xd0\\xcf\\x11\\xe0\"):\n        return pd.read_excel(p, engine=\"xlrd\")          # legacy .xls\n    if head[:5].lower() in (b\"\u003Chtml\", b\"\u003C!doc\"):\n        return pd.read_html(p)[0]                        # HTML table export\n    if head and not head.startswith(b\"\u003C?xml\"):\n        return pd.read_csv(p, sep=None, engine=\"python\", encoding=\"utf-8-sig\")\n\n    raise UnreadableSpreadsheet(f\"{p}: unrecognised content {head!r}\")\n",[13,1233,1234,1239,1249,1255,1259,1269,1273,1288,1293,1297,1311,1320,1347,1351,1364,1374,1398,1413,1429,1448,1474,1489,1509,1545,1549],{"__ignoreMap":183},[187,1235,1236],{"class":93,"line":189},[187,1237,1238],{"class":196},"\"\"\"Read a spreadsheet whatever the producer actually sent.\"\"\"\n",[187,1240,1241,1243,1245,1247],{"class":93,"line":236},[187,1242,240],{"class":239},[187,1244,244],{"class":243},[187,1246,247],{"class":239},[187,1248,250],{"class":243},[187,1250,1251,1253],{"class":93,"line":253},[187,1252,247],{"class":239},[187,1254,258],{"class":243},[187,1256,1257],{"class":93,"line":261},[187,1258,265],{"emptyLinePlaceholder":264},[187,1260,1261,1263,1265,1267],{"class":93,"line":268},[187,1262,247],{"class":239},[187,1264,724],{"class":243},[187,1266,727],{"class":239},[187,1268,730],{"class":243},[187,1270,1271],{"class":93,"line":293},[187,1272,265],{"emptyLinePlaceholder":264},[187,1274,1275,1278,1281,1283,1286],{"class":93,"line":305},[187,1276,1277],{"class":239},"class",[187,1279,1280],{"class":192}," UnreadableSpreadsheet",[187,1282,935],{"class":243},[187,1284,1285],{"class":281},"RuntimeError",[187,1287,419],{"class":243},[187,1289,1290],{"class":93,"line":317},[187,1291,1292],{"class":239},"    pass\n",[187,1294,1295],{"class":93,"line":342},[187,1296,265],{"emptyLinePlaceholder":264},[187,1298,1299,1301,1304,1306,1308],{"class":93,"line":353},[187,1300,271],{"class":239},[187,1302,1303],{"class":274}," read_any",[187,1305,278],{"class":243},[187,1307,282],{"class":281},[187,1309,1310],{"class":243},") -> pd.DataFrame:\n",[187,1312,1313,1315,1317],{"class":93,"line":369},[187,1314,296],{"class":243},[187,1316,299],{"class":239},[187,1318,1319],{"class":243}," Path(path).resolve()\n",[187,1321,1322,1324,1326,1328,1330,1333,1336,1339,1341,1344],{"class":93,"line":377},[187,1323,385],{"class":243},[187,1325,299],{"class":239},[187,1327,390],{"class":243},[187,1329,393],{"class":281},[187,1331,1332],{"class":243},"] ",[187,1334,1335],{"class":239},"if",[187,1337,1338],{"class":243}," p.is_file() ",[187,1340,436],{"class":239},[187,1342,1343],{"class":239}," b",[187,1345,1346],{"class":196},"\"\"\n",[187,1348,1349],{"class":93,"line":382},[187,1350,265],{"emptyLinePlaceholder":264},[187,1352,1353,1355,1357,1359,1362],{"class":93,"line":399},[187,1354,308],{"class":239},[187,1356,404],{"class":243},[187,1358,407],{"class":239},[187,1360,1361],{"class":196},"\"PK\"",[187,1363,419],{"class":243},[187,1365,1366,1369,1371],{"class":93,"line":422},[187,1367,1368],{"class":239},"        if",[187,1370,311],{"class":239},[187,1372,1373],{"class":243}," zipfile.is_zipfile(p):\n",[187,1375,1376,1379,1382,1385,1387,1389,1391,1393,1396],{"class":93,"line":442},[187,1377,1378],{"class":239},"            raise",[187,1380,1381],{"class":243}," UnreadableSpreadsheet(",[187,1383,1384],{"class":239},"f",[187,1386,416],{"class":196},[187,1388,330],{"class":329},[187,1390,10],{"class":243},[187,1392,336],{"class":329},[187,1394,1395],{"class":196},": zip header but damaged archive — re-download\"",[187,1397,799],{"class":243},[187,1399,1400,1402,1405,1407,1409,1411],{"class":93,"line":460},[187,1401,320],{"class":239},[187,1403,1404],{"class":243}," pd.read_excel(p, ",[187,1406,791],{"class":780},[187,1408,299],{"class":239},[187,1410,796],{"class":196},[187,1412,799],{"class":243},[187,1414,1415,1417,1419,1421,1423,1425,1427],{"class":93,"line":468},[187,1416,308],{"class":239},[187,1418,404],{"class":243},[187,1420,407],{"class":239},[187,1422,416],{"class":196},[187,1424,453],{"class":329},[187,1426,416],{"class":196},[187,1428,419],{"class":243},[187,1430,1431,1433,1435,1437,1439,1442,1445],{"class":93,"line":502},[187,1432,320],{"class":239},[187,1434,1404],{"class":243},[187,1436,791],{"class":780},[187,1438,299],{"class":239},[187,1440,1441],{"class":196},"\"xlrd\"",[187,1443,1444],{"class":243},")          ",[187,1446,1447],{"class":752},"# legacy .xls\n",[187,1449,1450,1452,1454,1456,1458,1460,1462,1464,1466,1468,1470,1472],{"class":93,"line":510},[187,1451,308],{"class":239},[187,1453,473],{"class":243},[187,1455,476],{"class":281},[187,1457,479],{"class":243},[187,1459,482],{"class":239},[187,1461,485],{"class":243},[187,1463,407],{"class":239},[187,1465,490],{"class":196},[187,1467,210],{"class":243},[187,1469,407],{"class":239},[187,1471,497],{"class":196},[187,1473,419],{"class":243},[187,1475,1476,1478,1481,1483,1486],{"class":93,"line":524},[187,1477,320],{"class":239},[187,1479,1480],{"class":243}," pd.read_html(p)[",[187,1482,49],{"class":281},[187,1484,1485],{"class":243},"]                        ",[187,1487,1488],{"class":752},"# HTML table export\n",[187,1490,1491,1493,1496,1499,1501,1503,1505,1507],{"class":93,"line":532},[187,1492,308],{"class":239},[187,1494,1495],{"class":243}," head ",[187,1497,1498],{"class":239},"and",[187,1500,311],{"class":239},[187,1502,404],{"class":243},[187,1504,407],{"class":239},[187,1506,519],{"class":196},[187,1508,419],{"class":243},[187,1510,1511,1513,1516,1519,1521,1524,1526,1528,1530,1533,1535,1538,1540,1543],{"class":93,"line":565},[187,1512,320],{"class":239},[187,1514,1515],{"class":243}," pd.read_csv(p, ",[187,1517,1518],{"class":780},"sep",[187,1520,299],{"class":239},[187,1522,1523],{"class":281},"None",[187,1525,210],{"class":243},[187,1527,791],{"class":780},[187,1529,299],{"class":239},[187,1531,1532],{"class":196},"\"python\"",[187,1534,210],{"class":243},[187,1536,1537],{"class":780},"encoding",[187,1539,299],{"class":239},[187,1541,1542],{"class":196},"\"utf-8-sig\"",[187,1544,799],{"class":243},[187,1546,1547],{"class":93,"line":570},[187,1548,265],{"emptyLinePlaceholder":264},[187,1550,1552,1555,1557,1559,1561,1563,1565,1567,1570,1572,1574,1576,1578,1580],{"class":93,"line":1551},25,[187,1553,1554],{"class":239},"    raise",[187,1556,1381],{"class":243},[187,1558,1384],{"class":239},[187,1560,416],{"class":196},[187,1562,330],{"class":329},[187,1564,10],{"class":243},[187,1566,336],{"class":329},[187,1568,1569],{"class":196},": unrecognised content ",[187,1571,330],{"class":329},[187,1573,555],{"class":243},[187,1575,558],{"class":239},[187,1577,336],{"class":329},[187,1579,416],{"class":196},[187,1581,799],{"class":243},[10,1583,1584],{},"Then log the rejection rather than crashing the whole run:",[178,1586,1588],{"className":224,"code":1587,"language":226,"meta":183,"style":183},"frames, rejected = [], []\nfor f in sorted(Path(\"\u002Fmnt\u002Fshare\u002Fincoming\").glob(\"*.xlsx\")):\n    try:\n        frames.append(read_any(f))\n    except UnreadableSpreadsheet as exc:\n        rejected.append(str(exc))\n\nif rejected:\n    print(f\"{len(rejected)} file(s) skipped:\", *rejected, sep=\"\\n  \")\n",[13,1589,1590,1600,1628,1635,1640,1653,1663,1667,1674],{"__ignoreMap":183},[187,1591,1592,1595,1597],{"class":93,"line":189},[187,1593,1594],{"class":243},"frames, rejected ",[187,1596,299],{"class":239},[187,1598,1599],{"class":243}," [], []\n",[187,1601,1602,1605,1608,1610,1613,1616,1619,1622,1625],{"class":93,"line":236},[187,1603,1604],{"class":239},"for",[187,1606,1607],{"class":243}," f ",[187,1609,482],{"class":239},[187,1611,1612],{"class":281}," sorted",[187,1614,1615],{"class":243},"(Path(",[187,1617,1618],{"class":196},"\"\u002Fmnt\u002Fshare\u002Fincoming\"",[187,1620,1621],{"class":243},").glob(",[187,1623,1624],{"class":196},"\"*.xlsx\"",[187,1626,1627],{"class":243},")):\n",[187,1629,1630,1633],{"class":93,"line":253},[187,1631,1632],{"class":239},"    try",[187,1634,290],{"class":243},[187,1636,1637],{"class":93,"line":261},[187,1638,1639],{"class":243},"        frames.append(read_any(f))\n",[187,1641,1642,1645,1648,1650],{"class":93,"line":268},[187,1643,1644],{"class":239},"    except",[187,1646,1647],{"class":243}," UnreadableSpreadsheet ",[187,1649,727],{"class":239},[187,1651,1652],{"class":243}," exc:\n",[187,1654,1655,1658,1660],{"class":93,"line":293},[187,1656,1657],{"class":243},"        rejected.append(",[187,1659,282],{"class":281},[187,1661,1662],{"class":243},"(exc))\n",[187,1664,1665],{"class":93,"line":305},[187,1666,265],{"emptyLinePlaceholder":264},[187,1668,1669,1671],{"class":93,"line":317},[187,1670,1335],{"class":239},[187,1672,1673],{"class":243}," rejected:\n",[187,1675,1676,1678,1680,1682,1684,1686,1689,1692,1694,1697,1699,1702,1705,1707,1709,1711,1714,1717],{"class":93,"line":342},[187,1677,932],{"class":281},[187,1679,935],{"class":243},[187,1681,1384],{"class":239},[187,1683,416],{"class":196},[187,1685,330],{"class":329},[187,1687,1688],{"class":281},"len",[187,1690,1691],{"class":243},"(rejected)",[187,1693,336],{"class":329},[187,1695,1696],{"class":196}," file(s) skipped:\"",[187,1698,210],{"class":243},[187,1700,1701],{"class":239},"*",[187,1703,1704],{"class":243},"rejected, ",[187,1706,1518],{"class":780},[187,1708,299],{"class":239},[187,1710,416],{"class":196},[187,1712,1713],{"class":329},"\\n",[187,1715,1716],{"class":196},"  \"",[187,1718,799],{"class":243},[10,1720,1721,1722,1726],{},"That structure turns a hard stop into a report. The valid files still produce today's numbers, and the operations team gets a list of exactly which uploads to re-send. Pair it with the logging setup in ",[22,1723,1725],{"href":1724},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel script output to a file"," so those messages survive the cron session.",[166,1728,1730],{"id":1729},"verify-the-fix-before-you-trust-it","Verify the fix before you trust it",[10,1732,1733],{},"After converting or re-fetching a file, confirm the container is sound rather than assuming it. Three assertions cover everything this error touches — a valid archive, the expected sheet parts present, and a non-trivial row count:",[178,1735,1737],{"className":224,"code":1736,"language":226,"meta":183,"style":183},"import zipfile\nfrom openpyxl import load_workbook\n\npath = \"report_fixed.xlsx\"\nassert zipfile.is_zipfile(path), \"not a zip container\"\nwith zipfile.ZipFile(path) as zf:\n    assert any(n.startswith(\"xl\u002Fworksheets\u002F\") for n in zf.namelist()), \"no sheet parts\"\n\nwb = load_workbook(path, read_only=True)\nws = wb[wb.sheetnames[0]]\nprint(f\"{ws.max_row} rows x {ws.max_column} cols in {wb.sheetnames}\")\nwb.close()\n",[13,1738,1739,1745,1757,1761,1771,1782,1794,1823,1827,1847,1862,1903],{"__ignoreMap":183},[187,1740,1741,1743],{"class":93,"line":189},[187,1742,247],{"class":239},[187,1744,258],{"class":243},[187,1746,1747,1749,1752,1754],{"class":93,"line":236},[187,1748,240],{"class":239},[187,1750,1751],{"class":243}," openpyxl ",[187,1753,247],{"class":239},[187,1755,1756],{"class":243}," load_workbook\n",[187,1758,1759],{"class":93,"line":253},[187,1760,265],{"emptyLinePlaceholder":264},[187,1762,1763,1766,1768],{"class":93,"line":261},[187,1764,1765],{"class":243},"path ",[187,1767,299],{"class":239},[187,1769,1770],{"class":196}," \"report_fixed.xlsx\"\n",[187,1772,1773,1776,1779],{"class":93,"line":268},[187,1774,1775],{"class":239},"assert",[187,1777,1778],{"class":243}," zipfile.is_zipfile(path), ",[187,1780,1781],{"class":196},"\"not a zip container\"\n",[187,1783,1784,1786,1789,1791],{"class":93,"line":293},[187,1785,916],{"class":239},[187,1787,1788],{"class":243}," zipfile.ZipFile(path) ",[187,1790,727],{"class":239},[187,1792,1793],{"class":243}," zf:\n",[187,1795,1796,1799,1802,1805,1808,1810,1812,1815,1817,1820],{"class":93,"line":305},[187,1797,1798],{"class":239},"    assert",[187,1800,1801],{"class":281}," any",[187,1803,1804],{"class":243},"(n.startswith(",[187,1806,1807],{"class":196},"\"xl\u002Fworksheets\u002F\"",[187,1809,961],{"class":243},[187,1811,1604],{"class":239},[187,1813,1814],{"class":243}," n ",[187,1816,482],{"class":239},[187,1818,1819],{"class":243}," zf.namelist()), ",[187,1821,1822],{"class":196},"\"no sheet parts\"\n",[187,1824,1825],{"class":93,"line":317},[187,1826,265],{"emptyLinePlaceholder":264},[187,1828,1829,1832,1834,1837,1840,1842,1845],{"class":93,"line":342},[187,1830,1831],{"class":243},"wb ",[187,1833,299],{"class":239},[187,1835,1836],{"class":243}," load_workbook(path, ",[187,1838,1839],{"class":780},"read_only",[187,1841,299],{"class":239},[187,1843,1844],{"class":281},"True",[187,1846,799],{"class":243},[187,1848,1849,1852,1854,1857,1859],{"class":93,"line":353},[187,1850,1851],{"class":243},"ws ",[187,1853,299],{"class":239},[187,1855,1856],{"class":243}," wb[wb.sheetnames[",[187,1858,49],{"class":281},[187,1860,1861],{"class":243},"]]\n",[187,1863,1864,1866,1868,1870,1872,1874,1877,1879,1882,1884,1887,1889,1892,1894,1897,1899,1901],{"class":93,"line":369},[187,1865,573],{"class":281},[187,1867,935],{"class":243},[187,1869,1384],{"class":239},[187,1871,416],{"class":196},[187,1873,330],{"class":329},[187,1875,1876],{"class":243},"ws.max_row",[187,1878,336],{"class":329},[187,1880,1881],{"class":196}," rows x ",[187,1883,330],{"class":329},[187,1885,1886],{"class":243},"ws.max_column",[187,1888,336],{"class":329},[187,1890,1891],{"class":196}," cols in ",[187,1893,330],{"class":329},[187,1895,1896],{"class":243},"wb.sheetnames",[187,1898,336],{"class":329},[187,1900,416],{"class":196},[187,1902,799],{"class":243},[187,1904,1905],{"class":93,"line":377},[187,1906,1907],{"class":243},"wb.close()\n",[10,1909,1910,1913],{},[13,1911,1912],{},"read_only=True"," keeps the check cheap on a large workbook, and closing the handle matters on Windows, where an open file blocks the next step from replacing it.",[166,1915,1917],{"id":1916},"conclusion","Conclusion",[10,1919,1920,1922,1923,1925,1926,1928],{},[13,1921,1043],{}," is a statement about the container, never about your spreadsheet code. Read the first bytes to learn what the file really is, then apply the matching fix: a different reader for HTML or CSV, ",[13,1924,831],{}," or a conversion for legacy ",[13,1927,827],{},", decryption for a protected workbook, or a re-download for a truncated one. Wrap the check in a helper so every job in your codebase reports the filename and the true format instead of a stack trace from the zip module.",[166,1930,1932],{"id":1931},"frequently-asked-questions","Frequently asked questions",[10,1934,1935,1938,1939,1941,1942,1944,1945,1947],{},[703,1936,1937],{},"Why does openpyxl say \"File is not a zip file\" when Excel opens the file?","\nAn ",[13,1940,19],{}," is a zip archive of XML parts. Excel will happily open a legacy ",[13,1943,827],{},", an HTML table or a CSV that has been renamed ",[13,1946,19],{},", silently detecting the real format. openpyxl does not — it opens the container first, and a non-zip file fails immediately.",[10,1949,1950,1953,1954,1957],{},[703,1951,1952],{},"Can I repair the file from Python?","\nOnly if the zip structure is mostly intact. ",[13,1955,1956],{},"zipfile.ZipFile.testzip()"," names the first damaged member; if the central directory itself is gone, no pure-Python fix exists and you need the file re-exported from the source system.",[10,1959,1960,1963],{},[703,1961,1962],{},"Why is the error intermittent on a scheduled job?","\nThe job is racing a writer. A file appears on a share or in a sync folder as soon as its first bytes land, so a timer-triggered read can open a half-written archive. Wait for a stable file size or have the producer write to a temporary name and rename on completion.",[10,1965,1966,1969,1970,1972,1973,1976],{},[703,1967,1968],{},"Does a zero-byte file give the same error?","\nYes. An empty file is not a valid zip, so it raises ",[13,1971,1043],{}," rather than a clearer message. Check ",[13,1974,1975],{},"st_size"," before reading so the log says \"zero bytes\" instead.",[10,1978,1979,1982],{},[703,1980,1981],{},"Is BadZipFile ever caused by encryption?","\nYes. A password-protected workbook is an OLE2 container wrapping encrypted contents, not a zip, so openpyxl reports it as not a zip file. Decrypt it first with msoffcrypto-tool.",[166,1984,1986],{"id":1985},"related","Related",[1032,1988,1989,1997,2002,2009,2014],{},[1035,1990,1991,1040,1994,1996],{},[703,1992,1993],{},"Up:",[22,1995,25],{"href":24}," — the triage map this error sits on.",[1035,1998,1999,2001],{},[22,2000,836],{"href":835}," — what to do once the bytes reveal an OLE2 file.",[1035,2003,2004,2008],{},[22,2005,2007],{"href":2006},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-file-format-cannot-be-determined-in-pandas\u002F","Fix \"Excel file format cannot be determined\" in pandas"," — the sibling error raised one layer higher, in pandas' engine lookup.",[1035,2010,2011,2013],{},[22,2012,841],{"href":840}," — converting the legacy file once instead of special-casing it forever.",[1035,2015,2016,2020],{},[22,2017,2019],{"href":2018},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhow-to-read-excel-with-pandas-step-by-step\u002F","How to read Excel with pandas step by step"," — the normal read path, once the file is genuinely a workbook.",[2022,2023,2024],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":183,"searchDepth":236,"depth":236,"links":2026},[2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037],{"id":168,"depth":236,"text":169},{"id":217,"depth":236,"text":218},{"id":592,"depth":236,"text":593},{"id":854,"depth":236,"text":855},{"id":1029,"depth":236,"text":1030},{"id":1095,"depth":236,"text":1096},{"id":1224,"depth":236,"text":1225},{"id":1729,"depth":236,"text":1730},{"id":1916,"depth":236,"text":1917},{"id":1931,"depth":236,"text":1932},{"id":1985,"depth":236,"text":1986},"2026-08-27","zipfile.BadZipFile: File is not a zip file — why openpyxl and pandas raise it, how to identify the real format, and how to recover the data or repair the workbook.","md",[2042,2044,2046,2048,2050],{"q":1937,"a":2043},"An .xlsx is a zip archive of XML parts. Excel will happily open a legacy .xls, an HTML table or a CSV that has been renamed .xlsx, silently detecting the real format. openpyxl does not — it opens the container first, and a non-zip file fails immediately.",{"q":1952,"a":2045},"Only if the zip structure is mostly intact. zipfile.ZipFile.testzip() names the first damaged member; if the central directory itself is gone, no pure-Python fix exists and you need the file re-exported from the source system.",{"q":1962,"a":2047},"The job is racing a writer. A file appears on a share or in a sync folder as soon as its first bytes land, so a timer-triggered read can open a half-written archive. Wait for a stable file size or have the producer write to a temporary name and rename on completion.",{"q":1968,"a":2049},"Yes. An empty file is not a valid zip, so it raises BadZipFile rather than a clearer message. Check st_size before reading so the log says \"zero bytes\" instead.",{"q":1981,"a":2051},"Yes. A password-protected workbook is an OLE2 container wrapping encrypted contents, not a zip, so openpyxl reports it as not a zip file. Decrypt it first with msoffcrypto-tool.",{},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python",{"title":2055,"description":2056},"Fix zipfile.BadZipFile Reading Excel","Diagnose and fix BadZipFile in Python: check the magic bytes, spot HTML or CSV renamed to .xlsx, handle truncated downloads and mid-write reads, and salvage the data.","fix-badzipfile-error-when-reading-excel-in-python","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002Findex","how-to","7e1KTDC6ebwXIV2f_TRczUk3tUmAyMiOwWe1hatf39k",[2062,2065],{"title":25,"path":2063,"stem":2064,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Findex",{"title":2066,"path":2067,"stem":2068,"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",1788710154009]