How-to
Getting Started With Python Excel AutomationDeep dive

Lightweight Python Excel Libraries Compared

When pandas and openpyxl are more than you need: pylightxl, python-calamine, tablib and pyexcel compared on what they read and write, their dependencies and install size, read speed on a 50,000-row file, the types they return for numbers and dates, and the compatibility surprises found in testing — with a recommendation for each use case.

pandas plus openpyxl is the default answer for Excel in Python, but it is not always the right one. A small command-line tool or AWS Lambda function that only needs to read a sheet does not want pandas, NumPy and their tens of megabytes; a script that must run on a locked-down machine may only be allowed pure-Python packages; and a job reading hundreds of large workbooks wants the fastest reader available. Several smaller libraries fill these gaps: pylightxl (pure Python, no dependencies), python-calamine (a Rust-based reader), and tablib and pyexcel (simple tabular interfaces over other engines). This page compares them on the same files — what they can do, what they pull in, how fast they read, what types they return — and reports two compatibility surprises found in testing. It belongs to Choosing a Python Excel Library.

Prerequisites

Bash
pip install pylightxl python-calamine tablib[xlsx] pyexcel pyexcel-xlsx openpyxl

The test files are the same 50,000-row sheet — ID, date, region, quantity, price — written twice: once by openpyxl (as pandas also does by default) and once by XlsxWriter. The versions tested were pylightxl 1.61, python-calamine 0.8, tablib 3.10, pyexcel 0.7 with pyexcel-xlsx 0.6, and openpyxl 3.1 as the baseline; timings are from one run on an ordinary laptop and are for comparison only.

The contenders

  • pylightxl — a single pure-Python module with no dependencies. Reads and writes .xlsx values (no formatting, no charts). Its selling point is that it can be copied into a project or installed where nothing else can.
  • python-calamine — Python bindings to the Rust calamine crate. Read-only, but reads .xlsx, .xlsm, .xlsb, .xls and .ods, and is very fast. It ships as a compiled wheel with no Python dependencies; pandas uses it for engine="calamine".
  • tablib — a library for tabular datasets with import and export to many formats (CSV, JSON, YAML, .xlsx). For Excel it uses openpyxl under the hood, so it is light in API, not in dependencies.
  • pyexcel — a uniform API over plugins; .xlsx support comes from the pyexcel-xlsx plugin, which also uses openpyxl.
What each library can do pylightxl reads and writes values with no dependencies. python-calamine only reads, but handles xlsx, xlsb, xls and ods quickly. tablib and pyexcel read and write xlsx through openpyxl. openpyxl reads and writes values, styles, charts and formulas. read write formats depends on pylightxl ✓ values ✓ values xlsx nothing python-calamine ✓ fast ✗ xlsx xlsb xls ods Rust wheel tablib ✓ ✓ xlsx + csv, json… openpyxl pyexcel ✓ ✓ via plugins openpyxl (plugin) openpyxl ✓ + styles ✓ + charts xlsx, xlsm et_xmlfile

Step 1 — Read the same file with each

Each library reads the sheet into rows; the helper times it and shows the first data row so the returned types are visible:

Python
import time
import pylightxl as xl
import pyexcel
import tablib
from openpyxl import load_workbook
from python_calamine import CalamineWorkbook

readers = {
    "pylightxl": lambda f: list(xl.readxl(f).ws("Data").rows),
    "python-calamine": lambda f: CalamineWorkbook.from_path(f).get_sheet_by_name("Data").to_python(),
    "pyexcel": lambda f: pyexcel.get_array(file_name=f),
    "tablib": lambda f: (lambda d: [tuple(d.headers)] + list(d))(tablib.Dataset().load(open(f, "rb"), format="xlsx")),
    "openpyxl (read-only)": lambda f: list(load_workbook(f, read_only=True)["Data"].iter_rows(values_only=True)),
}

def run(path):
    results = {}
    for name, read in readers.items():
        start = time.perf_counter()
        try:
            rows = read(path)
            results[name] = (round(time.perf_counter() - start, 2), len(rows), rows[1][:4])
        except Exception as e:
            results[name] = (None, 0, f"{type(e).__name__}: {e}")
    return results

xw = run("from_xlsxwriter.xlsx")
for name, (secs, n, first) in xw.items():
    print(f"{name:22} {secs!s:>6}s  rows={n}  first={first}")

All five read the XlsxWriter-made file. python-calamine is in a class of its own — around a tenth of a second where the others take three quarters of a second to a second and a half — because its parser is compiled Rust. openpyxl in read-only mode is the fastest of the pure-Python readers; tablib and pyexcel are slower than openpyxl because they use openpyxl and then convert its output. pylightxl, despite being tiny, is not fast: it parses with the standard library's XML module and builds all rows in memory. tablib keeps the header row separately in headers, so the helper puts it back in front to make the row counts comparable.

Seconds to read 50,000 rows (one run, XlsxWriter file) python-calamine read the file in about 0.1 seconds. openpyxl in read-only mode took about 0.8 seconds, tablib about 1.2, pyexcel about 1.4 and pylightxl about 1.5 seconds. python-calamine ≈ 0.1 s openpyxl read-only ≈ 0.8 s tablib ≈ 1.2 s pyexcel ≈ 1.4 s pylightxl ≈ 1.5 s

Step 2 — Look at the types you get back

Speed is not the only difference. The first data row printed above shows each library's idea of a number and a date:

Python
for name, (_, _, first) in xw.items():
    print(f"{name:22}", [type(v).__name__ for v in first], first[1])

The differences matter as soon as values are used. pylightxl returns the date as the string '2026/01/01' — it applies the cell's date format itself and gives you text, so date arithmetic needs parsing first. python-calamine returns a proper datetime.date, but returns whole numbers as floats (0.0), because Excel stores all numbers as floating point and calamine does not guess which were meant as integers; IDs and quantities need converting with int() where that matters. openpyxl, tablib and pyexcel return datetime.datetime for dates and int for whole numbers. None of these is wrong, but swapping one library for another without checking types is a classic source of subtle bugs — see How pandas Data Types Are Written to Excel Cells for the writing side of the same question.

Step 3 — A compatibility surprise

Now read the copy written by openpyxl — the format most Python-generated workbooks, including those from pandas.DataFrame.to_excel, are in:

Python
op = run("from_openpyxl.xlsx")
for name, (secs, n, first) in op.items():
    print(f"{name:22} {'failed' if secs is None else f'{secs}s':>7}  rows={n}  first={first if n else first[:70]}")

Every library reads it except pylightxl 1.61, which fails with a TypeError before reading any cells. The cause is in how it looks up each sheet's relationship ID: it only recognises the r: namespace when it is declared on the root element of workbook.xml, and openpyxl declares it on each <sheet> element instead — perfectly valid XML that pylightxl's lookup misses. Files saved by Excel and by XlsxWriter declare it on the root, so they read fine. The lesson is broader than one bug: small libraries are tested against fewer producers of .xlsx files, so test any lightweight reader against files from every source your job will see before relying on it.

The same sheet, two producers pylightxl read the workbook written by XlsxWriter but failed on the identical data written by openpyxl, because openpyxl declares the relationships namespace on each sheet element rather than on the root. python-calamine, tablib, pyexcel and openpyxl read both. file from XlsxWriter namespace declared on root all five libraries read it pylightxl dates come back as text file from openpyxl / pandas namespace declared per sheet pylightxl 1.61: TypeError the other four read it

Step 4 — Writing

For writing, the field narrows. python-calamine cannot write at all. tablib and pyexcel write .xlsx through openpyxl. pylightxl writes values with no dependencies:

Python
db = xl.Database()
db.add_ws(ws="Summary")
for r, row in enumerate([["Region", "Qty"], ["North", 120], ["South", 85]], start=1):
    for c, value in enumerate(row, start=1):
        db.ws("Summary").update_index(row=r, col=c, val=value)
xl.writexl(db=db, fn="summary_pylightxl.xlsx")

data = tablib.Dataset(["North", 120], ["South", 85], headers=["Region", "Qty"])
with open("summary_tablib.xlsx", "wb") as fh:
    fh.write(data.export("xlsx"))

for f in ("summary_pylightxl.xlsx", "summary_tablib.xlsx"):
    print(f, [[c.value for c in row] for row in load_workbook(f).active.iter_rows()])

Both produce a valid workbook that openpyxl reads back. Neither offers formatting worth the name — no number formats, column widths or fonts in pylightxl; tablib makes the header bold and little more. For anything that will be read by people rather than programs, XlsxWriter (a single pure-Python package with no dependencies of its own) is the better lightweight writer: it is fast, small and supports formatting, charts and tables. It is covered in Write a Formatted Excel Report with XlsxWriter.

Install footprint

On disk, after installation, the packages compared here occupied roughly: pylightxl 0.2 MB, tablib 0.3 MB (plus openpyxl), pyexcel with its io layer and xlsx plugin about 1 MB (plus openpyxl), python-calamine 2.2 MB (a compiled binary), openpyxl 2.8 MB, and XlsxWriter 2 MB. pandas with NumPy is well over 50 MB. For a Lambda function or a container image, the gap between "openpyxl or calamine" and "pandas" is what matters; between the small libraries, it rarely does.

Which to use

  • Fastest reading, any Excel format: python-calamine — directly, or through pandas.read_excel(engine="calamine") when you have pandas anyway. Convert floats back to integers where needed.
  • No dependencies allowed, simple values: pylightxl — but test it against your actual files, and parse its date strings.
  • Many formats through one small API: tablib, accepting openpyxl as a dependency.
  • Reading and writing with formatting, still lightweight: openpyxl to read, XlsxWriter to write.

More detailed timing across engines is in Benchmark Python Excel Read and Write Speed.

Common pitfalls

  • Assuming a small library reads every .xlsx. Test with files from Excel, openpyxl, XlsxWriter and any other producer you receive.
  • Ignoring returned types. Dates as text, integers as floats — check before computing.
  • Expecting tablib or pyexcel to avoid openpyxl. For .xlsx they use it underneath.
  • Using calamine to write. It is read-only.
  • Choosing a reader by install size alone. Speed and correctness on your files matter more.

Conclusion

The lightweight Excel libraries each have a niche. python-calamine is by far the fastest reader and handles every Excel format, returning floats for all numbers. pylightxl needs nothing but Python and reads and writes plain values, though it returns dates as text and, in version 1.61, could not read files written by openpyxl. tablib and pyexcel offer simple tabular APIs on top of openpyxl. For most jobs that want to stay small, python-calamine for reading and XlsxWriter for writing is the strongest pair — after testing against the files you actually receive.

Frequently asked questions

What is the fastest Python library for reading Excel files? python-calamine, which wraps the Rust calamine crate. It read a 50,000-row sheet in about a tenth of a second in our test, roughly eight times faster than openpyxl in read-only mode. pandas can use it with engine="calamine".

Is there an Excel library for Python with no dependencies? pylightxl is a single pure-Python module with no dependencies that reads and writes cell values. XlsxWriter also has no dependencies but only writes.

Why can't pylightxl read my Excel file? pylightxl 1.61 failed with a TypeError on workbooks written by openpyxl, including pandas output, because it does not find the relationships namespace when it is declared on the sheet elements. Files from Excel and XlsxWriter read correctly.

Do tablib and pyexcel need openpyxl? Yes, for xlsx files. tablib's xlsx support and the pyexcel-xlsx plugin both use openpyxl underneath.

More on choosing a library

More on choosing a library →