headroomlabs-ai/headroom · error · ImportError

Reading legacy .xls files requires xlrd. Install it with: pi

Error message

Reading legacy .xls files requires xlrd. Install it with: pip install headroom-ai[spreadsheet]

What it means

Reading legacy binary `.xls` (Excel 97-2003) workbooks requires the optional `xlrd` package, which is not installed. `xlrd` is kept separate from the base install (and from openpyxl, which only handles `.xlsx`), so legacy-format support is opt-in via the `spreadsheet` extra. Note the loader itself is marked as needing the optional dep plus a binary fixture, so it is exercised only lightly by the test suite.

Source

Thrown at headroom/transforms/spreadsheet_ingest.py:58

    sheets: dict[str, str] = {}
    try:
        for ws in wb.worksheets:
            rows = [list(r) for r in ws.iter_rows(values_only=True)]
            text = _rows_to_csv(rows)
            if text.strip():
                sheets[ws.title] = text
    finally:
        wb.close()
    return sheets


def _load_xls(
    path: Path,
) -> dict[str, str]:  # pragma: no cover - legacy .xls; needs optional xlrd + binary fixture
    try:
        import xlrd
    except ImportError as e:
        raise ImportError(
            "Reading legacy .xls files requires xlrd. "
            "Install it with: pip install headroom-ai[spreadsheet]"
        ) from e

    book = xlrd.open_workbook(str(path))
    sheets: dict[str, str] = {}
    for sheet in book.sheets():
        rows = [sheet.row_values(i) for i in range(sheet.nrows)]
        text = _rows_to_csv(rows)
        if text.strip():
            sheets[sheet.name] = text
    return sheets


def load_spreadsheet(path: str | Path) -> dict[str, str]:
    """Load a spreadsheet file into ``{sheet_name: csv_text}``.

    Args:

View on GitHub (pinned to 322425c43b)

Solutions

  1. Install the extra: `pip install headroom-ai[spreadsheet]` (it includes xlrd)
  2. Convert the workbook to `.xlsx` upstream (Excel/LibreOffice `--convert-to xlsx`, or pandas `read_excel` round-trip) if adding a dependency is undesirable
  3. Reject or quarantine `.xls` inputs with a clear pre-check if legacy format support is out of scope for your pipeline

Example fix

# before
$ pip install openpyxl
sheets = ingest_spreadsheet(Path("legacy_export.xls"))  # ImportError

# after
$ pip install "headroom-ai[spreadsheet]"
sheets = ingest_spreadsheet(Path("legacy_export.xls"))
Defensive patterns

Strategy: validation

Validate before calling

def xls_supported() -> bool:
    try:
        import xlrd  # noqa: F401
        return True
    except ImportError:
        return False

if path.suffix.lower() == ".xls" and not xls_supported():
    raise RuntimeError("legacy .xls ingestion requires headroom-ai[spreadsheet]")

Type guard

def can_read_xls() -> bool:
    return importlib.util.find_spec("xlrd") is not None

Try / catch

try:
    sheets = ingest_spreadsheet(path)
except ImportError as e:
    if "xlrd" in str(e):
        # convert instead of installing: libreoffice --headless --convert-to xlsx
        raise RuntimeError(".xls needs the spreadsheet extra or pre-conversion") from e
    raise

Prevention

When it happens

Trigger: Calling the ingest API with a path whose suffix is `.xls`, routing to `_load_xls`, in an environment where `import xlrd` fails.

Common situations: Ingesting exports from old enterprise systems that still emit `.xls`; installing only `openpyxl` (which since v2.x explicitly dropped `.xls` support) and assuming it covers all Excel formats; minimal CI images without the extra.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/fa4617ff275535a9. Report an issue: GitHub.