pip install xlrd★Gets 2.0.1 (11 Dec 2020), the final release. Pure Python, no dependencies.import xlrd; xlrd.__version__Confirm which side of the 2.0 divide you're on.pip install xlrd==1.2.0don'tThe "fix" every forum suggests. It restores.xlsxalong with the XML vulnerabilities that got it removed — and modern pandas rejects it anyway.xlrd.inspect_format(path)★Sniff before you open: returns'xls','xlsx','xlsb','ods','zip'orNone.xlrd.FILE_FORMAT_DESCRIPTIONSMaps those codes to human-readable names. xlrd can identify far more than it can read.2.0.0 removed everything but .xlsby designAlso droppedpsyco, and changed the fallback encoding fromasciitoiso-8859-1.
book = xlrd.open_workbook("file.xls")★Parses the entire file immediately and returns aBook.open_workbook(file_contents=blob)★Read from bytes or anmmapinstead of a path —filenameis then ignored.open_workbook("~/data.xls")~is expanded to your home directory automatically.with open_workbook(f) as book: …★Context-manager form callsrelease_resources()on exit.open_workbook(f, logfile=open("log.txt", "w"))Warnings and diagnostics go to stdout by default. Redirect them.open_workbook(f, verbosity=2)Turn up the trace detail when a file won't parse.
formatting_info=True★Unlocks fonts, colours, XF records, merged cells — and makes blank cells visible. Costs memory.on_demand=True★Load sheets only when asked for. See card 20.ragged_rows=True★Stop padding short rows out toncols. Big saving on uneven sheets.encoding_override="cp1251"For old files with a missing or lyingCODEPAGErecord.ignore_workbook_corruption=TrueSwallowCompDocError: Workbook corruptionand try anyway.use_mmap=FalseOverride the heuristic (mmap is used whenever it exists).logfile=· verbosity=· file_contents=The remaining three — see card 02.
book.nsheets★Sheet count. Known without loading any sheet.book.sheet_names()★List of names, also available before loading.book.datemode★0= 1900 system,1= 1904. You must pass this to every date conversion.book.biff_version80= BIFF8 (Excel 97+), down to20for Excel 2.x. Tells you what to expect.book.codepage · book.encoding1200means UTF-16LE. Older files derive a Python codec from the codepage.book.user_nameWhoever last saved the file, if recorded.book.countriesTelephone country codes — a hint at the right encoding for an unknown codepage.book.release_resources()★Drop the mmap and parsed data but keep loaded sheets. Safe to call twice.
sheet = book.sheet_by_index(0)★Index inrange(book.nsheets), zero-based.sheet = book.sheet_by_name("Data")★By name. RaisesXLRDErrorif there's no such sheet.book[0] · book["Data"]★Item access — added in 2.0.0. Both forms work.for sheet in book: sheet.nameBooks became iterable in 2.0.0 too.book.sheets()A list of all sheets — forces every sheet to load.book.sheet_loaded(0)Only meaningful withon_demand=True.
sheet.nrows · sheet.ncols★Row indexes arerange(sheet.nrows). There is nolen(sheet).sheet.name★The sheet's title.sheet.book.datemodeEvery Sheet keeps a back-reference to its Book. Handy inside helpers.sheet.row_len(rowx)★The real length of one row — essential withragged_rows=True.sheet.visibility0visible ·1hidden ·2very hidden (VBA-only).ncols is max index + 1careTrailing empty columns are ignored, so rows can be shorter than you expect.
sheet.cell_value(0, 0)★Straight to the value. This is cell A1.sheet.cell(rowx, colx)★TheCellobject:.ctype,.value,.xf_index.sheet.cell_type(rowx, colx)★Just the type code, without building a Cell.sheet.cell_xf_index(rowx, colx)formatting_infoIndex intobook.xf_list.sheet.cell(-1, -1)Negative indexes work exactly as they do on Python lists.sheet["A1"] · sheet.cell(1, 1)no A1 notationxlrd has none. And(1, 1)is B2 here, not A1 — the classic openpyxl migration bug.
sheet.row_values(rowx)★A plain list of values — what you want 90% of the time.sheet.row_values(rowx, 2, 5)★Sliced bystart_colx,end_colx.sheet.row(rowx)The same span asCellobjects, so you keep the types.sheet.row_types(rowx)Just the type codes for the row.sheet.row_slice(rowx, 0, 3)Sliced Cell objects.for row in sheet.get_rows(): …★A generator over rows of Cells — the cleanest full-sheet loop.[sheet.row_values(r) for r in range(1, sheet.nrows)]★Skip the header and materialize the body.
sheet.col_values(colx)★Values down one column.sheet.col_values(colx, 1)★start_rowx=1skips the header.sheet.col(colx)Cell objects for the whole column.sheet.col_types(colx, 1, 50)Type codes over a row range.sheet.col_slice(colx, 1, 50)Cell objects over a row range.dict(zip(sheet.row_values(0), zip(*rows)))Header-keyed columns, without pandas.
XL_CELL_EMPTY # 0 → ''No cell record existed at all.XL_CELL_TEXT # 1 → str★A Unicode string.XL_CELL_NUMBER # 2 → float★Always a float — there is no int type in Excel.XL_CELL_DATE # 3 → float★Also a float. The type says "date"; the value does not. See card 11.XL_CELL_BOOLEAN # 4 → int1is TRUE,0is FALSE.XL_CELL_ERROR # 5 → intAn internal Excel error code. See card 13.XL_CELL_BLANK # 6 → ''formatting_infoFormatted but empty. Invisible unless you asked for formatting.xlrd.sheet.ctype_text[cell.ctype]A readable name for the code, for logging.
xldate_as_datetime(cell.value, book.datemode)★The modern converter. Returns a realdatetime.datetime.xldate_as_tuple(value, datemode)★Returns(y, m, d, h, mi, s)— feed it to any date constructor.from xlrd import xldate_as_datetimeImportable from the top level since 1.1.0.forgetting book.datemode4 years offMac-authored files use the 1904 system. Guess wrong and every date shifts by 1,462 days.0.0 <= xldate < 1.0A time of day, not a date. You get(0, 0, 0, h, m, s).xldate_from_date_tuple((2020,1,1), 0)The inverse, plus_from_time_tupleand_from_datetime_tuple.a date column read as plain numberscheck ctypeA "date" is only a date ifctype == 3. Formatting alone doesn't make one.
XLDateAmbiguous1900 leap bugRaised whendatemode == 0and1.0 <= xldate < 61.0— Excel believes 1900 was a leap year, so these serials are genuinely undecidable.XLDateNegativeThe serial is below zero. Usually a corrupt or misread cell.XLDateTooLargeWould land in year 10000 or later.XLDateBadDatemodedatemodewas neither0nor1.XLDateBadTupleOut-of-range component when converting the other way.except xlrd.xldate.XLDateError:★The common base class — catch this one and log the row.
xlrd.error_text_from_code[value]★Turns the stored int into#DIV/0!,#N/Aand friends.0:#NULL! · 7:#DIV/0! · 15:#VALUE!The codes are sparse, not sequential.23:#REF! · 29:#NAME? · 36:#NUM! · 42:#N/AThe remaining four.if cell.ctype == 4: bool(cell.value)Booleans arrive as1/0, notTrue/False.cell.value is TrueneverIt's an int. Compare with==or cast.
BIFF8 → always UTF-16LE★Excel 97 and later. Strings come back as properstr, no work needed.book.codepage == 1200Confirms Unicode. Older files carry things like1252or10000.open_workbook(f, encoding_override="koi8_r")★When the CODEPAGE record is missing or simply wrong.no CODEPAGE → iso-8859-1The 2.0.0 fallback. Before that it assumedasciiand often raised.mojibake in a pre-97 filecodepageCheckbook.countriesfor a hint, then override the encoding.
open_workbook(f, formatting_info=True)★Off by default to save memory. Everything in cards 15–17 needs it.blank cells become visible★BLANK and MULBLANK records are read, sonrows/ncolscan grow.book.xf_list[cell.xf_index]★The XF record:.font_index,.format_key,.alignment,.border,.background,.protection.book.format_map[xf.format_key].format_str★The number-format string, e.g."0.00%". Useformat_map, notformat_list.book.style_name_mapMaps style names to(built_in, xf_index).memory and parse time both jumpcostOnly ask for formatting when you actually need it.
book.font_list[xf.font_index]★.name,.bold,.italic,.height,.colour_index,.underline_type.font.height # twipsA twip is 1/20 of a point, so divide by 20 for pt.font.weight # 400 normal, 700 bold.boldis a redundant convenience flag.book.colour_map[idx] → (r, g, b)★Resolve any colour index. "Magic" indexes map toNone.book.palette_recordThe raw 56-entry palette, only if the user customised it. For writing, not rendering.xf.background.background_colour_indexAlso.fill_patternand.pattern_colour_index.xf.border.top_line_style0none ·1thin ·2medium ·5thick ·6double, and more.sheet.computed_column_width(colx)In 1/256ths of a zero-character width.
sheet.merged_cellsformatting_info★A list of(rlo, rhi, clo, chi)tuples.upper bounds are exclusive★[2, 3, 7, 9]spans only two cells — row 2, columns 7–8.rlo, clo carries the value★The rest of the block reads as BLANK. Fill them yourself if you need a rectangle.open_workbook(f, ragged_rows=True)★Rows keep their true length instead of being padded toncols.sheet.row_len(rowx)★The companion call — always use it once ragged rows are on.sheet.rowinfo_map · sheet.colinfo_mapPer-row and per-column height, width, hidden and outline level.
book.name_map["tax_rate"]★Lower-cased name to a list ofNameobjects, sorted by scope.book.name_and_scope_map[(name, scope)]Exactly one Name per key.name.cell()★Convenience for a name pointing at a single cell.name.area2d()★Returns(sheet, rowxlo, rowxhi, colxlo, colxhi), clipped to the sheet by default.name.scope # -1 = global-2macro sheet,-3invalid,0..nsheetslocal to that sheet.name.builtinFlags the auto-generated ones likePrint_AreaandPrint_Titles.files older than Excel 5.0no namesName info isn't extracted whenbook.biff_version < 50.
sheet.cell_note_map[(rowx, colx)]Sparse map toNoteobjects:.text,.author,.show.sheet.hyperlink_map[(rowx, colx)]Maps to aHyperlink:.url_or_path,.type,.desc,.textmark.sheet.hyperlink_listAll of them; a link can cover a rectangle of cells.sheet.rich_text_runlist_map[(r, c)]formatting_info(offset, font_index)pairs for mixed formatting inside one cell.sheet.vert_split_pos · sheet.horz_split_posFrozen-pane positions.sheet.horizontal_page_breaksAndvertical_page_breaks, both formatting-only.sheet.row_label_ranges · sheet.col_label_rangesFrom Excel's old Insert > Name > Labels feature.
book = open_workbook(f, on_demand=True)★Sheets load only when you ask for them.book.unload_sheet(0)★Free one sheet after you're done with it. Name or index.book.release_resources()★Call when finished loading but still using the Book. Automatic in awithblock.book.sheets()defeats itLoads everything — as does iterating the Book. Usesheet_names()instead.on_demand + BIFF < 5.0ignoredSilently falls back to loading everything, with a warning to the logfile.book.load_time_stage_1 · _stage_2Seconds spent extracting vs parsing — useful when profiling a slow file.
xlrd.dump("broken.xls")★Every BIFF record in char and hex. The tool of last resort.xlrd.dump(f, unnumbered=True)Omit offsets so you can diff two dumps.xlrd.count_records("file.xls")A sorted(record_name, count)summary.python -m xlrd.runxlrd …The bundled CLI:biff_dump,biff_count,show,fonts.obj.dump()Almost every xlrd class inherits adump()for inspection.CompDocError: Workbook corruptionTryignore_workbook_corruption=Truebefore giving up.XLRDError: Unsupported formatThe error text includes the first 8 bytes — check them against the OLE2 signature.
pd.read_excel("legacy.xls")★pandas picks xlrd automatically for.xls— still true in pandas 3.0.pd.read_excel(f, engine="xlrd")Explicit, and clearer in code others will read.Pandas requires version '2.0.1' or newer of 'xlrd'upgradeYou pinned 1.2.0. Upgrade xlrd and switch.xlsxwork to openpyxl.pd.read_excel(f, sheet_name=None)★Every sheet as a dict of DataFrames.pd.DataFrame(sheet.get_rows())Going the manual route gives you Cell objects — map.valuefirst.pd.read_excel(f, engine="calamine")Reads.xlstoo, and is considerably faster. Needspython-calamine.
xlrd cannot write. At all.by designThe name is "xl read". There is no save, no write, no modify.pip install xlwtThe sibling writer for.xls. Also frozen, and it cannot read.pip install xlutils★The bridge:from xlutils.copy import copyturns a Book into a writable xlwt workbook.copy(open_workbook(f, formatting_info=True))★Withoutformatting_infothe copy loses all styling.producing new .xls in 2026reconsiderWrite.xlsxwith openpyxl or XlsxWriter unless a downstream system truly demands.xls.
.xlsx → openpyxl★The direct replacement, and it writes as well as reads..xlsb → pyxlsb · .ods → odfpyBinary and OpenDocument each need their own reader.anything → python-calamine★One Rust-backed reader for.xls,.xlsx,.xlsm,.xlsband.ods. Read-only, fast.libreoffice --headless --convert-to xlsx★Batch-convert the legacy pile once, then never think about BIFF again.index shift: (0,0) → (1,1)★The first thing to fix when porting xlrd code to openpyxl. See diagram 1.dates arrive as datetime alreadyopenpyxl and calamine both convert for you — drop thexldate_*calls.keep xlrd if: .xls, read-only, works★Frozen is not the same as broken. A stable parser for a stable format is fine.