[{"data":1,"prerenderedAt":1589},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format":1580},{"id":4,"title":5,"body":6,"dateModified":1554,"datePublished":1555,"description":1556,"extension":1557,"faq":1558,"meta":1571,"navigation":222,"path":1572,"seo":1573,"slug":1576,"stem":1577,"type":1578,"__hash__":1579},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format\u002Findex.md","Fix \"openpyxl does not support the old .xls file format\"",{"type":7,"value":8,"toc":1541},"minimark",[9,31,136,141,144,174,187,191,194,264,267,359,362,430,445,449,460,740,755,868,872,888,902,974,981,985,991,1079,1085,1089,1165,1169,1184,1188,1191,1404,1407,1411,1423,1427,1442,1460,1476,1485,1491,1495,1537],[10,11,12,16,17,20,21,24,25,30],"p",{},[13,14,15],"code",{},"openpyxl does not support the old .xls file format, please use xlrd to read this file, or convert it to the more recent .xlsx file format"," is an unusually helpful error message — it names the cause and two fixes in one line. Underneath it is a hard fact: ",[13,18,19],{},".xls"," and ",[13,22,23],{},".xlsx"," are different file formats that happen to share three letters. This guide reads the legacy file, converts it properly, and adds the check that stops the error reaching production at all. It is one branch of ",[26,27,29],"a",{"href":28},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[32,33,41,42,41,46,41,50,41,57,41,64,41,69,41,77,41,85,41,91,41,94,41,98,41,102,41,107,41,112,41,116,41,120,41,122,41,125,41,128,41,132],"svg",{"viewBox":34,"role":35,"ariaLabelledBy":36,"xmlns":39,"style":40},"0 0 760 250","img",[37,38],"xls-t","xls-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  ",[43,44,45],"title",{"id":37},"Two unrelated file formats behind one familiar icon",[47,48,49],"desc",{"id":38},"A legacy xls is an OLE2 compound document of binary BIFF records read by xlrd; a modern xlsx is a zip of XML parts read by openpyxl. Neither library can read the other format.",[51,52],"rect",{"x":53,"y":53,"width":54,"height":55,"fill":56},"0","760","250","#ffffff",[58,59,63],"text",{"x":60,"y":61,"style":62},"196","28","font-size:13px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle",".xls  (1997–2003)",[58,65,68],{"x":66,"y":61,"style":67},"566","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",".xlsx  (2007 onward)",[70,71],"line",{"x1":72,"y1":73,"x2":72,"y2":74,"stroke":75,"style":76},"380","40","234","var(--line,#cdd5e6)","stroke-width:1px",[51,78],{"x":79,"y":80,"width":81,"height":82,"rx":83,"fill":84,"stroke":75},"34","46","322","52","10","#f0f2f5",[58,86,90],{"x":87,"y":88,"style":89},"195","78","font-size:12.5px;fill:var(--text,#172033);text-anchor:middle","OLE2 compound document",[51,92],{"x":79,"y":93,"width":81,"height":82,"rx":83,"fill":84,"stroke":75},"106",[58,95,97],{"x":87,"y":96,"style":89},"138","binary BIFF8 records",[51,99],{"x":79,"y":100,"width":81,"height":82,"rx":83,"fill":101,"stroke":75},"166","#fdefd8",[58,103,106],{"x":87,"y":104,"style":105},"190","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","read by xlrd or calamine",[58,108,111],{"x":87,"y":109,"style":110},"210","font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","65,536 rows maximum",[51,113],{"x":114,"y":80,"width":81,"height":82,"rx":83,"fill":115,"stroke":75},"404","#ebebfd",[58,117,119],{"x":118,"y":88,"style":89},"565","zip container",[51,121],{"x":114,"y":93,"width":81,"height":82,"rx":83,"fill":115,"stroke":75},[58,123,124],{"x":118,"y":96,"style":89},"XML parts per sheet",[51,126],{"x":114,"y":100,"width":81,"height":82,"rx":83,"fill":127,"stroke":75},"#d9f4f1",[58,129,131],{"x":118,"y":104,"style":130},"font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","read by openpyxl or calamine",[58,133,135],{"x":118,"y":109,"style":134},"font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","1,048,576 rows maximum",[137,138,140],"h2",{"id":139},"prerequisites","Prerequisites",[10,142,143],{},"Install a reader for the legacy format alongside pandas:",[145,146,151],"pre",{"className":147,"code":148,"language":149,"meta":150,"style":150},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl xlrd\n","bash","",[13,152,153],{"__ignoreMap":150},[154,155,157,161,165,168,171],"span",{"class":70,"line":156},1,[154,158,160],{"class":159},"sMTad","pip",[154,162,164],{"class":163},"srMev"," install",[154,166,167],{"class":163}," pandas",[154,169,170],{"class":163}," openpyxl",[154,172,173],{"class":163}," xlrd\n",[10,175,176,179,180,182,183,186],{},[13,177,178],{},"xlrd"," 2.x reads ",[13,181,19],{}," only — that restriction is deliberate. If you would rather carry one dependency that reads every format, ",[13,184,185],{},"python-calamine"," is a good alternative and is covered below.",[137,188,190],{"id":189},"read-the-file-as-it-is","Read the file as it is",[10,192,193],{},"The minimal fix is to name the engine. pandas guesses from the extension, and the guess is right here — but being explicit documents the intent and survives a file arriving without an extension:",[145,195,199],{"className":196,"code":197,"language":198,"meta":150,"style":150},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.read_excel(\"legacy_report.xls\", engine=\"xlrd\")\nprint(df.head())\n","python",[13,200,201,217,224,254],{"__ignoreMap":150},[154,202,203,207,211,214],{"class":70,"line":156},[154,204,206],{"class":205},"s-kum","import",[154,208,210],{"class":209},"skGVy"," pandas ",[154,212,213],{"class":205},"as",[154,215,216],{"class":209}," pd\n",[154,218,220],{"class":70,"line":219},2,[154,221,223],{"emptyLinePlaceholder":222},true,"\n",[154,225,227,230,233,236,239,242,246,248,251],{"class":70,"line":226},3,[154,228,229],{"class":209},"df ",[154,231,232],{"class":205},"=",[154,234,235],{"class":209}," pd.read_excel(",[154,237,238],{"class":163},"\"legacy_report.xls\"",[154,240,241],{"class":209},", ",[154,243,245],{"class":244},"sa561","engine",[154,247,232],{"class":205},[154,249,250],{"class":163},"\"xlrd\"",[154,252,253],{"class":209},")\n",[154,255,257,261],{"class":70,"line":256},4,[154,258,260],{"class":259},"sP0c6","print",[154,262,263],{"class":209},"(df.head())\n",[10,265,266],{},"For a workbook with several sheets, read them all at once and get a dictionary keyed by sheet name:",[145,268,270],{"className":196,"code":269,"language":198,"meta":150,"style":150},"sheets = pd.read_excel(\"legacy_report.xls\", sheet_name=None, engine=\"xlrd\")\nfor name, frame in sheets.items():\n    print(f\"{name}: {len(frame)} rows\")\n",[13,271,272,303,317],{"__ignoreMap":150},[154,273,274,277,279,281,283,285,288,290,293,295,297,299,301],{"class":70,"line":156},[154,275,276],{"class":209},"sheets ",[154,278,232],{"class":205},[154,280,235],{"class":209},[154,282,238],{"class":163},[154,284,241],{"class":209},[154,286,287],{"class":244},"sheet_name",[154,289,232],{"class":205},[154,291,292],{"class":259},"None",[154,294,241],{"class":209},[154,296,245],{"class":244},[154,298,232],{"class":205},[154,300,250],{"class":163},[154,302,253],{"class":209},[154,304,305,308,311,314],{"class":70,"line":219},[154,306,307],{"class":205},"for",[154,309,310],{"class":209}," name, frame ",[154,312,313],{"class":205},"in",[154,315,316],{"class":209}," sheets.items():\n",[154,318,319,322,325,328,331,335,338,341,344,346,349,352,354,357],{"class":70,"line":226},[154,320,321],{"class":259},"    print",[154,323,324],{"class":209},"(",[154,326,327],{"class":205},"f",[154,329,330],{"class":163},"\"",[154,332,334],{"class":333},"sSjpA","{",[154,336,337],{"class":209},"name",[154,339,340],{"class":333},"}",[154,342,343],{"class":163},": ",[154,345,334],{"class":333},[154,347,348],{"class":259},"len",[154,350,351],{"class":209},"(frame)",[154,353,340],{"class":333},[154,355,356],{"class":163}," rows\"",[154,358,253],{"class":209},[10,360,361],{},"If you need cell-level access rather than a DataFrame — merged regions, per-cell formats, the sheet's own date mode — use xlrd directly:",[145,363,365],{"className":196,"code":364,"language":198,"meta":150,"style":150},"import xlrd\n\nbook = xlrd.open_workbook(\"legacy_report.xls\")\nsheet = book.sheet_by_index(0)\nprint(sheet.nrows, sheet.ncols)\nprint(sheet.cell_value(0, 0))\n",[13,366,367,373,377,391,405,413],{"__ignoreMap":150},[154,368,369,371],{"class":70,"line":156},[154,370,206],{"class":205},[154,372,173],{"class":209},[154,374,375],{"class":70,"line":219},[154,376,223],{"emptyLinePlaceholder":222},[154,378,379,382,384,387,389],{"class":70,"line":226},[154,380,381],{"class":209},"book ",[154,383,232],{"class":205},[154,385,386],{"class":209}," xlrd.open_workbook(",[154,388,238],{"class":163},[154,390,253],{"class":209},[154,392,393,396,398,401,403],{"class":70,"line":256},[154,394,395],{"class":209},"sheet ",[154,397,232],{"class":205},[154,399,400],{"class":209}," book.sheet_by_index(",[154,402,53],{"class":259},[154,404,253],{"class":209},[154,406,408,410],{"class":70,"line":407},5,[154,409,260],{"class":259},[154,411,412],{"class":209},"(sheet.nrows, sheet.ncols)\n",[154,414,416,418,421,423,425,427],{"class":70,"line":415},6,[154,417,260],{"class":259},[154,419,420],{"class":209},"(sheet.cell_value(",[154,422,53],{"class":259},[154,424,241],{"class":209},[154,426,53],{"class":259},[154,428,429],{"class":209},"))\n",[10,431,432,433,436,437,440,441,30],{},"Note that xlrd returns dates as floats. Convert them with ",[13,434,435],{},"xlrd.xldate_as_datetime(value, book.datemode)","; the ",[13,438,439],{},"datemode"," matters because workbooks saved on classic Macs use a 1904 epoch. The same trap in a pandas context is covered in ",[26,442,444],{"href":443},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates\u002F","Fix Excel serial numbers showing instead of dates",[137,446,448],{"id":447},"convert-once-instead-of-special-casing-forever","Convert once instead of special-casing forever",[10,450,451,452,456,457,459],{},"If the file will be read more than a handful of times, convert it. Everything downstream — formatting, charts, tables, ",[26,453,455],{"href":454},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","conditional formatting"," — needs ",[13,458,23],{},", and the conversion removes both the extra dependency and the 65,536-row ceiling.",[145,461,463],{"className":196,"code":462,"language":198,"meta":150,"style":150},"\"\"\"Convert every .xls in a folder to .xlsx, preserving sheet names.\"\"\"\nfrom pathlib import Path\n\nimport pandas as pd\n\nsrc_dir = Path(\"legacy\")\nout_dir = Path(\"converted\")\nout_dir.mkdir(exist_ok=True)\n\nfor src in sorted(src_dir.glob(\"*.xls\")):\n    sheets = pd.read_excel(src, sheet_name=None, engine=\"xlrd\")\n    dst = out_dir \u002F f\"{src.stem}.xlsx\"\n    with pd.ExcelWriter(dst, engine=\"openpyxl\") as writer:\n        for name, frame in sheets.items():\n            frame.to_excel(writer, sheet_name=name[:31], index=False)\n    print(f\"{src.name} -> {dst.name}  ({len(sheets)} sheet(s))\")\n",[13,464,465,470,483,487,497,501,516,531,547,552,574,601,630,654,666,695],{"__ignoreMap":150},[154,466,467],{"class":70,"line":156},[154,468,469],{"class":163},"\"\"\"Convert every .xls in a folder to .xlsx, preserving sheet names.\"\"\"\n",[154,471,472,475,478,480],{"class":70,"line":219},[154,473,474],{"class":205},"from",[154,476,477],{"class":209}," pathlib ",[154,479,206],{"class":205},[154,481,482],{"class":209}," Path\n",[154,484,485],{"class":70,"line":226},[154,486,223],{"emptyLinePlaceholder":222},[154,488,489,491,493,495],{"class":70,"line":256},[154,490,206],{"class":205},[154,492,210],{"class":209},[154,494,213],{"class":205},[154,496,216],{"class":209},[154,498,499],{"class":70,"line":407},[154,500,223],{"emptyLinePlaceholder":222},[154,502,503,506,508,511,514],{"class":70,"line":415},[154,504,505],{"class":209},"src_dir ",[154,507,232],{"class":205},[154,509,510],{"class":209}," Path(",[154,512,513],{"class":163},"\"legacy\"",[154,515,253],{"class":209},[154,517,519,522,524,526,529],{"class":70,"line":518},7,[154,520,521],{"class":209},"out_dir ",[154,523,232],{"class":205},[154,525,510],{"class":209},[154,527,528],{"class":163},"\"converted\"",[154,530,253],{"class":209},[154,532,534,537,540,542,545],{"class":70,"line":533},8,[154,535,536],{"class":209},"out_dir.mkdir(",[154,538,539],{"class":244},"exist_ok",[154,541,232],{"class":205},[154,543,544],{"class":259},"True",[154,546,253],{"class":209},[154,548,550],{"class":70,"line":549},9,[154,551,223],{"emptyLinePlaceholder":222},[154,553,555,557,560,562,565,568,571],{"class":70,"line":554},10,[154,556,307],{"class":205},[154,558,559],{"class":209}," src ",[154,561,313],{"class":205},[154,563,564],{"class":259}," sorted",[154,566,567],{"class":209},"(src_dir.glob(",[154,569,570],{"class":163},"\"*.xls\"",[154,572,573],{"class":209},")):\n",[154,575,577,580,582,585,587,589,591,593,595,597,599],{"class":70,"line":576},11,[154,578,579],{"class":209},"    sheets ",[154,581,232],{"class":205},[154,583,584],{"class":209}," pd.read_excel(src, ",[154,586,287],{"class":244},[154,588,232],{"class":205},[154,590,292],{"class":259},[154,592,241],{"class":209},[154,594,245],{"class":244},[154,596,232],{"class":205},[154,598,250],{"class":163},[154,600,253],{"class":209},[154,602,604,607,609,612,615,618,620,622,625,627],{"class":70,"line":603},12,[154,605,606],{"class":209},"    dst ",[154,608,232],{"class":205},[154,610,611],{"class":209}," out_dir ",[154,613,614],{"class":205},"\u002F",[154,616,617],{"class":205}," f",[154,619,330],{"class":163},[154,621,334],{"class":333},[154,623,624],{"class":209},"src.stem",[154,626,340],{"class":333},[154,628,629],{"class":163},".xlsx\"\n",[154,631,633,636,639,641,643,646,649,651],{"class":70,"line":632},13,[154,634,635],{"class":205},"    with",[154,637,638],{"class":209}," pd.ExcelWriter(dst, ",[154,640,245],{"class":244},[154,642,232],{"class":205},[154,644,645],{"class":163},"\"openpyxl\"",[154,647,648],{"class":209},") ",[154,650,213],{"class":205},[154,652,653],{"class":209}," writer:\n",[154,655,657,660,662,664],{"class":70,"line":656},14,[154,658,659],{"class":205},"        for",[154,661,310],{"class":209},[154,663,313],{"class":205},[154,665,316],{"class":209},[154,667,669,672,674,676,679,682,685,688,690,693],{"class":70,"line":668},15,[154,670,671],{"class":209},"            frame.to_excel(writer, ",[154,673,287],{"class":244},[154,675,232],{"class":205},[154,677,678],{"class":209},"name[:",[154,680,681],{"class":259},"31",[154,683,684],{"class":209},"], ",[154,686,687],{"class":244},"index",[154,689,232],{"class":205},[154,691,692],{"class":259},"False",[154,694,253],{"class":209},[154,696,698,700,702,704,706,708,711,713,716,718,721,723,726,728,730,733,735,738],{"class":70,"line":697},16,[154,699,321],{"class":259},[154,701,324],{"class":209},[154,703,327],{"class":205},[154,705,330],{"class":163},[154,707,334],{"class":333},[154,709,710],{"class":209},"src.name",[154,712,340],{"class":333},[154,714,715],{"class":163}," -> ",[154,717,334],{"class":333},[154,719,720],{"class":209},"dst.name",[154,722,340],{"class":333},[154,724,725],{"class":163},"  (",[154,727,334],{"class":333},[154,729,348],{"class":259},[154,731,732],{"class":209},"(sheets)",[154,734,340],{"class":333},[154,736,737],{"class":163}," sheet(s))\"",[154,739,253],{"class":209},[10,741,742,743,746,747,750,751,30],{},"Two details keep this safe: ",[13,744,745],{},"sheet_name=None"," preserves every sheet rather than only the first, and ",[13,748,749],{},"name[:31]"," respects Excel's sheet-name limit so the write cannot produce a file Excel offers to repair. A pandas round trip carries values, not formulas or macros — when you need a faithful conversion, drive LibreOffice headlessly instead, as shown in ",[26,752,754],{"href":753},"\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",[32,756,41,761,41,764,41,767,41,770,41,775,41,778,41,783,41,786,41,790,41,797,41,802,41,805,41,808,41,813,41,818,41,823,41,827,41,830,41,833,41,837,41,841,41,846,41,850,41,854,41,859,41,861,41,864],{"viewBox":757,"role":35,"ariaLabelledBy":758,"xmlns":39,"style":40},"0 0 760 226",[759,760],"xls2-t","xls2-d",[43,762,763],{"id":759},"Convert once at the boundary, then use one code path",[47,765,766],{"id":760},"Legacy files are converted to xlsx as they arrive, so every downstream step — cleaning, formatting, charting and delivery — sees a single modern format instead of branching on the extension.",[51,768],{"x":53,"y":53,"width":54,"height":769,"fill":56},"226",[58,771,774],{"x":72,"y":772,"style":773},"26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","One conversion at the edge removes every later branch",[51,776],{"x":61,"y":80,"width":777,"height":82,"rx":83,"fill":84,"stroke":75},"140",[58,779,782],{"x":780,"y":88,"style":781},"98","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",".xls inbox",[51,784],{"x":61,"y":785,"width":777,"height":82,"rx":83,"fill":84,"stroke":75},"118",[58,787,789],{"x":780,"y":788,"style":781},"150",".xlsx inbox",[70,791],{"x1":792,"y1":793,"x2":794,"y2":780,"stroke":795,"style":796},"168","72","204","var(--brand,#5b5cf0)","stroke-width:2px",[798,799],"polygon",{"points":800,"fill":801},"204,98 192,95 197,87","#5b5cf0",[70,803],{"x1":792,"y1":804,"x2":794,"y2":785,"stroke":795,"style":796},"144",[798,806],{"points":807,"fill":801},"204,118 197,129 192,121",[51,809],{"x":810,"y":811,"width":812,"height":82,"rx":83,"fill":115,"stroke":75},"208","82","160",[58,814,817],{"x":815,"y":93,"style":816},"288","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","normalise step",[58,819,822],{"x":815,"y":820,"style":821},"124","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","route on magic bytes",[70,824],{"x1":825,"y1":826,"x2":114,"y2":826,"stroke":795,"style":796},"368","108",[798,828],{"points":829,"fill":801},"404,108 394,103 394,113",[51,831],{"x":832,"y":811,"width":777,"height":82,"rx":83,"fill":127,"stroke":75},"408",[58,834,836],{"x":835,"y":93,"style":130},"478","one format",[58,838,840],{"x":835,"y":820,"style":839},"font-size:11px;fill:var(--teal-ink,#0b6157);text-anchor:middle",".xlsx only",[70,842],{"x1":843,"y1":826,"x2":844,"y2":826,"stroke":845,"style":796},"548","584","var(--teal,#0f9488)",[798,847],{"points":848,"fill":849},"584,108 574,103 574,113","#0f766e",[51,851],{"x":852,"y":80,"width":853,"height":82,"rx":83,"fill":101,"stroke":75},"588","148",[58,855,858],{"x":856,"y":88,"style":857},"662","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","clean and style",[51,860],{"x":852,"y":785,"width":853,"height":82,"rx":83,"fill":101,"stroke":75},[58,862,863],{"x":856,"y":788,"style":857},"chart and deliver",[58,865,867],{"x":72,"y":794,"style":866},"font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","Downstream code never asks which format it is reading",[137,869,871],{"id":870},"read-every-format-with-one-engine","Read every format with one engine",[10,873,874,875,877,878,880,881,884,885,887],{},"If your inputs are a mixed bag — some ",[13,876,19],{},", some ",[13,879,23],{},", the occasional ",[13,882,883],{},".xlsb"," — ",[13,886,185],{}," reads all of them through a single Rust-backed engine, and it is usually faster than either pure-Python reader:",[145,889,891],{"className":147,"code":890,"language":149,"meta":150,"style":150},"pip install python-calamine\n",[13,892,893],{"__ignoreMap":150},[154,894,895,897,899],{"class":70,"line":156},[154,896,160],{"class":159},[154,898,164],{"class":163},[154,900,901],{"class":163}," python-calamine\n",[145,903,905],{"className":196,"code":904,"language":198,"meta":150,"style":150},"import pandas as pd\n\nfor name in (\"legacy_report.xls\", \"modern_report.xlsx\", \"binary_report.xlsb\"):\n    df = pd.read_excel(name, engine=\"calamine\")\n    print(name, df.shape)\n",[13,906,907,917,921,948,967],{"__ignoreMap":150},[154,908,909,911,913,915],{"class":70,"line":156},[154,910,206],{"class":205},[154,912,210],{"class":209},[154,914,213],{"class":205},[154,916,216],{"class":209},[154,918,919],{"class":70,"line":219},[154,920,223],{"emptyLinePlaceholder":222},[154,922,923,925,928,930,933,935,937,940,942,945],{"class":70,"line":226},[154,924,307],{"class":205},[154,926,927],{"class":209}," name ",[154,929,313],{"class":205},[154,931,932],{"class":209}," (",[154,934,238],{"class":163},[154,936,241],{"class":209},[154,938,939],{"class":163},"\"modern_report.xlsx\"",[154,941,241],{"class":209},[154,943,944],{"class":163},"\"binary_report.xlsb\"",[154,946,947],{"class":209},"):\n",[154,949,950,953,955,958,960,962,965],{"class":70,"line":256},[154,951,952],{"class":209},"    df ",[154,954,232],{"class":205},[154,956,957],{"class":209}," pd.read_excel(name, ",[154,959,245],{"class":244},[154,961,232],{"class":205},[154,963,964],{"class":163},"\"calamine\"",[154,966,253],{"class":209},[154,968,969,971],{"class":70,"line":407},[154,970,321],{"class":259},[154,972,973],{"class":209},"(name, df.shape)\n",[10,975,976,977,30],{},"That collapses the engine table to one line of code. The trade-off is that calamine reads values only — no styles, no charts, no writing — so keep openpyxl for anything that produces or edits a workbook. Its speed characteristics are measured in ",[26,978,980],{"href":979},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fspeed-up-pandas-excel-reads-with-the-calamine-engine\u002F","Speed up pandas Excel reads with the calamine engine",[137,982,984],{"id":983},"choose-between-reading-converting-and-replacing-the-source","Choose between reading, converting and replacing the source",[10,986,987,988,990],{},"Three responses to a legacy file are all defensible, and the right one depends on who owns the producer. Reading in place is the smallest change but leaves an extra dependency in every environment. Converting at ingest costs one script and pays back on every later step. Getting the source system to emit ",[13,989,23],{}," is the only fix that removes the problem entirely — worth asking for when the export is a config option rather than a code change.",[32,992,41,997,41,1000,41,1003,41,1006,41,1009,41,1015,41,1019,41,1024,41,1027,41,1030,41,1034,41,1037,41,1042,41,1045,41,1048,41,1051,41,1054,41,1058,41,1063,41,1066,41,1069,41,1072,41,1075],{"viewBox":993,"role":35,"ariaLabelledBy":994,"xmlns":39,"style":40},"0 0 760 232",[995,996],"xls3-t","xls3-d",[43,998,999],{"id":995},"Cost and reach of the three responses to a legacy .xls",[47,1001,1002],{"id":996},"Reading in place is cheapest but keeps the dependency, converting at ingest costs one script and unlocks every downstream feature, and changing the source system removes the problem permanently.",[51,1004],{"x":53,"y":53,"width":54,"height":1005,"fill":56},"232",[58,1007,1008],{"x":72,"y":772,"style":773},"Effort now versus friction later",[51,1010],{"x":1011,"y":1012,"width":1013,"height":853,"rx":1014,"fill":115,"stroke":75},"30","44","222","12",[58,1016,1018],{"x":1017,"y":793,"style":67},"141","Read in place",[58,1020,1023],{"x":1017,"y":1021,"style":1022},"100","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","one engine= argument",[58,1025,1026],{"x":1017,"y":820,"style":866},"xlrd in every environment",[58,1028,1029],{"x":1017,"y":853,"style":866},"no styling or charts",[58,1031,1033],{"x":1017,"y":1032,"style":866},"172","65,536-row ceiling stays",[51,1035],{"x":1036,"y":1012,"width":1013,"height":853,"rx":1014,"fill":127,"stroke":75},"268",[58,1038,1041],{"x":1039,"y":793,"style":1040},"379","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Convert at ingest",[58,1043,1044],{"x":1039,"y":1021,"style":1022},"one script, run once",[58,1046,1047],{"x":1039,"y":820,"style":134},"all later tools available",[58,1049,1050],{"x":1039,"y":853,"style":134},"row limit lifted",[58,1052,1053],{"x":1039,"y":1032,"style":866},"macros need .xlsm",[51,1055],{"x":1056,"y":1012,"width":1057,"height":853,"rx":1014,"fill":101,"stroke":75},"506","224",[58,1059,1062],{"x":1060,"y":793,"style":1061},"618","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Fix the source",[58,1064,1065],{"x":1060,"y":1021,"style":1022},"often a config toggle",[58,1067,1068],{"x":1060,"y":820,"style":110},"problem gone for good",[58,1070,1071],{"x":1060,"y":853,"style":866},"needs another team",[58,1073,1074],{"x":1060,"y":1032,"style":866},"slowest to land",[58,1076,1078],{"x":72,"y":1077,"style":866},"218","Convert at ingest unless the file is read once and thrown away",[10,1080,1081,1082,1084],{},"In practice most teams end up with a hybrid: convert at ingest today, and ask the upstream owner to change the export format in parallel. The conversion script then becomes a no-op the day the source starts producing ",[13,1083,23],{},", because the byte-sniffing reader above already handles both.",[137,1086,1088],{"id":1087},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1090,1091,1092,1106,1130,1139,1145],"ul",{},[1093,1094,1095,1105],"li",{},[1096,1097,1098,1099,1102,1103,30],"strong",{},"Passing ",[13,1100,1101],{},"engine=\"xlrd\""," to an ",[13,1104,23],{}," This is the mirror-image error and appears in a lot of pre-2020 tutorials. Since xlrd 2.0 it raises immediately; use openpyxl for modern files.",[1093,1107,1108,1111,1112,1114,1115,1118,1119,1121,1122,1125,1126,30],{},[1096,1109,1110],{},"Trusting the extension."," A file named ",[13,1113,19],{}," that starts with ",[13,1116,1117],{},"PK"," is really an ",[13,1120,23],{},", and one that starts with ",[13,1123,1124],{},"\u003Chtml"," is a web export. Check the bytes — see ",[26,1127,1129],{"href":1128},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002F","Fix BadZipFile when reading an Excel file in Python",[1093,1131,1132,1135,1136,1138],{},[1096,1133,1134],{},"Losing precision on dates."," xlrd hands back serial floats; converting with the wrong ",[13,1137,439],{}," shifts every date by four years and a day.",[1093,1140,1141,1144],{},[1096,1142,1143],{},"Hitting the row ceiling."," A legacy sheet cannot hold more than 65,536 rows, so a large export may already be truncated at source. Compare the row count against the system that produced it.",[1093,1146,1147,1150,1151,1153,1154,1157,1158,1160,1161,30],{},[1096,1148,1149],{},"Assuming conversion preserves macros."," It does not. A ",[13,1152,19],{}," with VBA becomes ",[13,1155,1156],{},".xlsm",", not ",[13,1159,23],{},"; see ",[26,1162,1164],{"href":1163},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002F","Work with macro-enabled xlsm files in openpyxl",[137,1166,1168],{"id":1167},"performance-and-scale-notes","Performance and scale notes",[10,1170,1171,1172,1175,1176,1178,1179,1183],{},"xlrd parses the whole workbook into memory — there is no streaming mode, and no equivalent of openpyxl's ",[13,1173,1174],{},"read_only=True",". A 60,000-row ",[13,1177,19],{}," typically costs a few hundred megabytes while parsing, so converting large legacy files in a loop can exhaust a small container. Two mitigations work well: convert one file per process so memory is reclaimed between files, or switch the read to calamine, whose parser is both faster and markedly leaner. For folders of hundreds of files, the parallel pattern in ",[26,1180,1182],{"href":1181},"\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"," applies unchanged — the conversion is CPU-bound, so processes beat threads.",[137,1185,1187],{"id":1186},"detect-the-mismatch-before-it-fails","Detect the mismatch before it fails",[10,1189,1190],{},"The durable fix is to stop dispatching on the filename. This helper picks the engine from the file's own bytes, so a mislabelled export is read correctly instead of raising:",[145,1192,1194],{"className":196,"code":1193,"language":198,"meta":150,"style":150},"\"\"\"Choose a pandas engine from content, not from the extension.\"\"\"\nfrom pathlib import Path\n\nimport pandas as pd\n\ndef read_spreadsheet(path: str) -> pd.DataFrame:\n    head = Path(path).read_bytes()[:4]\n    if head == b\"\\xd0\\xcf\\x11\\xe0\":          # OLE2 -> legacy .xls\n        return pd.read_excel(path, engine=\"xlrd\")\n    if head[:2] == b\"PK\":                     # zip -> .xlsx\u002F.xlsm\u002F.ods\n        return pd.read_excel(path, engine=\"openpyxl\")\n    raise ValueError(f\"{path}: not a spreadsheet ({head!r})\")\n\ndf = read_spreadsheet(\"mystery_export.xls\")\n",[13,1195,1196,1201,1211,1215,1225,1229,1247,1263,1291,1307,1333,1347,1386,1390],{"__ignoreMap":150},[154,1197,1198],{"class":70,"line":156},[154,1199,1200],{"class":163},"\"\"\"Choose a pandas engine from content, not from the extension.\"\"\"\n",[154,1202,1203,1205,1207,1209],{"class":70,"line":219},[154,1204,474],{"class":205},[154,1206,477],{"class":209},[154,1208,206],{"class":205},[154,1210,482],{"class":209},[154,1212,1213],{"class":70,"line":226},[154,1214,223],{"emptyLinePlaceholder":222},[154,1216,1217,1219,1221,1223],{"class":70,"line":256},[154,1218,206],{"class":205},[154,1220,210],{"class":209},[154,1222,213],{"class":205},[154,1224,216],{"class":209},[154,1226,1227],{"class":70,"line":407},[154,1228,223],{"emptyLinePlaceholder":222},[154,1230,1231,1234,1238,1241,1244],{"class":70,"line":415},[154,1232,1233],{"class":205},"def",[154,1235,1237],{"class":1236},"s_Opv"," read_spreadsheet",[154,1239,1240],{"class":209},"(path: ",[154,1242,1243],{"class":259},"str",[154,1245,1246],{"class":209},") -> pd.DataFrame:\n",[154,1248,1249,1252,1254,1257,1260],{"class":70,"line":518},[154,1250,1251],{"class":209},"    head ",[154,1253,232],{"class":205},[154,1255,1256],{"class":209}," Path(path).read_bytes()[:",[154,1258,1259],{"class":259},"4",[154,1261,1262],{"class":209},"]\n",[154,1264,1265,1268,1271,1274,1277,1279,1282,1284,1287],{"class":70,"line":533},[154,1266,1267],{"class":205},"    if",[154,1269,1270],{"class":209}," head ",[154,1272,1273],{"class":205},"==",[154,1275,1276],{"class":205}," b",[154,1278,330],{"class":163},[154,1280,1281],{"class":333},"\\xd0\\xcf\\x11\\xe0",[154,1283,330],{"class":163},[154,1285,1286],{"class":209},":          ",[154,1288,1290],{"class":1289},"s-wDw","# OLE2 -> legacy .xls\n",[154,1292,1293,1296,1299,1301,1303,1305],{"class":70,"line":549},[154,1294,1295],{"class":205},"        return",[154,1297,1298],{"class":209}," pd.read_excel(path, ",[154,1300,245],{"class":244},[154,1302,232],{"class":205},[154,1304,250],{"class":163},[154,1306,253],{"class":209},[154,1308,1309,1311,1314,1317,1320,1322,1324,1327,1330],{"class":70,"line":554},[154,1310,1267],{"class":205},[154,1312,1313],{"class":209}," head[:",[154,1315,1316],{"class":259},"2",[154,1318,1319],{"class":209},"] ",[154,1321,1273],{"class":205},[154,1323,1276],{"class":205},[154,1325,1326],{"class":163},"\"PK\"",[154,1328,1329],{"class":209},":                     ",[154,1331,1332],{"class":1289},"# zip -> .xlsx\u002F.xlsm\u002F.ods\n",[154,1334,1335,1337,1339,1341,1343,1345],{"class":70,"line":576},[154,1336,1295],{"class":205},[154,1338,1298],{"class":209},[154,1340,245],{"class":244},[154,1342,232],{"class":205},[154,1344,645],{"class":163},[154,1346,253],{"class":209},[154,1348,1349,1352,1355,1357,1359,1361,1363,1366,1368,1371,1373,1376,1379,1381,1384],{"class":70,"line":603},[154,1350,1351],{"class":205},"    raise",[154,1353,1354],{"class":259}," ValueError",[154,1356,324],{"class":209},[154,1358,327],{"class":205},[154,1360,330],{"class":163},[154,1362,334],{"class":333},[154,1364,1365],{"class":209},"path",[154,1367,340],{"class":333},[154,1369,1370],{"class":163},": not a spreadsheet (",[154,1372,334],{"class":333},[154,1374,1375],{"class":209},"head",[154,1377,1378],{"class":205},"!r",[154,1380,340],{"class":333},[154,1382,1383],{"class":163},")\"",[154,1385,253],{"class":209},[154,1387,1388],{"class":70,"line":632},[154,1389,223],{"emptyLinePlaceholder":222},[154,1391,1392,1394,1396,1399,1402],{"class":70,"line":656},[154,1393,229],{"class":209},[154,1395,232],{"class":205},[154,1397,1398],{"class":209}," read_spreadsheet(",[154,1400,1401],{"class":163},"\"mystery_export.xls\"",[154,1403,253],{"class":209},[10,1405,1406],{},"Drop that into the ingest layer of a reporting job and the \"does not support\" error disappears permanently, whatever the upstream system decides to call its exports next.",[137,1408,1410],{"id":1409},"conclusion","Conclusion",[10,1412,1413,1414,1416,1417,1419,1420,1422],{},"openpyxl refuses ",[13,1415,19],{}," because it is a genuinely different format, not because of a version problem. Read legacy files with ",[13,1418,1101],{}," (or calamine for a single engine across every format), and convert them to ",[13,1421,23],{}," at the edge of your pipeline so the rest of the code has one format to think about. Dispatch on magic bytes rather than the extension, and the error cannot come back through a mislabelled export.",[137,1424,1426],{"id":1425},"frequently-asked-questions","Frequently asked questions",[10,1428,1429,1432,1433,1435,1436,1438,1439,1441],{},[1096,1430,1431],{},"Why can't openpyxl just read .xls too?","\nThey are unrelated formats. An ",[13,1434,19],{}," file is an OLE2 compound document with binary BIFF records; an ",[13,1437,23],{}," is a zip of XML parts. openpyxl implements only the latter, so supporting ",[13,1440,19],{}," would mean shipping a second, entirely separate parser.",[10,1443,1444,1447,1448,1450,1451,1453,1454,1456,1457,1459],{},[1096,1445,1446],{},"Is xlrd still maintained for .xls?","\nYes, for ",[13,1449,19],{}," only. Version 2.0 deliberately removed ",[13,1452,23],{}," support to reduce the security surface, which is why old code that passed ",[13,1455,1101],{}," for an ",[13,1458,23],{}," now fails. For legacy files xlrd remains the standard choice.",[10,1461,1462,1465,1466,1469,1470,1472,1473,1475],{},[1096,1463,1464],{},"What if the file has an .xls extension but is not really .xls?","\nThat is common with portal exports. Check the first bytes: OLE2 files start with ",[13,1467,1468],{},"D0 CF 11 E0",", real ",[13,1471,23],{}," files start with ",[13,1474,1117],{},", and HTML or CSV exports start with a tag or plain text. Route the read on the bytes, never on the name.",[10,1477,1478,1481,1482,1484],{},[1096,1479,1480],{},"Should I convert to .xlsx or keep reading .xls each time?","\nConvert once if you control the file. Conversion removes the extra dependency, lifts the 65,536-row limit, and makes every downstream tool available. Keep reading ",[13,1483,19],{}," only when a system you cannot change keeps producing it.",[10,1486,1487,1490],{},[1096,1488,1489],{},"Does converting lose anything?","\nFormulas and values convert cleanly; macros, some legacy chart types and unusual formatting may not survive a pandas round trip because pandas reads values only. For a fuller conversion, drive LibreOffice in headless mode.",[137,1492,1494],{"id":1493},"related","Related",[1090,1496,1497,1506,1511,1518,1525,1530],{},[1093,1498,1499,1502,1503,1505],{},[1096,1500,1501],{},"Up:"," ",[26,1504,29],{"href":28}," — the triage map for the whole error family.",[1093,1507,1508,1510],{},[26,1509,1129],{"href":1128}," — what you see when the same file is mislabelled the other way round.",[1093,1512,1513,1517],{},[26,1514,1516],{"href":1515},"\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 engine-selection error one layer up.",[1093,1519,1520,1524],{},[26,1521,1523],{"href":1522},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-xls-files-in-python-with-xlrd-and-pandas\u002F","Read xls files in Python with xlrd and pandas"," — the full legacy-reading walkthrough, including date modes.",[1093,1526,1527,1529],{},[26,1528,754],{"href":753}," — a faithful conversion when a pandas round trip is not enough.",[1093,1531,1532,1536],{},[26,1533,1535],{"href":1534},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods\u002F","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods"," — the full extension-to-engine map behind this error.",[1538,1539,1540],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":150,"searchDepth":219,"depth":219,"links":1542},[1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553],{"id":139,"depth":219,"text":140},{"id":189,"depth":219,"text":190},{"id":447,"depth":219,"text":448},{"id":870,"depth":219,"text":871},{"id":983,"depth":219,"text":984},{"id":1087,"depth":219,"text":1088},{"id":1167,"depth":219,"text":1168},{"id":1186,"depth":219,"text":1187},{"id":1409,"depth":219,"text":1410},{"id":1425,"depth":219,"text":1426},{"id":1493,"depth":219,"text":1494},"2026-09-05","2026-08-27","Why openpyxl refuses .xls files, how to read them with xlrd or calamine, how to convert a whole folder to .xlsx, and how to spot a mislabelled file before it fails.","md",[1559,1561,1563,1567,1569],{"q":1431,"a":1560},"They are unrelated formats. An .xls file is an OLE2 compound document with binary BIFF records; an .xlsx is a zip of XML parts. openpyxl implements only the latter, so supporting .xls would mean shipping a second, entirely separate parser.",{"q":1446,"a":1562},"Yes, for .xls only. Version 2.0 deliberately removed .xlsx support to reduce the security surface, which is why old code that passed engine=\"xlrd\" for an .xlsx now fails. For legacy files xlrd remains the standard choice.",{"q":1464,"a":1564},{"That is common with portal exports":1565},{" Check the first bytes":1566},"OLE2 files start with D0 CF 11 E0, real .xlsx files start with PK, and HTML or CSV exports start with a tag or plain text. Route the read on the bytes, never on the name.",{"q":1480,"a":1568},"Convert once if you control the file. Conversion removes the extra dependency, lifts the 65,536-row limit, and makes every downstream tool — openpyxl, formatting, charts — available. Keep reading .xls only when a system you cannot change keeps producing it.",{"q":1489,"a":1570},"Formulas and values convert cleanly; macros, some legacy chart types and unusual formatting may not survive a pandas round trip because pandas reads values only. For a fuller conversion, drive LibreOffice in headless mode.",{},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format",{"title":1574,"description":1575},"Fix openpyxl Does Not Support .xls","Read legacy .xls files in Python: use xlrd or python-calamine, convert to .xlsx once with a batch script, and detect files whose extension does not match their bytes.","fix-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","how-to","i8bxNuZ5WDR4hgDby3NJnbKf88jmVUq8mWabmFVQqJM",[1581,1585],{"title":1582,"path":1583,"stem":1584,"children":-1},"Fix \"Excel Found Unreadable Content\" After Writing with Python","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-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",{"title":1586,"path":1587,"stem":1588,"children":-1},"Fix \"Worksheet Does Not Exist\" KeyError in openpyxl","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-worksheet-does-not-exist-keyerror-in-openpyxl","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-worksheet-does-not-exist-keyerror-in-openpyxl\u002Findex",1788710154592]