Skip to main content

excelreader (Python)

Read and write XLSX, XLSB, XLS and CSV through ExcelReader's NativeAOT library. No .NET runtime required — the shared library is self-contained.

Install (from source)

python python/scripts/build_native.py   # requires the .NET 10 SDK, once per machine
pip install -e "python[dev]"

build_native.py publishes src/ExcelReader.Native for your platform and copies the resulting ExcelReader.Native.{dll,so,dylib} into excelreader/_lib/. To point at a binary you built elsewhere, set EXCELREADER_NATIVE_LIB to its full path.

Usage

from excelreader import open_workbook

with open_workbook("book.xlsx") as workbook:
    print(workbook.sheet_count, workbook.sheet_name)
    for row in workbook.rows():
        for cell in row:
            print(cell.column, cell.type.name, cell.value)

Formats

open_workbook sniffs XLS/XLSX/XLSB by file signature. CSV has no signature, so it is chosen by the .csv extension — or explicitly:

open_workbook("data.txt", format="csv")

Dates

cell.value is always the raw text as stored, so CellType.DATE cells hold Excel serial numbers. Use Cell.as_date() to convert, passing the workbook's epoch flag:

as_date = cell.as_date(workbook.is_date1904)

as_date() returns None for any cell that isn't CellType.DATE.

Reading everything at once

rows() iterates row-by-row; read_all() materializes the whole sheet in one call:

all_rows = workbook.read_all()

This holds every row in memory at once, so prefer rows() for very large sheets.

Reading everything at once, faster

read_all()/rows() build one Cell/str object per cell, which dominates wall-clock time on a large sheet. read_all_columnar() decodes the same data into parallel flat arrays instead — no per-cell object construction — and is several times faster on large sheets:

sheet = workbook.read_all_columnar()
# sheet.row_offsets[i]:row_offsets[i+1]  -> cell indices for row i
# sheet.columns[j] / sheet.types[j]      -> cell j's column index / CellType
# sheet.value_offsets[j]:[j+1]           -> cell j's byte slice into sheet.values

Materialize a single cell on demand instead of decoding every value up front:

from excelreader import decode_cell

first_cell = decode_cell(sheet, 0)

Each array is a stdlib array.array('i'), or a NumPy int32 array if NumPy is installed (pip install -e "python[numpy]") — NumPy is optional and never required.

Typed columns — the fastest path

Everything above hands back cell text, which means the library formats every value to a string on the way out. parse_typed() skips that entirely: you give it a schema, and the conversion happens natively, straight into typed column buffers. On a 65,536 × 14 sheet it is ~8× faster than read_all_columnar(), ~25× faster than read_all(), and faster than polars.read_excel() — see docs/NATIVE_BASELINE.md.

from excelreader import ColumnSpec, ColumnType

with open_workbook("sales.xlsb") as workbook:
    table = workbook.parse_typed([
        ColumnSpec(ColumnType.STRING, name="Region"),
        ColumnSpec(ColumnType.DATE, name="Order Date"),
        ColumnSpec(ColumnType.F64, name="Total Revenue", nullable=True),
    ])

table.row_count            # rows read
table.names                # ["Region", "Order Date", "Total Revenue"]
region, day, revenue = table.columns
region[0]                  # "Asia" — strings decode on demand, not one str per row up front
day[0]                     # 15477 — days since 1970-01-01
revenue[0]                 # 14862.69
table.validity[2]          # bit-packed nulls, or None when the column has none

Leave name out to resolve a column by position instead: ColumnSpec(ColumnType.I64, index=3). header_row defaults to 1 (the first row names the columns); pass header_row=0 for a sheet with no header, where every spec must resolve by index.

A column that fails to convert is an error unless its spec sets nullable=True, which records the failure in table.validity and keeps reading.

Note that parse_typed() always reads the whole sheet from its first row, independent of how far rows() has advanced — and it leaves that cursor alone.

Guessing a schema

Writing the ColumnSpec list by hand means already knowing every column's name and type. When you don't, infer_schema() samples the sheet and guesses one for you:

with open_workbook("sales.xlsb") as workbook:
    schema = workbook.infer_schema()   # header_row=1, sample_size=100 by default
    table = workbook.parse_typed(schema)

Each column's type comes from the CellType Excel already stored for its sampled cells — not text sniffing — so it costs nothing beyond the sample and is exact for XLSX/XLSB/XLS. A column with a real mix of kinds, only formula/error results, or nothing sampled falls back to ColumnType.STRING; nullable is set when any sampled row left the column empty. CSV cells carry no such type tag, so every CSV column is guessed ColumnType.STRING — inspect the result (or just try parsing) before trusting it, especially past the sample.

Writing

write_workbook() writes a TypedTable (what parse_typed() returns) back out as a single sheet, through the same xl_write_typed native export — one-shot, no writer handle before or after the call:

from excelreader import ColumnType, write_workbook

with open_workbook("sales.xlsb") as workbook:
    table = workbook.parse_typed(workbook.infer_schema())

types = [ColumnType.STRING, ColumnType.DATE, ColumnType.F64]  # one per table.columns, in order
write_workbook("sales_copy.xlsx", table, types)

types is required because a TypedTable column is a raw buffer (array/StringColumn/NumPy array) and nothing about the buffer alone tells I64 from TIME apart — both are 8-byte-per-row arrays. format is inferred from the path's extension (one of xlsx/xlsb/xls/csv) or set explicitly:

write_workbook("report.dat", table, types, format="csv")

write_pandas() and write_polars() build the table from a DataFrame instead (both go through write_arrow(), so pyarrow must be installed):

from excelreader import write_pandas, write_polars

write_pandas("report.xlsx", df)          # requires pandas + pyarrow
write_polars("report.xlsx", polars_df)   # requires polars + pyarrow

WriteOptions sets the sheet name and CSV dialect, mirroring xl_write_options — every field defaults to None, meaning "use the library default":

from excelreader import WriteOptions

write_workbook(
    "report.xlsx", table, types,
    options=WriteOptions(sheet_name="Q3 Results", use_shared_strings=True),
)

Phase-1 limits, stated plainly: a single sheet only (no multi-sheet workbooks); the whole table must already be in memory (no streaming/chunked writes); no styling beyond the temporal number formats xl_write_typed applies to DATE/TIME/TIMESTAMP columns. format="auto" is not accepted — sniffing reads a file's existing signature bytes, and a file being created has none.

Arrow

With pyarrow installed, to_arrow() runs the same read and hands the buffers to pyarrow zero-copy over the Arrow C Data Interface:

import pyarrow as pa

with open_workbook("sales.xlsb") as workbook:
    array = workbook.to_arrow(schema)

batch = pa.RecordBatch.from_struct_array(array)

pyarrow owns the buffers from that point on, so the result stays valid after the workbook is closed.

From memory

from excelreader import open_bytes

with open_bytes(payload) as workbook:
    ...

Reader options

open_workbook()/open_bytes() take an optional OpenOptions for CSV dialect settings and reader resource limits. Every field defaults to None, meaning "use the library default", so you set only what you want to change.

from excelreader import OpenOptions, open_workbook

# A semicolon-delimited CSV, which the default comma dialect would read as one column per row.
with open_workbook("export.csv", format="csv", options=OpenOptions(csv_delimiter=ord(";"))) as workbook:
    for row in workbook.rows():
        ...

csv_delimiter and csv_quote are byte values, so pass ord(";") rather than ";".

The max_* fields are resource limits rather than tuning knobs: they bound what a malformed or hostile file can make the reader allocate, and exceeding one raises ExcelReaderError. Lower them when parsing untrusted uploads.

options = OpenOptions(
    max_total_decompressed_bytes=64 * 1024 * 1024,  # zip-bomb budget for XLSX/XLSB
    max_cell_bytes=1024 * 1024,
    max_zip_entries=1024,
)

prefetch_decompression=True overlaps inflating an XLSX/XLSB sheet with parsing it — worth it for single-file batch work, not for a server already reading many files in parallel. See the root README for the measured trade.

Notes

  • A Workbook is not thread-safe. Use one per thread.
  • Empty cells are skipped, so cell.column may skip indices. Do not assume row[i].column == i.
  • The ABI is documented in src/ExcelReader.Native/include/excelreader.h.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

excelreader_native-2.1.2-py3-none-win_amd64.whl (1.6 MB view details)

Uploaded Python 3Windows x86-64

excelreader_native-2.1.2-py3-none-manylinux_2_39_x86_64.whl (1.7 MB view details)

Uploaded Python 3manylinux: glibc 2.39+ x86-64

excelreader_native-2.1.2-py3-none-macosx_26_0_arm64.whl (1.6 MB view details)

Uploaded Python 3macOS 26.0+ ARM64

File details

Details for the file excelreader_native-2.1.2-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for excelreader_native-2.1.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 95bebaf2837a9ea3339263238744c7cda80a5b902e4f7e1e2e2b1510f237e0d5
MD5 1627daa0174f64899cdbb8bf4918d9c6
BLAKE2b-256 7a6d13044e5bc1f1bfa4b9ef74b3d4acdef376716ecef75c9c290977eddaadaa

See more details on using hashes here.

Provenance

The following attestation bundles were made for excelreader_native-2.1.2-py3-none-win_amd64.whl:

Publisher: release.yml on GabrielMarquezMatte/ExcelReader

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file excelreader_native-2.1.2-py3-none-manylinux_2_39_x86_64.whl.

File metadata

File hashes

Hashes for excelreader_native-2.1.2-py3-none-manylinux_2_39_x86_64.whl
Algorithm Hash digest
SHA256 4dc4bb9b9c7c68076e9479d191d0ffa5875eea95664c0ce02aee40be0a132289
MD5 56f3b6c76fa6f7ea873a794c7b4575b2
BLAKE2b-256 47f0f0062a6d9f4700208e9bfe380095f981ea2d185aec36f104a4c2c3be0152

See more details on using hashes here.

Provenance

The following attestation bundles were made for excelreader_native-2.1.2-py3-none-manylinux_2_39_x86_64.whl:

Publisher: release.yml on GabrielMarquezMatte/ExcelReader

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file excelreader_native-2.1.2-py3-none-macosx_26_0_arm64.whl.

File metadata

File hashes

Hashes for excelreader_native-2.1.2-py3-none-macosx_26_0_arm64.whl
Algorithm Hash digest
SHA256 dd5a9fd1a10b460a4e3ad108189caa287582eb53b8eb57662c4dcebba2a42419
MD5 c89d3306d901774d9d4d39404fe6e5e2
BLAKE2b-256 d5c898b892909f9049f6e483a5734f1a8daf24eebe0d5c05476d43310fa1df24

See more details on using hashes here.

Provenance

The following attestation bundles were made for excelreader_native-2.1.2-py3-none-macosx_26_0_arm64.whl:

Publisher: release.yml on GabrielMarquezMatte/ExcelReader

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

2.1.3

3 files

This release

2.1.2 This release

3 files

2.1.1

3 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page