pola-rs/polars · error · OSError

file {source.name!r} must be opened in binary mode

Error message

file {source.name!r} must be opened in binary mode

What it means

Raised by pl.read_excel (engine='calamine', the default) in _initialise_spreadsheet_parser as an OSError when source is a file handle (BufferedReader or TextIOWrapper) whose mode lacks 'b' — i.e. a file opened in text mode. fastexcel needs bytes or a filename, so polars validates the handle's mode before handing it over.

Source

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

    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

    msg = f"unrecognized engine: {engine!r}"
    raise NotImplementedError(msg)


def _csv_buffer_to_frame(
    csv: StringIO,
    *,

View on GitHub (pinned to df599052da)

Solutions

  1. Open the file in binary mode: open(path, 'rb')
  2. Simplest: pass the path string itself and let polars open the file
  3. If the handle comes from elsewhere, read it as bytes (fh.read() in binary) or use its .name if it points at a real file

Example fix

# before
with open('report.xlsx') as f:  # text mode -> OSError
    df = pl.read_excel(f)

# after
with open('report.xlsx', 'rb') as f:
    df = pl.read_excel(f)
# or simply: df = pl.read_excel('report.xlsx')
Defensive patterns

Strategy: validation

Validate before calling

def ensure_binary_handle(fh):
    mode = getattr(fh, 'mode', 'b')
    if 'b' not in mode:
        fh.close()
        return open(fh.name, 'rb')
    return fh

with ensure_binary_handle(open('report.xlsx')) as fh:
    df = pl.read_excel(fh, engine='calamine')

Type guard

def is_binary_fileobj(fh) -> bool:
    return hasattr(fh, 'mode') and 'b' in fh.mode

Try / catch

try:
    df = pl.read_excel(fh, engine='calamine')
except OSError as e:
    if 'binary mode' in str(e):
        df = pl.read_excel(fh.name, engine='calamine')  # pass the path instead
    else:
        raise

Prevention

When it happens

Trigger: with open('f.xlsx') as f: pl.read_excel(f) — Python's open() defaults to text mode ('r'), producing a TextIOWrapper that fails the 'b' in mode check. Also open(path, 'rt'), or handles obtained from text-oriented helpers.

Common situations: Code copied from csv workflows (text mode is fine there); refactoring from open(path) context managers; passing a handle returned by a generic 'download to file' utility that opened it for text.

Related errors


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