v0.3.0
0.3.0 (2026-07-10)
Summary
Stability and correctness release. 27 silent data-loss/corruption bugs fixed, plus new features and hardening. All fixes validated by round-trip inspection, official ECMA-376 schema conformance, and property-based fuzzing (0 failures across 55,000+ writes/checks).
Features
- Direct DataFrame input — Arrow-consuming functions now accept any object implementing the Arrow PyCapsule interface (
__arrow_c_stream__), so bare Polars/pandas DataFrames work without.to_arrow(). Handles PolarsUtf8View/BinaryViewC-stream exports. Fully backward compatible; byte-identical values vs.to_arrow(). num_threadson single-sheet writers —write_sheet_arrow/write_sheet_arrow_to_bytesaccept"auto"(default; uses all cores, prints chosen count) or an explicit int (values <1 clamp to serial). Performance-only knob: every value produces byte-identical output. Invalid values raise cleanly.- Categorical/dictionary column support — pandas/Polars
Categorical/Enum(ArrowDictionary) columns are now decoded and written; previously rejected outright. Non-categorical workloads pay nothing (fast-path early return). - Null-type column support — all-
Nonecolumns (ArrowNull) now write as blank cells instead of raising. - Clear column-reference errors — unmatched
column_formats/column_widths/hidden_columnsnames hard-error and list available columns as a copyable Python literal. Case-sensitive exact match.
Performance enhancement
- Parallel single-sheet row generation — large single sheets (>50k rows, no table headers, multi-core) now generate row XML across rayon workers. Byte-identical to the serial path by construction. No regression on the serial path (
#[inline(always)]per-row writer).
Bug Fixes
Styles & formatting
- Cell borders never rendered (
borderIdhardcoded to 0). - Out-of-range
text_rotationcorrupted the workbook; now mapped/clamped to valid encoding. - Invalid color strings (
"red","#FF0000","F00") broke the entire stylesheet; now normalized to ARGB or omitted (fonts, fills, color scales, data bars).
Values & types uint64abovei64::MAXoverflowed to negative; now routed through the exact f64 path.- Formulas with a leading
=doubled to==; now stripped. - Excel-1900 date serials off by one on/before 1900-02-28 (phantom 1900-02-29); now matches Excel's canonical serials.
- Pre-1900 dates produced invalid negative serials; now written as ISO-8601 strings (1900+ stay date-typed).
- NaN/±Inf in the dict path emitted invalid
<v>NaN</v>/<v>inf</v>; now empty cells (matching the Arrow path).
Ranges & geometry - Reversed ranges (end before start) silently discarded CF/merge/data-validation rules; now normalized to valid A1.
- No enforcement of Excel's grid limits; sheets past 1,048,576 rows / 16,384 cols now rejected with a catchable error on all paths.
- Hyperlink on row 0 corrupted the sheet; row clamped to ≥1.
- Hyperlink inside a merged range corrupted the workbook; ref relocated to the merge anchor.
Tables & charts - CRITICAL — table with out-of-range
end_colpanicked and (underpanic="abort") crashed the whole Python process; now clamped/guarded on all four paths. - Tables with spaced/invalid names produced unopenable files; names sanitized.
- Duplicate table names corrupted the workbook; unique id appended to
name/displayName. legend_positiondocumented but ignored; now parsed.- Scatter-chart value-axis element order violated ECMA-376 (
majorGridlinesnow precedestitle). - Chart title/axis colors written raw; now normalized.
title_font_size=0(invalid) clamped to the valid 100–400000 range.