oxlsx
A high-performance Rust library for reading and writing .xlsx files with full formatting support — designed as an openpyxl equivalent in Rust, with Python bindings (PyO3).
5–10x faster than openpyxl on write operations. Streaming reads via
open_readonly().
Status
v0.2.0 — phases 1–14 plus temporal values, style API additions, and Python type stubs. See PyPI and crates.io for published packages.
| Phase | Scope | Status |
|---|---|---|
| 1 | ZIP parsing, styles, cell.font() / cell.fill() |
✅ Done |
| 2 | Shared strings, Value::String/Bool, cell.text() |
✅ Done |
| 3 | Multi-sheet, Value::Date, openpyxl API parity |
✅ Done |
| 4 | Write support — Workbook::new() + wb.save() |
✅ Done |
| 5 | Lazy / streaming reader (open_readonly()) |
✅ Done |
| 6 | PyO3 Python bindings | ✅ Done |
| 7 | Read-modify-write (open + edit + save) | ✅ Done |
| 8 | Extended write features | ✅ Done |
| 9 | Worksheet iteration API | ✅ Done |
| 10 | Bulk write (append, insert/delete rows/cols, move_range) |
✅ Done |
| 11 | Rich formatting | ✅ Done |
| 12 | Hyperlinks + plain-text comments | ✅ Done |
| 13 | Freeze panes, print settings, named ranges, tables (lossless RMW) | ✅ Done |
| 14 | Distribution — abi3 wheels, PyPI + crates.io, release CI | ✅ Done |
| 15 | Pandas engine / tabular adapter | 🔲 Planned |
openpyxl-migration additions (v0.1.1): conditional formatting (FormulaRule / CellIsRule), sheet auto_filter, PatternFill, get_column_letter / column_index_from_string, and dataframe_to_rows.
New in v0.2.0
- Typed datetime, time, and duration values, including Python
datetime,time, andtimedeltasupport. - Strikethrough fonts, theme-color tint preservation, fill-color aliases, and cell row/column accessors.
- Python type stubs for editor autocomplete and static analysis.
- Atomic workbook saves with symbolic-link destination rejection.
Rust migration from v0.1.1: this release includes source-breaking API changes:
- Replace
Color::Theme(index)withColor::Theme(index, None)(orSome(tint)); update matching patterns to accept the second field. - Include the new
Font.strikefield in complete struct literals, or use..Default::default(). - Handle
Value::DateTime,Value::Time, andValue::Durationin exhaustive matches.
The pandas engine remains planned; pandas Styler support is not included.
Install
Rust
# Cargo.toml
[dependencies]
oxlsx = "0.2.0"
Or pin to a tag from git:
oxlsx = { git = "https://github.com/alvaroroco/oxlsx", tag = "v0.2.0" }
Python (build from source via maturin)
Prebuilt abi3 wheels (a single cp38-abi3 wheel per platform, runs on CPython 3.8+) are published to PyPI on each release:
pip install oxlsx
To build from source, use maturin. The wheel build (abi3 extension-module) is configured in pyproject.toml, so no extra flags are needed:
pip install maturin
maturin develop --release # install into the active venv
# or: maturin build --release -> target/wheels/oxlsx-0.2.0-cp38-abi3-*.whl
Contributors running the in-crate PyO3 tests use the interpreter-linked path instead:
cargo test --features python.
Usage
Reading
use oxlsx::{Workbook, Value};
fn main() -> Result<(), oxlsx::OxlsxError> {
let wb = Workbook::open("file.xlsx")?;
// Access by index or name
let ws = wb.sheet(0).unwrap();
let ws = wb.sheet_by_name("Employees").unwrap();
println!("Sheets: {:?}", wb.sheetnames());
println!("Active: {}", wb.active().unwrap().title());
if let Some(cell) = ws.cell("A1") {
match cell.value() {
Value::String(s) => println!("text: {s}"),
Value::Number(n) => println!("number: {n}"),
Value::Bool(b) => println!("bool: {b}"),
Value::Date(d) => println!("date: {d}"),
Value::DateTime(d) => println!("datetime: {d}"),
Value::Time(t) => println!("time: {t}"),
Value::Duration(d) => println!("duration: {d}"),
Value::Formula(f) => println!("formula: {f}"),
Value::Empty => println!("empty"),
}
if let Some(font) = cell.font() {
println!("bold: {}", font.bold);
}
if let Some(fill) = cell.fill() {
println!("fill: {}", fill.pattern_type);
}
}
Ok(())
}
Writing
use oxlsx::{Color, Fill, Font, Workbook};
fn main() -> Result<(), oxlsx::OxlsxError> {
let mut wb = Workbook::new();
let ws = wb.active_mut().unwrap();
ws.set_cell("A1", "Hello");
ws.set_cell("B1", 42.0_f64);
ws.set_cell("C1", true);
ws.set_cell_font("A1", Font { bold: true, ..Default::default() });
ws.set_cell_fill("A1", Fill {
pattern_type: "solid".to_string(),
fg_color: Some(Color::Argb("FFFF00".to_string())),
bg_color: None,
});
wb.create_sheet("Data").set_cell("A1", "Sheet 2");
wb.save("output.xlsx")?;
Ok(())
}
API (openpyxl parity)
| openpyxl (Python) | oxlsx (Rust) |
|---|---|
load_workbook("f.xlsx") |
Workbook::open("f.xlsx") |
Workbook() |
Workbook::new() |
wb.worksheets |
wb.worksheets() |
wb.sheetnames |
wb.sheetnames() |
wb.active |
wb.active() / wb.active_mut() |
wb["Sheet1"] |
wb.sheet_by_name("Sheet1") |
ws.title |
ws.title() |
ws["A1"] |
ws.cell("A1") |
ws["A1"] = value |
ws.set_cell("A1", value) |
cell.value → str/int/float/date |
cell.value() → &Value |
cell.font.bold |
cell.font()?.bold |
cell.fill.fgColor |
cell.fill()?.fg_color |
cell.comment |
cell.comment() / ws.set_cell_comment(...) |
cell.hyperlink |
cell.hyperlink() / ws.set_cell_hyperlink(...) |
ws.freeze_panes |
ws.freeze_panes() / ws.set_freeze_panes(...) |
ws.print_area |
ws.print_area() / ws.set_print_area(...) |
ws.print_title_rows / print_title_cols |
ws.print_title_rows() / ws.print_title_cols() (+ setters) |
wb.defined_names |
wb.defined_names() (global) / ws.defined_names() (sheet-local) |
ws.add_table(Table(...)) |
ws.add_table(Table::new(...)) |
ws.tables / del ws.tables["T"] |
ws.tables() / ws.remove_table("T") |
ws.append([...]) / ws.append({...}) |
ws.append([...]) (Python also accepts a dict: column letters or 1-based ints) |
ws.insert_rows/delete_rows/insert_cols/delete_cols(idx, amount) |
same names + signatures (1-based idx, amount=1) |
ws.move_range(range, rows, cols, translate) |
ws.move_range(range, rows, cols, translate) (translate=True → NotImplementedError) |
ws.auto_filter.ref = "A1:D10" |
ws.auto_filter().ref / Python ws.auto_filter.ref = ... |
cell.fill = PatternFill(...) |
PatternFill(...) + ws.set_cell_fill(coord, fill) |
ws.conditional_formatting.add(range, FormulaRule(...)) |
same (Python); FormulaRule, CellIsRule |
get_column_letter(i) / column_index_from_string(s) |
oxlsx.get_column_letter(i) / oxlsx.column_index_from_string(s) |
dataframe_to_rows(df, index, header) |
oxlsx.dataframe_to_rows(df, index, header) |
wb.save("out.xlsx") |
wb.save("out.xlsx") |
Benchmarks
Measured against openpyxl 3.1.2 on a 10k-row file (5 columns, mixed types). Absolute numbers are machine-dependent; the ratios are what matter. Reproduce with cargo bench + python3 benches/openpyxl_comparison.py.
Write
| Operation | openpyxl | oxlsx | Speedup |
|---|---|---|---|
| Single cell | 1.90ms | 0.18ms | ~10.8x |
| 1k rows × 5 cols | 23.2ms | 3.68ms | ~6.3x |
| 10k rows × 5 cols (50k cells) | 222.4ms | 54.1ms | ~4.1x |
| 3 sheets × 1k rows | 35.9ms | 6.80ms | ~5.3x |
Read (10k rows, 5 cols)
| Operation | openpyxl | oxlsx | Speedup |
|---|---|---|---|
Metadata only (read_only / open_readonly()) |
1.47ms | 0.072ms | ~20x |
| Open + iterate all cells | 151ms (read_only) / 216ms (full) |
33.4ms | ~4.5–6.5x |
Eager open() metadata, 10k |
1.47ms | 28ms | tradeoff: eager loads the whole sheet for random access — use open_readonly() for streaming metadata |
For metadata or large-file streaming, use
open_readonly()(lazy, ~20x faster than openpyxl). Eageropen()loads the full sheet to enable randomws.cell("A1")access.
Run benchmarks yourself:
cargo bench # Rust (criterion)
python3 benches/openpyxl_comparison.py # Python baseline
Data Model
| Type | Description |
|---|---|
Workbook |
Entry point — open or create, access sheets |
Worksheet |
Access / set cells by Excel reference ("A1", "B3") |
Cell |
Holds Value + StyleId + shared Arc<StyleSheet> |
Value |
Empty | String(String) | Number(f64) | Bool(bool) | Date(NaiveDate) | DateTime(NaiveDateTime) | Time(NaiveTime) | Duration(chrono::Duration) | Formula(String) |
StyleSheet |
Registry mapping StyleId → Font + Fill |
StyleId |
Newtype (usize) — prevents index confusion at compile time |
Font |
Bold, italic, strikethrough, size, family, color |
Fill |
Pattern type, foreground/background color |
Color |
Argb(String) | Indexed(u32) | Theme(u32, Option<f64>) (theme index and optional tint) |
DefinedName |
Named range — name, value (+ comment, hidden); global or sheet-local |
Table |
Worksheet table — display_name, ref, columns, optional TableStyleInfo; lossless RMW for unmodeled features |
Dxf |
Differential format (fill / font) referenced by conditional-formatting rules |
ConditionalFormatting |
Per-sqref rules — FormulaRule (expression) / CellIsRule (operator); unmodeled rules (color scales, etc.) preserved verbatim on RMW |
Architecture
- SAX only —
quick-xmlevent loop, no DOM, no full-file loading - ZIP direct — reads from the container, no temp disk extraction
Arc<StyleSheet>— shared acrossWorkbook → Worksheet → Cell, no lifetime params- PyO3-ready — all public structs are
'staticcompatible - Newtype pattern —
StyleId(usize)prevents index confusion at compile time
Dependencies
| Crate | Role |
|---|---|
quick-xml |
SAX XML parser + writer |
zip |
OOXML container (deflate only, minimal wheel size) |
thiserror |
Typed error enum |
chrono |
Date resolution (NaiveDate) |
Roadmap
✅ Done
- ZIP container reading + SAX parsing
- Styles: fonts, fills, colors,
StyleIdnewtype - Shared strings →
Value::String -
Value::Bool,Value::Date(resolved at parse time) - Multi-sheet:
wb.sheetnames(),wb.sheet_by_name(),ws.title() - Write support:
Workbook::new(),ws.set_cell(),wb.save() - Style round-trip: bold font + solid fill survive write → open cycle
- Criterion benchmarks + openpyxl comparison script
- Streaming reader (
open_readonly()), PyO3 bindings, read-modify-write - Extended write: formulas, merged cells, column widths, date numFmt
- Iteration API (
iter_rows/iter_cols/rows/columns/values, bounds) - Bulk write (
append, insert/delete rows/cols,move_range) — Rust + Python; merges/dims shift, table-intersection guard, formulas verbatim - Rich formatting (
number_format,alignment,border,protection) - Hyperlinks + plain-text comments
- Freeze panes; print area / print titles
- Named ranges —
wb.defined_names()(global) +ws.defined_names()(sheet-local), conservative lossless passthrough - Tables —
ws.add_table(),ws.tables,TableStyleInfo; lossless RMW (unmodeled table features preserved verbatim when untouched) - Conditional formatting —
FormulaRule/CellIsRule+Dxf; RMW-preserving (unmodeled rules kept verbatim) - Sheet
auto_filter;PatternFill+ws.set_cell_fill - Utils —
get_column_letter,column_index_from_string,dataframe_to_rows - Distribution — abi3 wheels (CPython 3.8+), PyPI + crates.io, OIDC trusted publishing, release CI
🔲 Planned
- Phase 15 — Pandas engine / tabular adapter
engine="oxlsx"integration via a dedicated adapter- DataFrame ↔ XLSX mapping, headers/index handling, and type conversions
- Explicit adapter instead of monkeypatching by default
License
Licensed under either of:
at your option.
Metadata
Release files for oxlsx 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| oxlsx-0.2.0.tar.gz | 367.5 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| oxlsx-0.2.0-cp38-abi3-win_amd64.whl | CPython 3.8 | abi3 | Windows x86-64 | Details |
| oxlsx-0.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| oxlsx-0.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.8 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| oxlsx-0.2.0-cp38-abi3-macosx_11_0_arm64.whl | CPython 3.8 | abi3 | macOS 11.0+ ARM64 | Details |
| oxlsx-0.2.0-cp38-abi3-macosx_10_12_x86_64.whl | CPython 3.8 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 4.5 MB
Release files / oxlsx-0.2.0.tar.gz
| Download URL | oxlsx-0.2.0.tar.gz |
|---|---|
| Size | 367.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0aab2d79be09eec72ba05eaadb2d08d825f1c70756a56187c2ba4e2eb136b94f
|
|
BLAKE2b-256 checksum How to use checksums |
b94539e23ca11b5db5aaa3c99866b7c206feab073c799eb6a46d842d88c0f3bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / oxlsx-0.2.0-cp38-abi3-win_amd64.whl
| Download URL | oxlsx-0.2.0-cp38-abi3-win_amd64.whl |
|---|---|
| Size | 715.7 kB |
| Tags | CPython 3.8 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
faf9b4ce59135bf974bd06297d0eff1f1702a129c9841d60a69b0b89607d8074
|
|
BLAKE2b-256 checksum How to use checksums |
aaaffc9928a616590b1d276ccebfc5eaddc7564fce7f8a6912d057cba0b1c5fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / oxlsx-0.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | oxlsx-0.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 884.1 kB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
831b7487a452328f5a550d265421388e59014e76a77da016f11966df43e05856
|
|
BLAKE2b-256 checksum How to use checksums |
997da00320fe50bc1c71a0e5feeb118a57ae5843b13d49563e698ffebbe275d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / oxlsx-0.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | oxlsx-0.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 885.5 kB |
| Tags | CPython 3.8 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
f851e3592e25bb82fc185df5617050da427110a93ffb773763d7f5dd0a61a41b
|
|
BLAKE2b-256 checksum How to use checksums |
eedff3943c53439a7ebd1e6f067ade5ab4b825c8562cf334764dc6fb2f7586ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / oxlsx-0.2.0-cp38-abi3-macosx_11_0_arm64.whl
| Download URL | oxlsx-0.2.0-cp38-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 803.5 kB |
| Tags | CPython 3.8 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
981757d2732dcd5d3c99b7914ab0f9ce8f15b052c31232342ca28ebb83f2e924
|
|
BLAKE2b-256 checksum How to use checksums |
23c4281959c4070b964e0d06a74711148acf2e72e6417ad5a16d0ae58f956e8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency logRelease files / oxlsx-0.2.0-cp38-abi3-macosx_10_12_x86_64.whl
| Download URL | oxlsx-0.2.0-cp38-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 818.0 kB |
| Tags | CPython 3.8 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
01597bdb7ada731c542c1e2951b60f772fd7cc52b956ff6b993a18f804cf3e13
|
|
BLAKE2b-256 checksum How to use checksums |
d7e892d0df3014eef79cde955e9379a57ebf906ac658232f1bee7773bfac66ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 5, 2026.
Transparency log