mrkeyoor.com_
Sun 20 Sept 11:46 UTC
PyPIDataupdated 13 Sept 2026

xlrd review

Our xlrd 2.0.2 install was a small, pure-Python reader for the binary Excel 97-2003 .xls format. It opens a workbook, exposes sheets, rows, cells, type codes, dates, merged ranges, and BIFF formatting records. It reads cached formula results and ignores formulas themselves, macros, charts, pictures, comments, hyperlinks, filters, validation, and other embedded objects. It cannot write a workbook. Since 2.0.0 it rejects .xlsx, .xlsb, and .ods files by design. Version 2.0.2, released in June 2025 after a gap of more than four years, fixes an occasional failure when a sheet contains invalid formula records.

Verdict

Install xlrd 2.0.2 when the bytes are truly .xls and you need direct access to old BIFF cells or formatting records. For anything named .xlsx, choose openpyxl or python-calamine; downgrading xlrd only brings back an obsolete parser and the wrong maintenance path.

We installed it

Lab card: what happened when we installed xlrdScreenshot of xlrd documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport xlrd in 0.20s · pure Python · requires Python >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*
Known vulns0(pip-audit)

Answers from our run

Does xlrd install cleanly?

Yes. In a fresh container with an empty cache, pip install xlrd finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does xlrd need to run?

Python >=2.7, !=3.0., !=3.1., !=3.2., !=3.3., !=3.4., !=3.5., and nothing compiled: it is pure Python. In our run import xlrd succeeded in 0.20s.

xlrd or openpyxl: which should you use?

openpyxl: Choose it for reading or writing .xlsx and .xlsm workbooks, including formulas, styles, and modern worksheet structures. Install xlrd 2.0.2 when the bytes are truly .xls and you need direct access to old BIFF cells or formatting records.

When should you not use xlrd?

The file is .xlsx, .xlsm, .xlsb, or .ods. xlrd 2.x refuses those formats, regardless of a renamed extension

API stability4/5The Book, Sheet, Cell, open_workbook, xldate conversion, on-demand loading, and formatting APIs have been steady for old .xls consumers. Version 2.0.2 changes formula-error handling without altering normal caller code. The major exception is the 2.0.0 removal of every format except .xls, which intentionally broke .xlsx, .xlsb, and .ods users. Future callers should treat that narrow scope as permanent rather than expecting format support to return.
Docs3/5Read the Docs explains workbook and sheet APIs, cell types, dates, Unicode, on-demand loading, formatting records, and BIFF-specific behavior. The README places the .xls-only warning before its quick start and lists ignored features and password protection clearly. Examples retain older Python style, type information is absent, and common migration questions for renamed .xlsx files require readers to connect the format warning to another package themselves.
Maintenance2/5Version 2.0.2 was uploaded on 14 June 2025 to fix invalid formula-record handling, more than four years after 2.0.1. The repository pushed on 15 July 2026, but GitHub issues are disabled, so the visible count of three represents pull requests rather than a working support queue. The fixed file format reduces feature churn, yet the long release gap and unavailable issue tracker mean adopters should expect to diagnose unusual BIFF files themselves.
Ecosystem3/5Weekly use is recorded at 20,340,659 downloads and GitHub showed 2,206 stars. Much of that reach comes from pandas selecting xlrd for .xls and from old requirements files that outlived the broader format support. The python-excel family still provides historical context, but modern spreadsheet work has moved to openpyxl, python-calamine, and format-specific readers. xlrd's active ecosystem is now one narrow compatibility lane.

Use it if

  • The input is genuinely an Excel 97-2003 .xls file from an old ERP, bank export, government portal, or archive
  • You need BIFF cell type codes, datemode, merged ranges, or formatting records that a DataFrame import would flatten
  • A large multi-sheet .xls should load sheets on demand and release each sheet after processing
  • The deployment platform needs a pure-Python reader and the narrow .xls-only scope is acceptable
Skip it if

Setup reality

We installed xlrd 2.0.2 in a fresh Python 3.12 Bookworm container with no cache. uv finished in 0.2 seconds and left one package using 1 MB on disk. The measurement reported five direct dependencies. The package is pure Python and declares Python >=2.7 while excluding Python 3.0 through 3.5. pip-audit found zero known vulnerabilities. import xlrd worked in 0.20 seconds. The distribution uses the BSD license and does not ship py.typed metadata.

There are no credentials, services, or configuration files. File format is the first gate. inspect_format checks the signature and can distinguish xls, xlsx, xlsb, ods, generic zip data, and unknown input before open_workbook runs. Use it for uploads because users often rename a modern workbook to .xls. A real .xlsx passed to version 2 raises XLRDError. Installing xlrd 1.2.0 to recover modern-format parsing restores old code rather than fixing the format choice; openpyxl or python-calamine is the appropriate branch.

Excel stores dates as numeric serials and the workbook chooses either the 1900 or 1904 date system. Pass book.datemode into xldate_as_datetime or xldate_as_tuple; a hard-coded epoch shifts dates from a Mac-origin workbook. Numeric cells arrive as floats, including identifiers that look integral. Formula cells expose Excel's last cached result because xlrd does not calculate formulas. If the producer saved without recalculation, that value may be stale. Release 2.0.2 prevents malformed formula records from crashing some sheets but does not repair their formulas.

By default, opening a workbook parses every sheet. Set on_demand=True for large files, process one sheet, call unload_sheet, and release workbook resources when finished. formatting_info=True loads XF, font, color, and merged-cell data and adds memory cost. With ragged_rows=True, short rows are no longer padded to sheet.ncols, so callers must respect row_len. Password protection remains unsupported, and ignore_workbook_corruption skips selected consistency failures rather than restoring damaged data.

Patterns

Check the workbook signature inspect-file-format

import xlrd

format_name = xlrd.inspect_format(filename=upload_path)
if format_name != "xls":
    description = xlrd.FILE_FORMAT_DESCRIPTIONS.get(format_name, "unknown")
    raise ValueError(f"Expected .xls bytes, received {description}")

Inspect bytes rather than trusting the extension. Renaming .xlsx to .xls does not turn its ZIP-based format into BIFF.

Open a legacy workbook open-workbook

import xlrd

book = xlrd.open_workbook("legacy.xls")
print(book.sheet_names())
sheet = book.sheet_by_index(0)
print(sheet.name, sheet.nrows, sheet.ncols)

sheet_by_name raises XLRDError when the name is missing. It does not use KeyError like a normal dictionary lookup.

Parse uploaded bytes in memory read-upload-bytes

import xlrd

content = uploaded_file.read()
book = xlrd.open_workbook(
    filename=uploaded_file.name,
    file_contents=content,
)
sheet = book.sheet_by_index(0)

file_contents supplies the actual workbook. The filename is still useful in diagnostics even though xlrd does not read it from disk in this mode.

Map rows to header names iterate-row-values

headers = sheet.row_values(0)
for row_index in range(1, sheet.nrows):
    values = sheet.row_values(row_index)
    record = dict(zip(headers, values))
    process(record)

row_values discards ctype information. Use row() or cell() when dates, errors, empty cells, and numbers must stay distinguishable.

Interpret a cell by its BIFF type branch-on-cell-type

from xlrd import XL_CELL_DATE, XL_CELL_EMPTY, XL_CELL_ERROR, XL_CELL_NUMBER

cell = sheet.cell(row_index, column_index)
if cell.ctype == XL_CELL_EMPTY:
    value = None
elif cell.ctype == XL_CELL_NUMBER:
    value = cell.value
elif cell.ctype == XL_CELL_DATE:
    value = xlrd.xldate_as_datetime(cell.value, book.datemode)
elif cell.ctype == XL_CELL_ERROR:
    value = xlrd.error_text_from_code[cell.value]
else:
    value = cell.value

Numeric cells are floats. Convert identifier columns to int only when the file contract says fractional values are impossible.

Convert a serial date with its workbook epoch convert-excel-date

from xlrd import xldate_as_datetime

cell = sheet.cell(rowx=1, colx=3)
created_at = xldate_as_datetime(cell.value, book.datemode)

book.datemode selects the 1900 or 1904 system. Using the wrong value shifts the result by 1,462 days.

Release each sheet after processing load-sheets-on-demand

book = xlrd.open_workbook("large.xls", on_demand=True)
try:
    for sheet_name in book.sheet_names():
        sheet = book.sheet_by_name(sheet_name)
        process_sheet(sheet)
        book.unload_sheet(sheet_name)
finally:
    book.release_resources()

on_demand avoids parsing every sheet at open time. release_resources closes file or mmap resources even when processing fails.

Avoid padding sparse rows read-ragged-rows

book = xlrd.open_workbook("sparse.xls", ragged_rows=True)
sheet = book.sheet_by_index(0)

for row_index in range(sheet.nrows):
    length = sheet.row_len(row_index)
    values = sheet.row_values(row_index, end_colx=length)
    process(values)

With ragged_rows enabled, a row may be shorter than sheet.ncols. Use row_len before indexing its cells.

Locate values in merged cells read-merged-ranges

book = xlrd.open_workbook("layout.xls", formatting_info=True)
sheet = book.sheet_by_index(0)

for row_low, row_high, col_low, col_high in sheet.merged_cells:
    value = sheet.cell_value(row_low, col_low)
    print((row_low, row_high, col_low, col_high), value)

Merged ranges are half-open, and only the top-left cell stores the value. formatting_info is required for complete merged-cell information.

Inspect a cell's font record read-cell-format

book = xlrd.open_workbook("styled.xls", formatting_info=True)
sheet = book.sheet_by_index(0)

xf = book.xf_list[sheet.cell_xf_index(0, 0)]
font = book.font_list[xf.font_index]
print(font.name, font.bold, font.italic)

BIFF styles reference shared XF and font tables. These records describe formatting; xlrd cannot save a modified workbook.

Select xlrd explicitly in pandas read-xls-with-pandas

import pandas as pd

frame = pd.read_excel(
    "legacy.xls",
    engine="xlrd",
    sheet_name="Transactions",
)

Use this only for .xls. pandas needs openpyxl, calamine, or another matching engine for modern workbook formats.

Route formats to matching readers handle-unsupported-format

import xlrd
from openpyxl import load_workbook

format_name = xlrd.inspect_format(filename=path)
if format_name == "xls":
    workbook = xlrd.open_workbook(path)
elif format_name == "xlsx":
    workbook = load_workbook(path, read_only=True, data_only=True)
else:
    raise ValueError(f"Unsupported workbook format: {format_name}")

Routing is safer than catching every XLRDError because that exception can also report a damaged .xls file.

Alternatives

PackageRegistryPick it when
openpyxlPyPIChoose it for reading or writing .xlsx and .xlsm workbooks, including formulas, styles, and modern worksheet structures
python-calaminePyPIChoose it when one reader should accept .xls, .xlsx, .xlsb, and .ods through a Rust-backed engine
pandasPyPIChoose it when the desired result is a DataFrame and engine-specific workbook details are unimportant
pyxlsbPyPIChoose it for the binary .xlsb format, which is unrelated to the older BIFF .xls format handled here

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.