Fix "XLRDError: Excel xlsx file; not supported" in Python
A script that used to work now stops on the first read_excel call:
xlrd.biffh.XLRDError: Excel xlsx file; not supported
The message is accurate. xlrd 2.0, released in December 2020, dropped support for everything except the old binary .xls format. Any code that hands an .xlsx workbook to xlrd — directly through xlrd.open_workbook, or indirectly through pandas with engine="xlrd" — now gets this error. The library you need for .xlsx is openpyxl, which pandas already uses by default. The work is finding which line still routes the file to xlrd. This page belongs to Troubleshooting Common Python Excel Errors.
Why xlrd stopped reading .xlsx
xlrd was written for the binary .xls format that Excel used until 2007. Support for .xlsx was added later and was always the weaker half of the library, and the pandas 1.2 release notes cite potential security vulnerabilities in reading .xlsx with xlrd 1.2 and earlier. Rather than maintain a second .xlsx reader when openpyxl already did the job, the maintainers removed it and returned xlrd to its original purpose. pandas followed: from version 1.2 its default engine for .xlsx became openpyxl. pandas still passes the file to xlrd if you ask for that engine explicitly — which is exactly how the error in this page's title gets raised.
So the error is not a bug to wait out. It is the library telling you, correctly, that it no longer reads this format — and the environment your script was written in had an older xlrd that still did. That is also why the failure tends to appear "out of nowhere": a server rebuild, a fresh virtual environment or a new colleague's laptop installs xlrd 2.x, and code that never changed stops working.
Prerequisites
pip install pandas openpyxl
pip install xlrd # only if you also read legacy .xls files
Check what is installed before changing anything; the fix depends on it:
import importlib.metadata as md
for pkg in ("pandas", "openpyxl", "xlrd"):
try:
print(pkg, md.version(pkg))
except md.PackageNotFoundError:
print(pkg, "not installed")
Step 1 — Reproduce it and read the traceback
The error comes from two kinds of call, and the traceback tells you which one you have. With pandas, an explicit engine is the usual culprit:
import pandas as pd
try:
pd.read_excel("orders.xlsx", engine="xlrd")
except Exception as exc:
print(type(exc).__module__, type(exc).__name__, exc)
# xlrd.biffh XLRDError Excel xlsx file; not supported
If the bottom of the traceback shows your own code calling xlrd.open_workbook, the script uses xlrd directly — go to Step 4. If it shows pandas' _xlrd.py reader, something told pandas to use xlrd — Step 2.
Step 2 — Stop forcing the engine
Before pandas 1.2, read_excel defaulted to xlrd for every Excel file, so older scripts often spelled the engine out, and tutorials copied it. Today the right choice is to leave engine unset. pandas reads the file's first bytes, recognises the zip container of an .xlsx, and picks openpyxl:
df = pd.read_excel("orders.xlsx") # engine chosen from the content
print(df)
Search for every place the engine is set — it hides in more places than read_excel itself:
import pathlib, re
pattern = re.compile(r"""engine\s*=\s*['"]xlrd['"]|open_workbook\(""")
for path in pathlib.Path(".").rglob("*.py"):
for n, line in enumerate(path.read_text(errors="ignore").splitlines(), 1):
if pattern.search(line):
print(f"{path}:{n}: {line.strip()}")
Check pd.ExcelFile(..., engine="xlrd") calls and configuration files too. A setting like io.excel.xls.reader only applies to .xls and is harmless; an engine stored in a job's YAML or passed from a command-line flag is not.
Step 3 — Check for .xlsx files wearing an .xls extension
The error also appears when a file named .xls is really an .xlsx. Systems that export spreadsheets sometimes keep the old extension after switching formats, and people rename files by hand. xlrd inspects the bytes, sees a zip archive and refuses, whatever the name says. Look at the signature:
def excel_kind(path):
with open(path, "rb") as fh:
head = fh.read(8)
if head.startswith(b"PK\x03\x04"):
return "xlsx-family (zip) — read with openpyxl"
if head.startswith(bytes.fromhex("D0CF11E0A1B11AE1")):
return "legacy .xls (OLE2) — read with xlrd"
return f"not an Excel workbook: {head!r}"
print(excel_kind("orders.xlsx"))
print(excel_kind("legacy.xls"))
A zip signature under an .xls name is fixed by not forcing the engine — pandas' detection goes by content. For good measure, rename the file so people and other tools are not misled. If the bytes are neither signature, the file is something else again — often HTML or CSV saved with an Excel extension — and Fix "Excel File Format Cannot Be Determined" in pandas covers that case.
Step 4 — Port direct xlrd code to openpyxl
Scripts written against xlrd's own API need more than an engine change, because openpyxl names things differently and counts from 1 rather than 0. Here is typical xlrd-era code:
book = xlrd.open_workbook("orders.xlsx")
sheet = book.sheet_by_name("Sheet1")
for r in range(1, sheet.nrows):
sku = sheet.cell_value(r, 0)
qty = sheet.cell_value(r, 1)
The openpyxl version reads the same values. values_only=True hands back plain tuples, which is the closest match to xlrd's row_values:
from openpyxl import load_workbook
book = load_workbook("orders.xlsx", read_only=True, data_only=True)
sheet = book["Sheet1"]
for sku, qty in sheet.iter_rows(min_row=2, values_only=True):
print(sku, qty)
book.close()
The translations you will need most often:
Two behaviour differences are worth testing for when you port. data_only=True gives you cached formula results, the same thing xlrd returned; without it, openpyxl hands you the formula text. And xlrd returned every number as a float, while openpyxl returns integers as int — code that did int(sheet.cell_value(...)) keeps working, but code that compared types or formatted with %f may print differently.
Step 5 — Keep xlrd only for real .xls files
xlrd is still the right tool for legacy .xls workbooks, and pandas will use it for them automatically. If a job reads a mix of formats, leave the engine unset and let the content decide:
for name in ("orders.xlsx", "legacy.xls"):
print(name, pd.read_excel(name).to_dict("records"))
If the job only ever reads .xlsx, uninstall xlrd from its environment. A missing optional dependency fails loudly with a clear message the day someone feeds it an .xls, which is better than a stale engine argument quietly surviving. For converting a folder of old files once and for all, see Convert .xls to .xlsx with Python.
Step 6 — Add a test so the engine cannot creep back
Once the script works, pin the behaviour with a small test that writes an .xlsx, reads it through the same function your job uses, and fails if anything routes it to xlrd again. It runs in a fraction of a second and catches the regression the day someone pastes an old snippet back in:
def load_orders(path):
return pd.read_excel(path) # the job's real reading function
def test_reads_xlsx_without_xlrd(tmp_path):
path = tmp_path / "orders.xlsx"
pd.DataFrame({"sku": ["A-1"], "qty": [4]}).to_excel(path, index=False)
frame = load_orders(path)
assert frame.to_dict("records") == [{"sku": "A-1", "qty": 4}]
import pathlib, tempfile
with tempfile.TemporaryDirectory() as d:
test_reads_xlsx_without_xlrd(pathlib.Path(d))
print("test passed")
In a real project the test lives in tests/ and pytest supplies tmp_path; the last three lines only run it here. Test Excel Output with pytest shows the fuller setup.
Common pitfalls
- Pinning
xlrd==1.2.0. It silences the error by reinstating a parser the maintainers removed. You inherit its bugs, and pip will eventually refuse to resolve it with a current pandas. Fix the code path instead. - Setting
engine="openpyxl"everywhere. Better than xlrd for.xlsx, but it now breaks on genuine.xlsfiles. Unless you have a reason, let pandas choose. - Forgetting
data_only=Truewhen porting. Without it, formula cells return strings like=SUM(B2:B9)where xlrd gave numbers. - Off-by-one errors. xlrd rows and columns start at 0; openpyxl's start at 1. Port loops carefully and compare the first and last rows against the old output.
- Assuming the extension is the truth. Check the first bytes of any file whose origin you do not control.
Performance notes
For large .xlsx files, load_workbook(..., read_only=True) streams rows instead of building every cell in memory, which is roughly what xlrd's on-demand mode offered. If reading speed was the reason someone reached for xlrd in the first place, the calamine engine is considerably faster than either — Speed Up pandas Excel Reads with the calamine Engine has the numbers.
Conclusion
Excel xlsx file; not supported means an .xlsx workbook reached xlrd, which since version 2.0 only reads .xls. Remove any engine="xlrd" and let pandas detect the format, check the bytes of files whose extension may lie, port direct xlrd.open_workbook code to openpyxl with data_only=True and 1-based indexes, and keep xlrd installed only where genuine .xls files still arrive.
Frequently asked questions
Can I just install xlrd 1.2.0 to make the error go away? It works, but it is the wrong fix. xlrd 1.2 is unmaintained for .xlsx, its .xlsx parser was removed partly over XML security concerns, and it will not install cleanly alongside current pandas versions forever. Switch the .xlsx path to openpyxl instead.
Do I still need xlrd at all? Only for legacy .xls files (Excel 97–2003). pandas uses xlrd automatically for those when it is installed. For .xlsx, .xlsm and .xlsb you do not need it.
Why does the error appear on a file that ends in .xls? The file is really an .xlsx workbook with the wrong extension. xlrd looks at the bytes, sees a zip container, and refuses. pandas' own format detection gets this right if you stop forcing engine="xlrd".
What replaces xlrd's xldate_as_datetime? Nothing is needed with openpyxl. It converts date-formatted cells to Python datetime objects as it reads, so the conversion step disappears.
Related
- Up one level: Troubleshooting Common Python Excel Errors — the map of read and write errors.
- Read .xls Files in Python with xlrd and pandas — where xlrd still belongs.
- Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods — the full engine map.
- Fix "openpyxl does not support the old .xls file format" — the mirror-image error.
More on errors & fixes
- FixHandle Permission Denied When Writing Excel in Python
- How-toWhat openpyxl Loses When You Save an Existing Workbook
- FixFix Excel Serial Numbers Showing Instead of Dates
- How-toHandle Daylight Saving Time in Excel Timestamps
- How-toHandle Microsoft Graph Throttling in Excel Automation
- GuideError Handling and Logging in Excel Automation