How-to
Getting Started With Python Excel AutomationDeep dive

Read Strict Open XML Excel Files in Python

Read workbooks saved in Excel's "Strict Open XML Spreadsheet" format, which pandas, openpyxl and calamine reject with "Worksheet index 0 is invalid, 0 worksheets found" or "Cannot detect file format": recognise a Strict file, convert its namespaces to the usual transitional ones in memory so pandas can read it, convert with LibreOffice when that is not enough, and stop the files arriving.

Excel's Save As list contains two formats with the .xlsx extension: Excel Workbook, the one almost everyone uses, and Strict Open XML Spreadsheet. The second is the stricter flavour of the ISO standard behind .xlsx, and some organisations pick it for archiving or by policy. The files look identical in Explorer and open normally in Excel, but most Python libraries cannot read them. pandas with openpyxl reports ValueError: Worksheet index 0 is invalid, 0 worksheets found, openpyxl on its own loads a workbook with no sheets, and the calamine engine says Cannot detect file format. Nothing in those messages mentions Strict, so people suspect a corrupt file. This page shows why the readers fail, how to recognise a Strict workbook in two lines, how to convert it to the ordinary format in memory so pandas reads it, when to use LibreOffice instead, and how to stop such files arriving. It belongs to Handling Excel File Formats and Conversions.

Prerequisites

Bash
pip install pandas openpyxl python-calamine

LibreOffice (soffice) is used for the alternative fix in Step 4. The sample, sales_strict.xlsx, has the namespaces and conformance flag Excel writes when saving as Strict Open XML Spreadsheet: a Sales sheet with three regions.

Step 1 — Reproduce the errors

Python
import warnings
import pandas as pd
from openpyxl import load_workbook

for engine in ["openpyxl", "calamine"]:
    try:
        pd.read_excel("sales_strict.xlsx", engine=engine)
    except Exception as e:
        print(f"{engine:<9} {type(e).__name__}: {e}")

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    wb = load_workbook("sales_strict.xlsx")
print("openpyxl sees sheets:", wb.sheetnames, "| warning:", str(caught[0].message)[:60] if caught else None)

openpyxl opens the package — it is a valid zip — but finds no worksheets, because it looks for elements and relationships in the transitional namespaces and the Strict file uses different ones; pandas then asks for sheet 0 of an empty workbook. calamine fails earlier, unable to recognise the package at all. The same "0 worksheets found" message also appears for broken relationship paths in some generated files — a different cause with different fixes, covered in Fix "Worksheet index 0 is invalid, 0 worksheets found" — so check which one you have before fixing.

Step 2 — Recognise a Strict workbook

Python
import zipfile

def is_strict(path):
    with zipfile.ZipFile(path) as z:
        workbook = z.read("xl/workbook.xml").decode("utf-8", "replace")
    return 'conformance="strict"' in workbook or "purl.oclc.org/ooxml" in workbook

print("sales_strict.xlsx:", is_strict("sales_strict.xlsx"), "| _transitional.xlsx:", is_strict("_transitional.xlsx"))

The workbook part of a Strict file declares conformance="strict" and uses namespaces under http://purl.oclc.org/ooxml/ instead of http://schemas.openxmlformats.org/. Both formats share the same zip layout, the same part names such as xl/workbook.xml and xl/worksheets/sheet1.xml, and the same content types, which is why file-type detection by extension or by zip contents cannot tell them apart and why the readers' errors are so unhelpful. Checking for the flag or the namespaces takes a few milliseconds and needs only the standard library, so a reading job can recognise Strict files up front and either convert them or report them clearly instead of failing with a misleading error.

Transitional and Strict namespaces The spreadsheet, relationship and drawing namespaces in an ordinary transitional workbook start with schemas.openxmlformats.org. In a Strict workbook the same namespaces start with purl.oclc.org/ooxml, and the workbook part declares conformance strict. transitional (Excel Workbook) strict spreadsheet openxmlformats.org/…/spreadsheetml purl.oclc.org/ooxml/spreadsheetml relationships openxmlformats.org/…/relationships purl.oclc.org/…/relationships drawing openxmlformats.org/drawingml/… purl.oclc.org/ooxml/drawingml workbook flag — conformance="strict"

Step 3 — Convert the namespaces in memory and read with pandas

For ordinary data workbooks the difference is mostly the namespace URIs, including those inside relationship types. Rewrite them back to the transitional ones in an in-memory copy and hand that to pandas:

Python
from io import BytesIO

TO_TRANSITIONAL = {
    "http://purl.oclc.org/ooxml/spreadsheetml/main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
    "http://purl.oclc.org/ooxml/officeDocument/relationships": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
    "http://purl.oclc.org/ooxml/drawingml/main": "http://schemas.openxmlformats.org/drawingml/2006/main",
}

def strict_to_transitional(path):
    out = BytesIO()
    with zipfile.ZipFile(path) as zin, zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zout:
        for item in zin.infolist():
            data = zin.read(item.filename)
            if item.filename.endswith((".xml", ".rels")):
                text = data.decode("utf-8")
                for strict, transitional in TO_TRANSITIONAL.items():
                    text = text.replace(strict, transitional)
                data = text.replace(' conformance="strict"', "").encode("utf-8")
            zout.writestr(item, data)
    out.seek(0)
    return out

def read_any_xlsx(path, **kwargs):
    source = strict_to_transitional(path) if is_strict(path) else path
    return pd.read_excel(source, **kwargs)

print(read_any_xlsx("sales_strict.xlsx", sheet_name="Sales").to_string(index=False))

The rewritten copy reads normally — the Sales sheet with all three regions — and the original file is untouched. Because the copy lives in a BytesIO buffer, nothing is written to disk, which matters when the input folder is read-only or shared, and the conversion costs about as much as reading the file twice. Because the relationship types are URIs under the same namespace prefix (…/relationships/worksheet, …/relationships/officeDocument), replacing the prefix fixes them too. read_any_xlsx makes the check-and-convert step invisible to the rest of a pipeline. The limits are worth stating: Strict also allows dates to be stored as ISO strings and has a few other structural differences, and workbooks with charts, pivot tables or other rich content use more namespaces than these three. For plain data sheets the rewrite is enough; for anything richer, or if the result looks wrong, use LibreOffice.

Reading a workbook that might be Strict Check the workbook part for the strict conformance flag or purl.oclc.org namespaces. If it is transitional, read it directly. If it is Strict, rewrite the namespaces in an in-memory copy and read that with pandas, or convert it with LibreOffice when the workbook has rich content. is_strict() check xl/workbook.xml Transitional read directly Strict rewrite namespaces in memory Rich content LibreOffice conversion

Step 4 — Convert with LibreOffice when the rewrite is not enough

LibreOffice reads Strict files fully and can write them back out as ordinary workbooks:

Python
import subprocess

subprocess.run(["soffice", "--headless", "--convert-to", "xlsx:Calc MS Excel 2007 XML", "--outdir", "converted", "sales_strict.xlsx"],
               check=True, capture_output=True)
converted = "converted/sales_strict.xlsx"
print("strict after conversion:", is_strict(converted))
print(pd.read_excel(converted).to_string(index=False))

The Calc MS Excel 2007 XML export filter writes the ordinary transitional format, which every Python library reads; the converted file is no longer Strict and gives the same data. LibreOffice handles the content the namespace rewrite does not — ISO dates, charts, styles — at the cost of needing LibreOffice installed and taking a second or two per file. It is the right choice for batch conversion of an archive of Strict workbooks. Conversions in general are covered in Convert Excel to PDF on Linux with LibreOffice, which uses the same command with a different target.

Namespace rewrite or LibreOffice? For a plain data workbook read by a Python job, rewrite the namespaces in memory, which is fast and needs nothing installed. For workbooks with charts, pivot tables or dates stored as text, or for archives of many files to convert once, use LibreOffice. What is in the Strict workbook? plain data sheets in-memory rewrite fast, no extra software charts, pivots, ISO dates LibreOffice conversion complete and reliable an archive of files batch with LibreOffice convert once, keep originals

Finding Strict files in a folder

When files arrive from many senders, scan the incoming folder with the same check and report Strict workbooks alongside the data job's other warnings. A loop over Path("inbox").glob("*.xlsx") that calls is_strict() on each file, skipping Excel's ~$ lock files, lists the senders to contact; the check reads one small part of each archive, so it is quick even for large workbooks. Logging "converted from Strict Open XML" whenever read_any_xlsx takes the conversion path also leaves a trace, so if a converted file's figures are ever questioned, the conversion is the first thing to rule out — compare with a LibreOffice conversion of the same file, which should give identical values for plain data sheets, as it did here.

Stop them arriving

Strict files usually come from one person or one system set to save in that format. In Excel, File › Save As shows the format in the type list: choosing Excel Workbook (*.xlsx) instead fixes it for that file, and File › Options › Save › Save files in this format sets the default. A system that exports Strict workbooks may have a setting for the ordinary format. Telling the sender — with the check from Step 2 in your job's error message — is usually the quickest permanent fix.

Common pitfalls

  • Assuming the file is corrupt. It opens in Excel; check for Strict first.
  • Confusing the two "0 worksheets" causes. Strict namespaces and broken relationship paths need different fixes.
  • Rewriting rich workbooks. The namespace swap suits plain data; use LibreOffice for charts and pivots.
  • Overwriting the original. Convert in memory or into a separate folder.
  • Fixing every file forever. Ask the sender to save as Excel Workbook.

Conclusion

A Strict Open XML workbook uses purl.oclc.org/ooxml namespaces and a conformance="strict" flag, which makes openpyxl find no worksheets and calamine fail to detect the format. Detect it by reading xl/workbook.xml, then either rewrite the namespaces to the transitional ones in an in-memory copy and read that with pandas, or convert the file with LibreOffice's Calc MS Excel 2007 XML filter when it contains more than plain data — and ask the sender to save as Excel Workbook in future.

Frequently asked questions

What is a Strict Open XML Excel file? An .xlsx saved with Excel's Strict Open XML Spreadsheet option; it uses the strict variant of the ISO standard, with different XML namespaces from the usual Excel Workbook format.

Why does pandas say "Worksheet index 0 is invalid, 0 worksheets found" for my file? One common cause is a Strict Open XML workbook: openpyxl does not recognise its namespaces and finds no sheets. Check xl/workbook.xml for purl.oclc.org namespaces.

How do I read a Strict xlsx file with pandas? Rewrite the purl.oclc.org namespaces to the transitional schemas.openxmlformats.org ones in an in-memory copy and read that, or convert the file with LibreOffice first.

How do I stop Excel saving files as Strict Open XML? Use Save As and choose Excel Workbook (*.xlsx), and set the default format under File › Options › Save.

More on file formats & conversion

More on file formats & conversion →