{"record":{"id":"92087a67579a9bd3","repo":"pola-rs/polars","slug":"fastexcel-0-10-is-required-to-read-bytes-fou","errorCode":null,"errorMessage":"`fastexcel` >= 0.10 is required to read bytes; found {module_version})","messagePattern":"`fastexcel` >= 0\\.10 is required to read bytes; found (.+?)\\)","errorType":"exception","errorClass":"ModuleUpgradeRequiredError","httpStatus":null,"severity":"error","filePath":"py-polars/src/polars/io/spreadsheet/functions.py","lineNumber":877,"sourceCode":"        openpyxl = import_optional(\"openpyxl\")\n        if isinstance(source, bytes):\n            source = BytesIO(source)\n\n        parser = openpyxl.load_workbook(source, data_only=True, **engine_options)\n        sheets = [{\"index\": i + 1, \"name\": ws.title} for i, ws in enumerate(parser)]\n        return _read_spreadsheet_openpyxl, parser, sheets\n\n    elif engine == \"calamine\":\n        fastexcel = import_optional(\"fastexcel\", min_version=\"0.7.0\")\n        reading_bytesio, reading_bytes = (\n            isinstance(source, BytesIO),\n            isinstance(source, bytes),\n        )\n        if (reading_bytesio or reading_bytes) and parse_version(\n            module_version := fastexcel.__version__\n        ) < (0, 10):\n            msg = f\"`fastexcel` >= 0.10 is required to read bytes; found {module_version})\"\n            raise ModuleUpgradeRequiredError(msg)\n\n        if reading_bytesio:\n            source = source.getvalue()  # type: ignore[union-attr]\n        elif isinstance(source, (BufferedReader, TextIOWrapper)):\n            if \"b\" not in source.mode:\n                msg = f\"file {source.name!r} must be opened in binary mode\"\n                raise OSError(msg)\n            elif (filename := source.name) and Path(filename).exists():\n                source = filename\n            else:\n                source = source.read()\n\n        parser = fastexcel.read_excel(source, **engine_options)\n        sheets = [\n            {\"index\": i + 1, \"name\": nm} for i, nm in enumerate(parser.sheet_names)\n        ]\n        return _read_spreadsheet_calamine, parser, sheets\n","sourceCodeStart":859,"sourceCodeEnd":895,"githubUrl":"https://github.com/pola-rs/polars/blob/df599052daf96e7a9cc30a3b0c6bd25d6947e3c0/py-polars/src/polars/io/spreadsheet/functions.py#L859-L895","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Upgrade: pip install -U 'fastexcel>=0.10' (latest is fine and also unlocks table_name/use_columns)","If you cannot upgrade, write the bytes to a temporary file and pass the path instead (old fastexcel reads paths fine)","Alternatively switch to engine='openpyxl', which accepts BytesIO directly"],"exampleFix":"# before (fastexcel 0.7.x)\npl.read_excel(requests.get(url).content)\n\n# after (no upgrade possible: spool to a temp file)\nimport tempfile, pathlib\nwith tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as f:\n    f.write(workbook_bytes)\npl.read_excel(f.name, engine='openpyxl')  # or upgrade fastexcel>=0.10 and pass bytes directly","handlingStrategy":"validation","validationCode":"def supports_bytes_source() -> bool:\n    try:\n        import fastexcel\n        return tuple(int(p) for p in fastexcel.__version__.split('.')[:2]) >= (0, 10)\n    except ImportError:\n        return False\n\nif isinstance(source, (bytes, io.BytesIO)) and not supports_bytes_source():\n    import tempfile\n    with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as f:\n        f.write(source if isinstance(source, bytes) else source.getvalue())\n    source = f.name  # hand old fastexcel a path instead of bytes\n\ndf = pl.read_excel(source, engine='calamine')","typeGuard":null,"tryCatchPattern":"from polars.exceptions import ModuleUpgradeRequiredError\ntry:\n    df = pl.read_excel(workbook_bytes)\nexcept ModuleUpgradeRequiredError as e:\n    if 'fastexcel' in str(e):\n        df = pl.read_excel(workbook_bytes, engine='openpyxl')  # fallback engine takes bytes\n    else:\n        raise","preventionTips":["Pin fastexcel >= 0.10 (ideally latest) in requirements next to polars","Remember URL sources become BytesIO internally, so remote reads need the same version floor","Catch ModuleUpgradeRequiredError (a ModuleNotFoundError subclass) in deployment smoke tests"],"tags":["polars","excel","calamine","fastexcel","version","bytes","upgrade"],"backgroundTag":null,"analyzedSha":"df599052daf96e7a9cc30a3b0c6bd25d6947e3c0","analyzedAt":"2026-08-16T12:10:03.978Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}