pola-rs/polars · error · ModuleUpgradeRequiredError

`fastexcel` >= 0.10 is required to read bytes; found {module

Error message

`fastexcel` >= 0.10 is required to read bytes; found {module_version})

What it means

Raised by pl.read_excel (engine='calamine', the default) in _initialise_spreadsheet_parser when source is bytes or BytesIO and the installed fastexcel is older than 0.10.0. polars imports fastexcel with a floor of only 0.7.0, then refuses to hand raw bytes to older versions that cannot accept them (ModuleUpgradeRequiredError, a ModuleNotFoundError subclass; note the message text carries a stray trailing parenthesis — cosmetic only). This also affects reads of remote URLs, since polars downloads them to a BytesIO before parsing.

Source

Thrown at py-polars/src/polars/io/spreadsheet/functions.py:877

        openpyxl = import_optional("openpyxl")
        if isinstance(source, bytes):
            source = BytesIO(source)

        parser = openpyxl.load_workbook(source, data_only=True, **engine_options)
        sheets = [{"index": i + 1, "name": ws.title} for i, ws in enumerate(parser)]
        return _read_spreadsheet_openpyxl, parser, sheets

    elif engine == "calamine":
        fastexcel = import_optional("fastexcel", min_version="0.7.0")
        reading_bytesio, reading_bytes = (
            isinstance(source, BytesIO),
            isinstance(source, bytes),
        )
        if (reading_bytesio or reading_bytes) and parse_version(
            module_version := fastexcel.__version__
        ) < (0, 10):
            msg = f"`fastexcel` >= 0.10 is required to read bytes; found {module_version})"
            raise ModuleUpgradeRequiredError(msg)

        if reading_bytesio:
            source = source.getvalue()  # type: ignore[union-attr]
        elif isinstance(source, (BufferedReader, TextIOWrapper)):
            if "b" not in source.mode:
                msg = f"file {source.name!r} must be opened in binary mode"
                raise OSError(msg)
            elif (filename := source.name) and Path(filename).exists():
                source = filename
            else:
                source = source.read()

        parser = fastexcel.read_excel(source, **engine_options)
        sheets = [
            {"index": i + 1, "name": nm} for i, nm in enumerate(parser.sheet_names)
        ]
        return _read_spreadsheet_calamine, parser, sheets

View on GitHub (pinned to df599052da)

Solutions

  1. Upgrade: pip install -U 'fastexcel>=0.10' (latest is fine and also unlocks table_name/use_columns)
  2. If you cannot upgrade, write the bytes to a temporary file and pass the path instead (old fastexcel reads paths fine)
  3. Alternatively switch to engine='openpyxl', which accepts BytesIO directly

Example fix

# before (fastexcel 0.7.x)
pl.read_excel(requests.get(url).content)

# after (no upgrade possible: spool to a temp file)
import tempfile, pathlib
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as f:
    f.write(workbook_bytes)
pl.read_excel(f.name, engine='openpyxl')  # or upgrade fastexcel>=0.10 and pass bytes directly
Defensive patterns

Strategy: validation

Validate before calling

def supports_bytes_source() -> bool:
    try:
        import fastexcel
        return tuple(int(p) for p in fastexcel.__version__.split('.')[:2]) >= (0, 10)
    except ImportError:
        return False

if isinstance(source, (bytes, io.BytesIO)) and not supports_bytes_source():
    import tempfile
    with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as f:
        f.write(source if isinstance(source, bytes) else source.getvalue())
    source = f.name  # hand old fastexcel a path instead of bytes

df = pl.read_excel(source, engine='calamine')

Try / catch

from polars.exceptions import ModuleUpgradeRequiredError
try:
    df = pl.read_excel(workbook_bytes)
except ModuleUpgradeRequiredError as e:
    if 'fastexcel' in str(e):
        df = pl.read_excel(workbook_bytes, engine='openpyxl')  # fallback engine takes bytes
    else:
        raise

Prevention

When it happens

Trigger: pl.read_excel(workbook_bytes, engine='calamine') or pl.read_excel('https://host/report.xlsx') with fastexcel 0.7.x–0.9.x installed; BytesIO sources from HTTP downloads, S3 clients, or in-memory generation.

Common situations: Stale fastexcel pin in requirements.txt/lockfile; CI caches that keep the old wheel; environments where only the import floor (0.7) was ever checked; datasets fetched over the network.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/92087a67579a9bd3. Report an issue: GitHub.