Fix "Worksheet index 0 is invalid, 0 worksheets found"
A file opens fine in Excel, but pandas refuses it:
ValueError: Worksheet index 0 is invalid, 0 worksheets found
pandas asked openpyxl for the first worksheet and openpyxl reported that the workbook has none. The data is there; openpyxl could not find it. This almost always happens with files produced by some other system — an ERP export, a reporting tool, a web application's "download as Excel" — whose internal file paths are written in a way Excel tolerates and openpyxl does not. This page shows how to confirm that, and three ways around it. It is part of Troubleshooting Common Python Excel Errors.
Prerequisites
pip install pandas openpyxl python-calamine
erp_export.xlsx stands in for the problem file: it opens in Excel and LibreOffice with a three-row sales table, and fails in pandas with the default engine.
Step 1 — Reproduce and confirm it is "no worksheets", not a bad file
First make sure the file is a real .xlsx and see what openpyxl finds in it:
import zipfile
import pandas as pd
from openpyxl import load_workbook
print("zip archive:", zipfile.is_zipfile("erp_export.xlsx"))
try:
pd.read_excel("erp_export.xlsx")
except ValueError as e:
print("pandas:", e)
wb = load_workbook("erp_export.xlsx")
print("openpyxl sheet names:", wb.sheetnames)
print("calamine sees:", list(pd.read_excel("erp_export.xlsx", sheet_name=None, engine="calamine")))
The same root cause produces two other messages, depending on how the file is read. With sheet_name=None, asking for every sheet, pandas raises ValueError: Sheet name is an empty list. With openpyxl directly, wb.active is None, and the first attribute access fails with AttributeError: 'NoneType' object has no attribute 'max_row', which looks like a bug in your own code rather than in the file:
print(repr(wb.active))
try:
pd.read_excel("erp_export.xlsx", sheet_name=None)
except ValueError as e:
print("sheet_name=None:", e)
The file is a valid zip, openpyxl loads it without an error, and still lists no sheets, while a different reader sees Sheet1. That combination points at how openpyxl locates sheets. If the file is not a zip at all, the problem is different: see Fix BadZipFile When Reading an Excel File in Python.
Step 2 — Compare the relationships with the zip contents
An .xlsx file is a zip of XML parts. xl/workbook.xml lists the sheets; xl/_rels/workbook.xml.rels maps each sheet to the path of its part inside the zip. openpyxl looks up each path exactly, and silently skips any sheet whose path does not match a member of the archive. Excel matches paths without regard to case. Print both sides:
import posixpath
import re
with zipfile.ZipFile("erp_export.xlsx") as z:
names = z.namelist()
rels = z.read("xl/_rels/workbook.xml.rels").decode()
targets = re.findall(r'Target="([^"]+)"', rels)
for t in targets:
path = posixpath.normpath(posixpath.join("xl", t)) if not t.startswith("/") else t.lstrip("/")
exact = path in names
loose = [n for n in names if n.lower() == path.lower()]
print(f"{t:26} exact match: {exact!s:5} case-insensitive match: {loose}")
The relationship points at Worksheets/Sheet1.xml; the archive contains xl/worksheets/sheet1.xml. Excel resolves that; openpyxl does not, so the sheet is skipped and the workbook appears to have none.
Why no warning? In openpyxl's reader, each sheet's path is checked against the list of files in the archive, and a sheet whose path is not in that list is skipped with a plain continue. That behaviour makes sense for genuinely optional parts, but for a worksheet it turns a malformed file into an apparently empty one. Knowing this is the difference between an afternoon spent looking for data that "vanished" and a two-minute diagnosis.
Other mismatches produce the same symptom: backslashes in member names written by some Windows zip libraries, or a relationship pointing at a part that is missing altogether. The comparison above shows all of them.
Fix 1 — Read it with calamine
The calamine engine resolves the parts differently and reads the file as it is. For reading data, this is the quickest fix:
df = pd.read_excel("erp_export.xlsx", engine="calamine")
print(df)
Nothing else in the code has to change, so this is also the least risky fix for a scheduled job that has just started failing on a new supplier file. calamine is also considerably faster than openpyxl on large files. It reads values only, not formatting, which is all read_excel needs anyway; see Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods.
Fix 2 — Repair the paths in the zip
If the file must go through openpyxl, for example to keep formatting or edit it, rewrite the relationships so each target matches the archive member's actual name. The repair copies every part unchanged except the relationships files:
def repair_part_names(src, dst):
with zipfile.ZipFile(src) as zi:
names = zi.namelist()
by_lower = {n.lower(): n for n in names}
with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zo:
for item in zi.infolist():
data = zi.read(item.filename)
if item.filename.endswith(".rels"):
base = posixpath.dirname(posixpath.dirname(item.filename))
def fix(m):
target = m.group(1)
full = posixpath.normpath(posixpath.join(base, target)) if not target.startswith("/") else target.lstrip("/")
actual = by_lower.get(full.lower())
if actual and actual != full:
return f'Target="{posixpath.relpath(actual, base or ".")}"'
return m.group(0)
data = re.sub(r'Target="([^"]+)"', fix, data.decode()).encode()
zo.writestr(item, data)
repair_part_names("erp_export.xlsx", "erp_export_repaired.xlsx")
print(load_workbook("erp_export_repaired.xlsx").sheetnames)
print(pd.read_excel("erp_export_repaired.xlsx"))
Relationship targets are relative to the folder above the _rels folder that contains them, which is what base computes; external links such as hyperlinks do not match any member and are left alone.
Fix 3 — Let LibreOffice rewrite the file
When there are many such files and they need to be ordinary .xlsx files for other tools, a headless LibreOffice conversion writes a clean file with standard part names:
soffice -env:UserInstallation=file://$PWD/lo_profile --headless --convert-to xlsx --outdir converted erp_export.xlsx > /dev/null 2>&1
ls converted
print(load_workbook("converted/erp_export.xlsx").sheetnames)
Conversion rewrites the whole file, so check that formatting and formulas you care about survive; for a data export they nearly always do.
Checking a folder of exports
When a supplier's system produces these files, every export has the same defect, and a pipeline that processes a folder should detect it up front rather than fail on the first file. Wrap the comparison from Step 2 in a function that returns the mismatched targets, and run it over the folder before reading anything:
from pathlib import Path
def mismatched_parts(path):
with zipfile.ZipFile(path) as z:
names = set(z.namelist())
rels = z.read("xl/_rels/workbook.xml.rels").decode()
bad = []
for t in re.findall(r'Target="([^"]+)"', rels):
full = t.lstrip("/") if t.startswith("/") else posixpath.normpath(posixpath.join("xl", t))
if full not in names:
bad.append(t)
return bad
for f in sorted(Path(".").glob("*.xlsx")):
problems = mismatched_parts(f)
print(f"{f.name:28} {'ok' if not problems else 'mismatched: ' + ', '.join(problems)}")
A pipeline can then route each file: straight to openpyxl when it is clean, through repair_part_names or calamine when it is not, and log which files needed help. That log is also the evidence to send to the supplier.
Common pitfalls
- Assuming the file is empty. "0 worksheets found" describes what openpyxl found, not what the file contains; compare with another reader before investigating the data.
- Patching by hand in a zip tool. Some tools re-compress members with backslashes or change their order; repair with Python's
zipfile, which keeps names exactly. - Only fixing the first sheet. Check every relationship target, including those for shared strings and styles, which fail with a
KeyErrorrather than silently. - Reporting it upstream. If a supplier's system produces these files, send them the comparison output; the file does not follow the package conventions openpyxl relies on.
Conclusion
"Worksheet index 0 is invalid, 0 worksheets found" means openpyxl skipped every sheet, usually because the workbook's relationships point at part names whose case or form does not exactly match the zip's members. Confirm it by comparing relationship targets with zipfile.namelist(). Read such files with engine="calamine", repair the relationship targets with zipfile when openpyxl must open the file, or convert them with LibreOffice.
Frequently asked questions
Why does pandas say 0 worksheets found when the file opens in Excel? Excel resolves the parts inside an .xlsx case-insensitively; openpyxl requires exact matches and skips sheets it cannot find. Files from some exporters use different capitalisation in the relationships than in the zip.
How do I read an Excel file that openpyxl cannot find sheets in? Use pd.read_excel(path, engine="calamine") after installing python-calamine, which reads such files. To use openpyxl, repair the relationship targets or convert the file with LibreOffice first.
Is this the same error as "Worksheet named ... not found"? No. That one means the sheet name you asked for is not in the workbook, often because of spelling or trailing spaces. "Worksheet index 0 is invalid" means openpyxl found no worksheets at all.
Can hidden sheets cause this error? No. Hidden worksheets are still worksheets and are counted. A workbook containing only chart sheets would, since pandas counts worksheets only.
Related
- Up one level: Troubleshooting Common Python Excel Errors — the other common errors.
- Fix BadZipFile When Reading an Excel File in Python — when the file is not a zip at all.
- Fix "Excel File Format Cannot Be Determined" in pandas — another reader-selection error.
- Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods — calamine and the other engines.
- Fix "Worksheet Does Not Exist" KeyError in openpyxl — the named-sheet version.
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