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 the values the authoring application cached, and a cell you have not touched keeps that cached value — including if it was stale when the file was written. Use open_recalculated, or call recalculate(), when you need every formula evaluated by this engine rather than trusted from the file.

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:

left = Workbook.open_collaborative(data)
right = Workbook.open_collaborative(data)
print(left.client_id, right.client_id)

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)

Nothing is written until the proposal is accepted, and accepting applies it as a single undo step. proposals() lists the ones still awaiting a decision.

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)

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

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

openpyxl is a far broader library and covers plenty this does not. If you need formulas evaluated or a sheet rasterized, that is the gap this fills.

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.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

Every call that can trigger a calculation — open_recalculated, open_collaborative, recalculate, set, set_many, undo, redo, apply_update, propose, accept_proposal, set_number_format, and set_style — takes 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 for now: converting them needs the workbook's date system, which is not exposed yet, and stringifying them would write text that only looks like a date. Pass the Excel serial number as a float if you need a date today.

Strings are interpreted the way Excel interprets typed input: a leading = is a formula, TRUE/FALSE become booleans, and numeric text becomes a number. Prefix with an apostrophe to force text.

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

Mutating calls return a Mutation — truthy when something changed, with changed listing the cells the engine recalculated as a result, and cycles and limited the ones it gave up on. A cell you wrote directly is not itself a recalculation, so it will not always appear there. Addresses are bare A1 on the active sheet and Sheet!A1 anywhere else, so match on the suffix rather than the whole string.

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 an unknown proposal ID raises KeyError.

Status

0.0.x, and the API may change before 0.1.0.

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.

A sheet you do edit is reserialized from the model, so unmodeled row, column, and cell markup on that one sheet is lost, and its autofilter, data-validation, conditional-formatting, table, and sparkline ranges stay at their source coordinates. Collaborative sessions compare only the modeled workbook, so two peers holding the same cells but different macros or custom XML still accept each other as the same base.

This binding exposes no structural edits: no sheet rename or removal, no row or column insert or delete. The engine refuses those anyway while a pivot table or an unmodeled chart part is preserved, because it cannot rewrite the references they hold.

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.

Links

Apache-2.0.

Metadata

Release files for betteroffice-xlsx 0.0.2

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.0.2
File Size Uploaded
betteroffice_xlsx-0.0.2.tar.gz 808.9 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for betteroffice-xlsx 0.0.2
File
betteroffice_xlsx-0.0.2-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details
betteroffice_xlsx-0.0.2-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.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details
betteroffice_xlsx-0.0.2-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details
betteroffice_xlsx-0.0.2-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details

Total release size: 13.8 MB

Release files / betteroffice_xlsx-0.0.2.tar.gz

Download URL betteroffice_xlsx-0.0.2.tar.gz
Size 808.9 kB
Tags Source
SHA-256 checksum
How to use checksums
990a09e6ca498288f8fe3fc89046ac90e2e3f8070ff2bff092e781d4f3f2253a
BLAKE2b-256 checksum
How to use checksums
83f02db7a9b1feaee8647151a2145562a887a9bab636460defb3a4b77d609db8
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 Aug 19, 2026.

Transparency log

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

Download URL betteroffice_xlsx-0.0.2-cp39-abi3-win_amd64.whl
Size 2.7 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
cf6ce2d375ba1314624f9fd4355b1ed240de2dfdeb112e2091a2ac9033c3bef2
BLAKE2b-256 checksum
How to use checksums
f53bc6856b8ce549ff38e0d56d042dcd2f7a293fe51609c4af79f6ae019155cc
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 Aug 19, 2026.

Transparency log

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

Download URL betteroffice_xlsx-0.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.7 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
5fcc622ddd303635ca4cad78c122362a560026516fa753ee892fcc0b6326aed9
BLAKE2b-256 checksum
How to use checksums
85b8f772f666f7e8a0973fb2531bc49dabfb80a871921bfdf9b3f834b0a9f792
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 Aug 19, 2026.

Transparency log

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

Download URL betteroffice_xlsx-0.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.5 MB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
30b6868482684ce56c5e3fd30d6178aefa55e2118b5b779bd08d63874c7b4b70
BLAKE2b-256 checksum
How to use checksums
c3401ce83dff78830bbee5955bd78d81c3db912fea30aece5f868c2269118703
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 Aug 19, 2026.

Transparency log

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

Download URL betteroffice_xlsx-0.0.2-cp39-abi3-macosx_11_0_arm64.whl
Size 2.5 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
ecb55e59b45423258c8bd4b738005c793a81cdd2ef1a8c6b72ce3edd5d5299e1
BLAKE2b-256 checksum
How to use checksums
305c1564fc40e638085b3e96b0b63e9b3ddf310a56aff81f313c64fbcd359de2
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 Aug 19, 2026.

Transparency log

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

Download URL betteroffice_xlsx-0.0.2-cp39-abi3-macosx_10_12_x86_64.whl
Size 2.6 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
9ceb14bd56a94c205f8dda15ee2ad7d481af59770719e836638fffd9bebd187a
BLAKE2b-256 checksum
How to use checksums
f155c6251f97108e02e8384a24da51005fa1b8eedd0c6cb8212f066513c35f16
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 Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

6 release files

0.1.0

6 release files

This release

0.0.2 This release

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