Library / SDK
tafia/calamine avatar
tafia/calamine

tafia/calamine: a pure Rust reader for xlsx, xlsb, xls and ods files

A pure Rust Excel/OpenDocument SpreadSheets file reader: rust on metal sheets

2,439 stars254 forksRustMIT

At a glance

What is it?
calamine reads and deserializes spreadsheets in pure Rust, with lazy sheet loading for xlsx and xlsb and a Serde path for typed rows. It is a reader only, and the documentation is explicit that files need to be simple enough.
Who is it for?
Adopt calamine if you need to read xlsx, xlsb, xls or ods files inside a Rust service and you want typed rows through Serde rather than hand-written cell matching. Do not adopt it if you need to write spreadsheets, or if your inputs are heavily formatted workbooks where the README's own hedge about files being simple enough applies.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 29 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What calamine reads, and who it is for

calamine is a Rust library, not a command line tool and not a service. It reads spreadsheet files in two families: the Excel formats (xls, xlsx, xlsm, xlsb, xla, xlam) and OpenDocument spreadsheets (ods). The README describes it as a pure Rust library to read and deserialize any spreadsheet file, and then adds the qualifier that matters: as long as your files are simple enough, this library should just work. That sentence is the honest scope statement. It tells you the target is ingestion of data, not faithful reproduction of every formatting feature a spreadsheet application can produce.

The audience is Rust developers who receive spreadsheets from other people. A billing service that ingests a monthly xlsx export, a data pipeline that reads xlsb files from a finance team, a validation job that checks ods files before they enter a warehouse. In all of those, the work is turning cells into structs, and the failure mode you care about is a type error on row 4000, not a lost cell border. calamine is built for exactly that shape of problem, and its Serde integration is the part that distinguishes it from a generic XML parser pointed at the same file.

How calamine opens a workbook and where the data flows

The entry points are open_workbook, which takes a path and a concrete workbook type, and open_workbook_auto, which infers the format at runtime. Once the workbook is open, you ask for a named sheet and get back a Range, a rectangular block of cells with rows() and used_cells() accessors. DataType is the cell enum, and the README's more complex example counts non-empty cells by filtering out DataType::Empty.

The loading strategy differs by format, and the README states the difference plainly. For xlsx and xlsb, sheets are lazily loaded, so a header row setting takes effect immediately when you read a sheet range. For xls and ods, all sheets are loaded at once when the workbook is opened with default settings, so setting a header row afterwards gives no performance benefit. That is a real architectural split, not a footnote: if you are streaming a large legacy xls file, the whole workbook is already in memory before your header logic runs.

Beyond cell values, the README documents access to the VBA project, defined names, and formulas. worksheet_formula returns formulas per sheet, and defined_names returns name and formula pairs as strings. The VBA side exposes get_module and get_references, and the example checks r.is_missing() to report a reference that is broken or not accessible. Those features are what make calamine useful for auditing workbooks rather than only extracting tables.

Adding calamine and reading your first sheet

Installation is a single Cargo command. The README shows the dependency being added to the project's Cargo.toml this way:

bash
$ cargo add calamine

After that, the simplest read opens a workbook, asks for a sheet by name, and iterates rows. The README's simple reader example looks like this:

rust
use calamine::{Reader, Xlsx, open_workbook};

let mut excel: Xlsx<_> = open_workbook("file.xlsx").unwrap();
if let Ok(r) = excel.worksheet_range("Sheet1") {
    for row in r.rows() {
        println!("row={:?}, row[0]={:?}", row, row[0]);
    }
}

You should see one line per row, with the row slice printed and the first cell repeated. If the sheet name is wrong, worksheet_range returns an error and the if let arm is skipped, which is why the README's Serde example maps that case to calamine::Error::Msg("Cannot find Sheet1").

For typed rows, RangeDeserializerBuilder converts a range into an iterator of your own struct. With headers supplied explicitly, the builder looks like this:

rust
let iter_records =
    RangeDeserializerBuilder::with_headers(&["metric", "value"]).from_range(&range)?;

The README also documents a header row offset, used when the real header is not the first row:

rust
let sheet1 = excel
    .with_header_row(HeaderRow::Row(3))
    .worksheet_range("Sheet1")
    .unwrap();

Remember the format split here. For xlsx and xlsb this takes effect at read time; for xls and ods the sheets are already loaded, so it only changes which row is treated as the header.

The optional features are all off by default. chrono adds Chrono date and time types to the API, dates is a deprecated synonym for it, and picture adds support for reading raw picture data. Enable one with:

bash
cargo add calamine -F chrono

The example read_picture_data in the repository requires the picture feature, and Cargo.toml declares that requirement, so that example will not build without it.

The Serde path and what happens to cells that do not fit

The deserialization layer is where calamine earns its place. RangeDeserializerBuilder::new().from_range(&range) produces an iterator whose items deserialize into a tuple or a struct, and the README's temperature example asserts a (String, f64) pair with label "celsius" and value 22.2222.

The interesting part is the handling of dirty data. Real spreadsheets contain a column that should be floats but sometimes holds a string like "n/a". calamine ships helper functions for that case. deserialize_as_f64_or_none, used through Serde's deserialize_with field attribute, discards invalid values and yields None. deserialize_as_f64_or_string keeps them, returning either a float or the original string. That choice is a design decision you have to make per column, and the README presents both without recommending one, which is correct: discarding bad values silently is right for a metrics import and wrong for an audit trail.

The repository's examples directory covers more ground than the README: deserialize_fallible, deserialize_flatten, deserialize_no_headers, deserialize_range, deserialize_seed and deserialize_struct, plus excel_to_csv, read_hyperlinks, read_picture_data, search_errors, simple_read and xlsx_formula_stream. If your use case is not in the README, that directory is where to look before writing your own adapter.

One consequence of the Serde approach is worth stating. The builder deserializes from a range, so the range is materialized first. For the lazy formats that is fine, but it means the typed iterator is a view over an already-read block of cells, not a streaming parser over the file.

Read-only is the constraint that decides most evaluations

calamine does not write spreadsheets. The README's performance section opens by noting that as calamine is readonly, the comparisons only involve reading an xlsx file and then iterating over the rows. If your job is to produce an xlsx report, this library is the wrong tool and no feature flag changes that.

The second limitation is the one the README volunteers: files need to be simple enough. The documentation does not enumerate what makes a file complex, so there is no checklist to run against your inputs. That is a genuine gap. A workbook with merged cells, unusual date encodings or formulas that depend on external references may or may not land inside the supported set, and the only way to know is to open the actual file. The README does not document rollback or recovery behaviour for partially read workbooks either.

Memory behaviour is the third constraint. For xls and ods, the README states that all sheets are loaded at once when the workbook is opened with default settings. A large legacy xls file therefore occupies memory in proportion to the whole workbook, and the header row setting you apply afterwards cannot reduce that. For xlsx and xlsb the lazy loading avoids this, so format choice has a direct operational consequence.

Finally, the toolchain floor is Rust 1.88, set in Cargo.toml with the comment that it is needed for zip. Projects pinned to an older compiler cannot use this version without upgrading.

None of these are defects in the library. They are the boundary of what a reader can promise, and the README draws that boundary rather than hiding it.

calamine compared with openpyxl and excelize

The README's performance section names three comparison libraries from three other languages: openpyxl in Python, excelize in Go, and ClosedXML in C#. The stated method is to read an xlsx file and iterate over the rows, using a 186MB xlsx file produced by converting a public dataset. The README describes the benchmark setup but the excerpt here does not include the resulting numbers, so no throughput claim can be repeated from it.

The real difference is not speed, it is integration. openpyxl gives a Python process a spreadsheet reader, which is the right answer when the surrounding work is pandas and notebooks. excelize is a Go library with both read and write support, so a Go service that must emit xlsx files has a reason to stay in one ecosystem. calamine is read-only and pure Rust, which means no C library, no Python runtime and no subprocess boundary in a Rust service. If your pipeline is already Rust and the spreadsheets are inputs rather than outputs, that removes a whole class of deployment problems. If you need to write files, calamine is not a candidate and the comparison ends there.

The choice between them is therefore mostly about where the rest of the processing lives, not about which library is better in the abstract.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-02. Releases are frequent enough to be worth tracking: v0.36.1 on 2026-07-27, v0.36.0 on 2026-07-06, and v0.35.0 on 2026-05-10. A Changelog.md sits at the top level, so version-to-version changes are recorded in the repository rather than only in release notes.

The dependency set is moderate and mostly low-level: log, serde, codepage, atoi_simd, byteorder, encoding_rs, fast-float2, zip and quick-xml, with chrono optional. The zip dependency is the reason for the Rust 1.88 floor, and it is pinned with default-features disabled and only the deflate feature enabled, which keeps the tree smaller than a default zip inclusion would. Upgrading calamine therefore mostly means tracking quick-xml and zip, the two dependencies most likely to move.

The licence is MIT, declared in Cargo.toml and present as LICENSE-MIT.md at the repository root. MIT is permissive and imposes no source disclosure obligation on your own code. That is a statement about the licence text, not legal advice; if you redistribute calamine in a product, your own counsel should confirm the attribution requirements you take on.

The project publishes to crates.io and documents at docs.rs, and the README points to the examples directory for material beyond the documentation.

Editorial conclusion

Adopt calamine if you need to read xlsx, xlsb, xls or ods files inside a Rust service and you want typed rows through Serde rather than hand-written cell matching. Do not adopt it if you need to write spreadsheets, or if your inputs are heavily formatted workbooks where the README's own hedge about files being simple enough applies. Before committing, verify that the concrete files you will receive open with open_workbook_auto, that the sheet names you depend on exist, and that the header row offset you pass to with_header_row matches the real layout, because for xls and ods the header setting is applied after the whole workbook has already been loaded.

Frequently asked questions

How do I install calamine in a Rust project?

Add it with cargo add calamine, which writes the dependency into your Cargo.toml. Optional features are off by default and can be enabled in the same command, for example cargo add calamine -F chrono.

Can calamine write or modify spreadsheet files?

No. The README describes calamine as readonly, and its performance section states that the comparisons only involve reading an xlsx file and iterating over the rows.

Which spreadsheet formats does calamine support?

It reads the Excel family (xls, xlsx, xlsm, xlsb, xla, xlam) and OpenDocument spreadsheets (ods). open_workbook_auto infers the format when it is not known at compile time.

Does calamine read VBA modules and formulas?

The README documents vba_project with get_module and get_references, worksheet_formula for per-sheet formulas, and defined_names for name and formula pairs. The example checks r.is_missing() to report a reference that is broken or not accessible.

What is the minimum Rust version for calamine?

Cargo.toml sets rust-version to 1.88, with a comment that this is needed for zip. Projects on an older compiler cannot use this version.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. tafia/calamine on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tafia-calamine.svg)](https://hysenlabs.com/projects/tafia-calamine)