Skip to main content

betteroffice-xlsx

Read, recalculate, render, and write XLSX workbooks from Python. Where openpyxl hands back a formula's source text or whatever value the authoring application happened to cache, this evaluates the formula and can rasterize the sheet: the Rust BetterOffice XLSX core is compiled into the wheel — no Excel, no LibreOffice subprocess, no COM.

pip install betteroffice-xlsx

The distribution is hyphenated, the module is not: import betteroffice_xlsx.

Formulas actually calculate

from betteroffice_xlsx import Workbook

wb = Workbook.open_path("budget.xlsx")

sheet = wb["Sheet1"]
sheet["B1"] = 10
sheet["B2"] = 32
sheet["B3"] = "=SUM(B1:B2)"

print(sheet["B3"])            # 42.0   <- computed here, not read from a cache
print(sheet.formula("B3"))    # 'SUM(B1:B2)'

wb.save_path("budget-out.xlsx")

Value and formula are separate accessors on purpose: sheet["B3"] is the value, sheet.formula("B3") is the source text. Writing a cell recalculates its dependents, so the value above is computed here rather than read back from the file.

Workbook.open starts from cached values. Edits recalculate dependent formulas; open_recalculated and recalculate() request a full recalculation.

Render a sheet to PNG

png = wb.render_png("Sheet1", scale=2.0, range="A1:H40")
png.write("preview.png")
print(png.width, png.height)

Rendering is the same grid layout and display list the browser editor uses, so server-side output matches what the web canvas paints.

Opening, recalculating, rendering, and saving release the GIL, so they run in parallel across threads instead of serializing your workers.

Collaboration

Every workbook opened with open_collaborative is a Yrs replica. The binding exposes the byte-level primitives rather than a transport, so it drops into a WebSocket server, a queue, or a test harness without committing you to asyncio:

data = wb.save()
left = Workbook.open_collaborative(data)
right = Workbook.open_collaborative(data)

left["Sheet1"]["B3"] = 1000
right.apply_update(left.diff(right.state_vector()))   # right now agrees

joiner = Workbook.open_collaborative(data)
joiner.apply_update(left.state_as_update())           # catch up from nothing

The binding generates a client ID when it is omitted and exposes the chosen ID through the read-only client_id property. A server may pass a deterministic client_id explicitly, but it must be unique among connected peers because Yrs cannot detect duplicates once two replicas have started authoring. Collaboration byte inputs accept bytes, bytearray, and memoryview.

Undo, redo, and batches

wb.set_many("Sheet1", {"H1": 10, "H2": 20, "H3": "=H1+H2"})   # one undo step
wb.undo()
wb.redo()
wb.history()          # History(undo_depth=1, redo_depth=0)

Undo covers this replica's own edits. Updates applied from a peer are not in local history, so undo will not revert someone else's work.

Agent proposals

An agent can stage edits for a human instead of applying them. Each proposed edit carries the display text a reviewer would compare — before and after are what the cell shows, as strings, while input is what would be written:

proposal = wb.propose("copilot", [("Sheet1", "H1", "=B3*2")], note="double the total")

for edit in proposal.edits:
    print(edit.address, repr(edit.before), "->", repr(edit.after))   # H1 '' -> '84'

wb.accept_proposal(proposal.id)     # or wb.reject_proposal(proposal.id)

In standalone workbooks, accepting a proposal records one undo step. proposals() lists pending proposals.

A proposal goes stale when one of its target cells changes after it was staged. accept_proposal then raises StaleProposalError, whose cells names the addresses that moved underneath it — re-propose against the new values, or apply it anyway with force=True:

from betteroffice_xlsx import StaleProposalError

try:
    wb.accept_proposal(proposal.id)
except StaleProposalError as stale:
    print("changed underneath:", stale.cells)   # ['H1']
    wb.accept_proposal(proposal.id, force=True)

A changed formula dependency can also make acceptance stale. In that case, read proposals() again to review the refreshed preview before accepting. Pending proposals remain local to this workbook session; ordinary peer edits preserve them and acceptance still checks their targets.

An unknown proposal ID raises KeyError; reject_proposal returns False instead when there is nothing left to reject.

Version-checked edit batches

read_cells returns values, formulas and display text with the workbook version they were read at. apply_edits applies a batch against that version as one recalculated undo step, or returns a refusal with nothing changed. Requests and results are the camelCase dictionaries every binding shares, typed in betteroffice_xlsx.edits:

b3 = {"sheetId": "sheet:0", "range": {"kind": "a1", "a1": "B3"}}
read = wb.read_cells({"ranges": [b3]})
result = wb.apply_edits({
    "expectVersion": read["version"],
    "calculation": {"nowSerial": 45658.5},
    "steps": [
        {"op": "setCellInputs", "target": b3, "inputs": [["120"]],
         "expect": {"cells": [[{"displayText": read["ranges"][0]["cells"][0][0]["displayText"]}]]}},
        {"op": "patchStyle", "target": b3, "patch": {"bold": True}},
    ],
})
if not result["ok"]:
    print(result["failure"]["code"])   # "stale-version", "content-mismatch", ...

Steps set inputs (parsed like set), formulas (source without =), number formats and styles. validate_edits stages a batch without changing anything, find_text searches display text exactly, and history: "none" keeps a batch out of undo. A malformed request raises ValueError.

Structured export

export_structured returns sparse cells, sheet metadata and diagnostics with the version they were read at; export_markdown renders the same read as bounded Markdown grids with <!-- xlsx-export:N --> markers. Neither recalculates: formula results are the stored values.

result = wb.export_structured(scope=[{"sheet": 0, "range": "A1:D20"}])
if result["ok"]:
    for cell in result["content"]["sheets"][0]["cells"]:
        print(cell["anchor"]["a1"], cell["value"], cell["formula"], cell["displayText"])

from betteroffice_xlsx import export_xlsx_markdown
print(export_xlsx_markdown(data, markdown_options={"maxRows": 50})["markdown"])

Anchors carry sheet: {"sheetId", "index", "name"}: a cell or range anchor's {"sheetId": anchor["sheet"]["sheetId"], "range": {"kind": "a1", "a1": anchor["a1"]}} is its apply_edits target at the exported version (sheet:{index} ids for bytes). Hidden sheets, rows, columns and names are excluded unless asked for (include_hidden_sheets=True, ...). Comments, rich-text runs, charts and pictures are diagnosed or exported as placeholders. max_cells and max_bytes stop at a complete record with truncated. Unusable scopes come back as {"ok": False, ...}; malformed options raise ValueError.

Formatting

wb.set_number_format("Sheet1", "B3:B10", "#,##0.00")
wb.set_style("Sheet1", "A1:D1", bold=True, fill_color="#eeeeee",
             horizontal_alignment="center")

set_number_format takes automatic, text, number, percent, scientific, currency, date, time, or a custom pattern.

Reading without recalculating

Workbook.open keeps whatever values the file already carried. Use open_recalculated to evaluate everything up front, or call recalculate() later:

wb = Workbook.open_recalculated(open("report.xlsx", "rb").read())

summary = wb.recalculate()
print(summary.changed, summary.cycles)

Compared with openpyxl

openpyxl betteroffice-xlsx
Read cell values yes yes
Evaluate formulas no — returns the formula string, or a stale cached value yes
Render to an image no yes, PNG
Engine pure Python Rust, compiled

Use betteroffice-xlsx for formula evaluation, PNG rendering and collaborative editing.

API

Workbook.open(data) open from bytes
Workbook.open_path(path) open from a path
Workbook.open_recalculated(data) open and evaluate every formula
Workbook.open_collaborative(data) open a Yrs replica — on the class, like the other openers
wb.recalculate() re-evaluate; returns a Calculation summary
wb.last_calculation() the Calculation the most recent one produced
wb.sheet_names / wb.sheet_count sheet metadata
wb[key] / wb.sheet(key) a Sheet by name or index
wb.sheet_index(key) resolve a name or index to an index
sheet[addr] cell value — see the note below on when it is recalculated
sheet[addr] = value set from what a user would type
sheet.formula(addr) source formula, or None
wb.value(sheet, addr) / wb.formula(sheet, addr) the same two reads without a Sheet
wb.set(sheet, addr, value) / wb.set_many(sheet, edits) write one cell, or many as one undo step
wb.merged_ranges(sheet, range) merged regions overlapping a range
wb.undo() / wb.redo() walk local history
wb.can_undo / wb.can_redo / wb.history() what history is available
wb.propose(...) / proposals() / accept_proposal / reject_proposal staged agent edits
wb.version() / read_cells(...) / find_text(...) versioned reads
wb.validate_edits(...) / apply_edits(...) version-checked edit batches
wb.export_structured(...) / export_markdown(...) anchored JSON and Markdown export, never recalculated
export_xlsx_structured(data) / export_xlsx_markdown(data) / render_xlsx_markdown(content) the same from bytes or content
wb.set_style(...) / set_number_format(...) formatting over a range
wb.diff(sv) / apply_update(u) / state_vector() / state_as_update() exchange Yrs updates
wb.client_id / wb.is_collaborative which kind of workbook you are holding
wb.active_sheet / set_active_sheet(...) read or persist the active tab
wb.render_png(sheet, ...) render to PNG
wb.save() / wb.save_path(path) serialize to XLSX

The following calculation calls — open_recalculated, open_collaborative, recalculate, set, set_many, undo, redo, apply_update, propose, accept_proposal, set_number_format, and set_style — take a keyword-only now_serial. It is the clock TODAY() and NOW() read, as an Excel serial number. The engine has none of its own, so both return #VALUE! unless you pass one:

wb.recalculate(now_serial=45658.5)   # 2025-01-01, midday

Cell values come back as None, float, str, bool, or CellError. Numbers are f64 in the engine, so they arrive as float and are not narrowed to int. Errors are a CellError instance rather than a string, so #DIV/0! as a value is distinguishable from a cell containing that text. CellError compares equal to its code, and hashes like it, so it works as a dict key:

if sheet["D3"] == "#DIV/0!":
    ...

Writing accepts None (clears the cell), bool, int, float, Decimal, and str. date, datetime, time and timedelta raise TypeError; pass Excel serials as floats.

General cells interpret strings like Excel: a leading = is a formula, TRUE/FALSE become booleans, and numeric text becomes a number. Text (@) cells preserve input as text. Prefix with an apostrophe to force text in any format.

sheet["A1"] = "'=1+1"   # the text "=1+1"
sheet["A2"] = "=1+1"    # the formula, evaluating to 2.0

Mutating calls return a truthy-on-change Mutation. changed lists recalculated cells; cycles and limited identify calculation diagnostics. Addresses are A1 on the active sheet and Sheet!A1 elsewhere.

Errors raise XlsxError or a more specific subclass: ParseError, RangeError, RenderError, InvalidUpdateError, CollaborativeStateError, StaleProposalError, NotCollaborativeError. Invalid peer updates, broken local collaboration state, stale proposals, and collaboration-only operations are the last four in that order. StaleProposalError.cells lists the changed A1 addresses and targets their sheets and coordinates, and an unknown proposal ID raises KeyError.

Status

Pre-1.0: the API may change between minor versions.

save keeps the parts the model does not represent — charts, drawings, pivot tables, comments, macros, custom XML, and their relationships — rather than regenerating the package from the modeled features, and sheets you did not touch are copied through byte for byte. The stylesheet is left alone unless styles actually change.

Saving serializes edited worksheet state and retains source range coordinates. Collaboration fingerprints the modeled workbook.

Wheels are built for Linux (x86_64, aarch64), macOS (arm64, x86_64), and Windows (x86_64) against the stable ABI for CPython 3.9 and up.

The extension embeds Carlito, a Calibri-metric-compatible face used to measure and draw cell text, under the SIL Open Font License. Its license travels with the wheel — see THIRD-PARTY-NOTICES.md and licenses/Carlito-OFL.txt. The package's own code is Apache-2.0.

Apache-2.0.

Metadata

Release files for betteroffice-xlsx 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for betteroffice-xlsx 0.2.0
File Size Uploaded
betteroffice_xlsx-0.2.0.tar.gz 1.3 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for betteroffice-xlsx 0.2.0
File
betteroffice_xlsx-0.2.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details
betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details
betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details
betteroffice_xlsx-0.2.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
betteroffice_xlsx-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 18.4 MB

Release files / betteroffice_xlsx-0.2.0.tar.gz

Download URL betteroffice_xlsx-0.2.0.tar.gz
Size 1.3 MB
Tags Source
SHA-256 checksum
How to use checksums
c202e9f1ed8b88c7baedbc7eb0148461420d33187f4f8e884f94b7805a0602c6
BLAKE2b-256 checksum
How to use checksums
398c2e9c6b2511720a9fb07e15a632cb83d604323461813bd3f312961463bb7f
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 28, 2026.

Transparency log

Release files / betteroffice_xlsx-0.2.0-cp39-abi3-win_amd64.whl

Download URL betteroffice_xlsx-0.2.0-cp39-abi3-win_amd64.whl
Size 3.6 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
d9ce96e375d8bfadb3e43f3e7feabefe1bf67c949db0cd8994f7a344960810c5
BLAKE2b-256 checksum
How to use checksums
a96bbe4495025132a91a3b419ce5f19d51a0fe7a2655898d5b2e1b900b1c1041
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 28, 2026.

Transparency log

Release files / betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 3.5 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
8cb1a3740923c38cbbc82cbbb565317933bb0a72f976fe1cc1670750018b6c9f
BLAKE2b-256 checksum
How to use checksums
7505fb2e3f558035c0a285a21382bf0d767a72b14c2725ded72039e67bfc77f2
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 28, 2026.

Transparency log

Release files / betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL betteroffice_xlsx-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 3.3 MB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
c88925ba3983439fbfc2bd674932325170ea7519b74a01f47519f7fc6776e16f
BLAKE2b-256 checksum
How to use checksums
535cc24260ed351d210ecf56a02ce13caacbd3b8b3373ced4302b810a0aba7ab
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 28, 2026.

Transparency log

Release files / betteroffice_xlsx-0.2.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL betteroffice_xlsx-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
Size 3.2 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
27ddf27d230b4131019a252968448cf1c376c4df5fcef7d50cb392d56a9186d5
BLAKE2b-256 checksum
How to use checksums
87b0eb0fc61471b6ec66526f9e77ab6daf97cad9feabb2ab709770ead184ca35
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 28, 2026.

Transparency log

Release files / betteroffice_xlsx-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL betteroffice_xlsx-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 3.5 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
dfb29b28f33a8025c4237c18df6e56b326aa8d522314df4a40f6c54e1d799839
BLAKE2b-256 checksum
How to use checksums
cd1d88cbcca1a6d2e3a40716c7800b56329cdc0fea56a7bf9ed1b03546f1eb4c
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 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

6 release files

0.1.0

6 release files

0.0.2

6 release files

0.0.1

6 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page