Python reader and writer for .ods files (OpenDocument Spreadsheet — LibreOffice /
OpenOffice Calc), with a numpy-inspired indexing API:
from odsslicer import ODSReader
table = ODSReader("workbook.ods")
sheet = table.sheet("Sheet1")
sheet["A1"].value # typed: str / float / bool / date / datetime / time / timedelta / None
sheet["A1:B3"].to_numpy() # any block as a numpy array
sheet[:, 0] # entire column A
sheet["C1"].formula = "SUM(A1:A10)"
sheet["C1"].style.bold = True
table.save("out.ods") # add recalculate=True to have LibreOffice compute formulas/pivotsodsslicer works directly on the ODF XML (via BeautifulSoup/lxml), so it preserves what
other tools tend to drop — cell formats (currency, percentage, date, time), formulas, merged
and repeated cells, styles, comments — and writes them back faithfully. It has no calculation
engine of its own: like ODF itself, it describes what to compute — and can hand the file to a
local LibreOffice (save(..., recalculate=True)) to compute formulas and pivot tables for you.
Full documentation with an example for every feature: DOCS.md.
pip install odsslicerRequires Python ≥ 3.10. Dependencies (beautifulsoup4, lxml, numpy) are installed
automatically. For local development:
git clone https://github.com/antnardo/odsslicer.git
cd odsslicer
pip install -e ".[test]"Each line links to the detailed section (with examples) in DOCS.md.
| Area | Feature | Example |
|---|---|---|
| Reading | numpy-style indexing — addresses, slices, out-of-range returns empty cells | sheet["A1"], sheet[0, 0], sheet["A1:B3"], sheet[:, 0] |
| Typed cells — value, displayed text, format, address | cell.value, cell.text, cell.format |
|
Arrays — to_list(), to_numpy(), numpy-like shapes |
sheet["A1:B3"].to_numpy() |
|
| Writing | Values of every ODF type, then save() |
sheet["A1"].value = 42.5 |
| Ranges at once — broadcast or element-wise | sheet["A1:C1"].value = [1, 2, 3] |
|
| Auto-unrolling of repeated/merged cells on first write | transparent | |
| Auto-growth when writing past the sheet's extent | sheet["Z100"].value = 1 |
|
| Displayed text inferred from the document's own formats | "50,00 %", "05/01/30" |
|
| Files & sheets | New file from scratch | ODSReader.new() |
| Add / rename / reorder / delete sheets — renaming fixes cross-sheet formulas | table.rename_sheet("Sheet1", "Q4") |
|
| Structure | Insert rows/columns — formula references and merges follow | sheet.insert_rows(2, 5) |
| Delete rows/columns — formula references follow, batchable | sheet.delete_rows([3, 7, 20]) |
|
| Copy cells/ranges — value + formula + style, overlap-safe | sheet.copy("A1:B2", "D5") |
|
Sort a range — stable, None last, formulas follow their row |
sheet.sort("A2:C10", by=1) |
|
| Merge / unmerge + read merge state | sheet.merge("A1:C2"), cell.merge_range |
|
| Formulas | Write in ordinary syntax, translated to ODF's | cell.formula = "IF(A1>0,1,-1)" |
| Read back in ordinary syntax | cell.formula_friendly |
|
| Fill across a range like a fill handle | cell.fill_formula("B3:B10") |
|
{r}/{c} templates for per-cell patterns |
sheet["A2:A10"].formula = "$A{r-1}+1" |
|
| Pivot tables — definition written, computed by the spreadsheet | sheet.create_pivot_table(...) |
|
Recalculate with LibreOffice — formulas + pivot refresh, headless, no UNO needed; from the shell with soffice --convert-to |
table.save("out.ods", recalculate=True) |
|
| Styles | Read and write cell styles — font, colors, alignment, borders, rotation, wrap… | cell.style.bold = True |
| Copy a style in one shot | b.style = a.style |
|
| Number formats — read, assign, or create from scratch | NumberFormat.create(table, "currency", ...) |
|
| Conditional formats (e.g. negatives in red) | fmt.add_condition("value()<0", red) |
|
| Row / column / sheet styles — height, width, visibility, tab color | sheet.column_style(0).width = "5cm" |
|
| Annotations | Comments — text, author, date, visibility | cell.comment = "Check this" |
| Hyperlinks | cell.hyperlink = "https://…" |
|
| Document | Properties — title, author, keywords, typed custom properties | table.properties.title = "Q4" |
See Known limitations for what's deliberately out of scope (no calculation engine, no charts/images, no partial rich text…).
| Package | Read/Write | Latest release | Notes |
|---|---|---|---|
odfpy |
Low-level R/W | 1.4.1 — Jan 2020 | Dormant, but still the brick pandas uses internally |
odfdo |
Full R/W | 3.24 — Aug 2026 | Actively maintained modern fork of odfpy; generic DOM-like API for all ODF document types |
pyexcel-ods3 |
R/W | 0.6.1 — Jan 2022 | Inactive; values only, no styles/formulas |
ezodf |
R/W | 0.3.2 — Dec 2015 | Abandoned |
pandas (engine="odf") |
Read (via odfpy) | follows pandas | Convenient for data frames; loses formulas and fine-grained formats |
python-calamine |
Read-only (Rust), fast | 0.8 — Jul 2026 | The best option for fast pure reading |
pandas-ods-reader |
Read-only → DataFrame | 1.0.2 — May 2025 | Maintained, limited scope |
(Versions and dates as of August 2026.)
Where odsslicer sits: a spreadsheet-shaped API (sheet["A1:B3"], numpy arrays, fill
handles, copy/sort/merge) rather than a generic ODF DOM, with read and write of the
things data-oriented tools usually lose — formats, formulas (in ordinary syntax), styles,
merged cells, comments, pivot definitions — and every write verified against a real
LibreOffice. If you only need to read values fast, use python-calamine; if you need to
manipulate arbitrary ODF documents (text, presentations) at the XML level, use odfdo.
pip install -e ".[test]"
pytestThe main suite (tests/test_odsslicer.py) covers every feature above plus regression tests
for the bugs fixed along the way; it runs on every push/PR via
GitHub Actions across Python 3.10 to 3.13.
tests/test_wild_files.py confronts the API with real-world files written by other
generators — Excel 16, a 2012-era LibreOffice 3.5, recent LibreOffice on Linux and Windows,
a Google Sheets export (see tests/wild/README.md
for provenance) — reading, writing and round-tripping each one.
tests/test_libreoffice_consistency.py is an opt-in suite that hands files odsslicer wrote
to a real, local LibreOffice (soffice --headless --convert-to fods) and inspects what
LibreOffice itself made of them — the strongest available signal that a write is genuinely
valid ODF, not just something our own reader happens to parse back. It skips automatically if
no soffice/libreoffice binary is on PATH.
Version numbers are derived automatically from git tags (via setuptools-scm) and follow
Semantic Versioning — while the major version stays 0, the API can
still change between minor versions. See
CHANGELOG.md (and the
Releases page) for what changed in each
version.
MIT — reuse with essentially no restriction, just keep the copyright notice.
odsslicer, to reflect the module's real differentiator — numpy-style indexing/slicing by
cell address — rather than a generic "ods reader".