formulon
Excel 365 calculation engine, exposed as a pure-Python binding driven by
WebAssembly. Evaluates formulas; loads and saves .xlsx workbooks; and
edits cells, the row/column matrix, styles, merges, comments,
hyperlinks, data validations, conditional formats, defined names, and
PivotTables. It also exposes recalc (full and partial), dependency-graph
tracing (precedents / dependents), dynamic-array spill info, the function
catalog, and per-sheet view / protection settings. The goal is 1-bit
compatibility with the default win-365-ja_JP behavior profile; callers can
select the separately supported mac-365-ja_JP profile when required.
Install
pip install formulon
Requires Python 3.9 or newer. The wheel is py3-none-any: it contains a
single formulon_capi.wasm (a standalone reactor-style WebAssembly
module exporting the engine's C ABI) plus a thin Python wrapper that
drives it through wasmtime. One
wheel works on every platform wasmtime supports: Linux x86_64 /
aarch64, macOS x86_64 / arm64, Windows x86_64.
wasmtime is the only runtime dependency, declared in the wheel's
metadata; pip resolves the right platform-specific wasmtime build at
install time.
Quick start
import formulon
# One-shot formula evaluation against a fresh workbook.
v = formulon.eval_formula("=SUM(1,2,3)")
print(v.to_python()) # 6.0
# Cell-level Excel errors surface as Value(kind=ValueKind.ERROR), not
# Python exceptions.
v = formulon.eval_formula("=1/0")
print(v.kind) # ValueKind.ERROR
print(v.error_code) # 1 (ErrorCode::Div0)
Workbook example
from formulon import Workbook
with Workbook.create_default() as wb:
wb.set_number(0, 0, 0, 21.0) # Sheet1!A1 = 21
wb.set_formula(0, 0, 1, "=A1*2") # Sheet1!B1 = =A1*2
wb.recalc()
print(wb.get_value(0, 0, 1).to_python()) # 42.0
# Serialise back to an in-memory .xlsx.
blob = wb.save()
with open("output.xlsx", "wb") as f:
f.write(blob)
# Load back from disk.
with open("output.xlsx", "rb") as f:
blob = f.read()
with Workbook.load(blob) as wb:
wb.recalc()
for cell in wb.iter_cells(0):
print(cell)
API surface
Workbook mirrors the C ABI surface that the npm bindings expose, minus
the exclusions listed under Not exposed. Every public
method carries a docstring (help(Workbook.set_number)), and the
signatures live in the hand-rolled type stubs in formulon/__init__.pyi.
The groups, with one example each:
Core -- create_default() / create_empty() / load(bytes) factories,
set_number/set_bool/set_text/set_blank/set_formula, get_value,
recalc(), save(), and the iter_cells/iter_defined_names/iter_tables/ iter_passthrough iterators.
Tables -- create, retarget, and drop xl/tables/tableN.xml entries:
wb.set_text(0, 0, 0, "Region")
wb.set_text(0, 0, 1, "Amount")
index = wb.table_create(0, "A1:B3", "Table1", "Table1", ["Region", "Amount"])
wb.table_update(index, "A1:B9") # retarget; None preserves style / flags
wb.table_remove(index)
Phonetic guides -- the OOXML <rPh> furigana attached to a cell string:
wb.set_text(0, 0, 0, "日本語")
wb.set_phonetic(0, 0, 0, "ニホンゴ")
wb.get_phonetic(0, 0, 0) # -> 'ニホンゴ'; '' when the cell has none
AutoFilter -- the raw <autoFilter> fragment, preserved verbatim so
filter criteria and extensions survive a round trip:
wb.set_auto_filter_xml(0, '<autoFilter ref="A1:B3"/>')
wb.get_auto_filter_xml(0)
wb.set_auto_filter_xml(0, "") # removes it
Sheets & matrix edits
wb.add_sheet("Data")
wb.rename_sheet(1, "Numbers")
wb.move_sheet(1, 0)
wb.set_number(0, 0, 0, 11.0) # A1 = 11
wb.insert_rows(0, 0, 1) # A1 shifts down to A2
wb.delete_cols(0, 5, 1) # delete column F
Defined names
wb.set_defined_name("TaxRate", "Sheet1!$A$1") # set / replace
wb.set_defined_name("TaxRate", "") # empty formula removes it
Partial recalc -- recompute only the closure feeding a viewport:
recomputed = wb.partial_recalc(sheet=0, first_row=0, last_row=0, first_col=0, last_col=1)
Merges / comments / hyperlinks / validations
from formulon import MergeRange, DataValidationInput
wb.add_merge(0, MergeRange(0, 0, 1, 1))
wb.set_comment(0, 0, 0, author="alice", text="see note")
wb.add_hyperlink(0, 0, 0, "https://example.com", "Example", "tooltip")
wb.add_validation(0, DataValidationInput(type=3, ranges=[MergeRange(0, 0, 4, 0)], formula1='"a,b,c"'))
Styles & number formats
from formulon import FontRecord, FillRecord, CellXf
fi = wb.add_font(FontRecord(name="Calibri", size=12.0, bold=True))
fill = wb.add_fill(FillRecord(pattern=1, fg_argb=0xFFFFFF00))
border = wb.add_border({"left": {"style": 1, "color_argb": 0xFF000000}})
nf = wb.add_num_fmt("0.00")
xf = wb.add_cell_xf(
CellXf(
font_index=fi,
fill_index=fill,
border_index=border,
num_fmt_id=nf,
horizontal_align=0,
vertical_align=0,
wrap_text=False,
)
)
wb.set_cell_xf_index(0, 0, 0, xf)
Conditional formatting
from formulon import ConditionalFormatInput, MergeRange
wb.add_conditional_format(
0,
ConditionalFormatInput(
sqref=[MergeRange(0, 0, 9, 0)],
type=1, # cellIs
op_engaged=True,
op=4,
formula1="100",
),
) # greaterThan 100
matches = wb.evaluate_cf_range(0, 0, 0, 9, 0) # per-cell resolved matches
PivotTables -- build a cache, project a layout:
from formulon import PivotFieldSpec, PivotDataFieldSpec, PivotAxis, PivotAggregation
cache = wb.pivot_cache_create()
wb.pivot_cache_field_add(cache, "Region")
wb.pivot_cache_field_add(cache, "Amount")
rec = wb.pivot_cache_record_add(cache)
wb.pivot_cache_record_set_text(cache, rec, 0, "East")
wb.pivot_cache_record_set_number(cache, rec, 1, 10.0)
pivot = wb.pivot_create(0, "Pivot1", cache, anchor_row=0, anchor_col=4)
region = wb.pivot_field_add(0, pivot, PivotFieldSpec("Region", axis=PivotAxis.ROW))
amount = wb.pivot_field_add(0, pivot, PivotFieldSpec("Amount", axis=PivotAxis.VALUE))
wb.pivot_data_field_add(0, pivot, PivotDataFieldSpec("Sum of Amount", amount, aggregation=PivotAggregation.SUM))
layout = wb.pivot_layout(0, pivot) # -> PivotLayout(top, left, rows, cols, cells)
Dependency trace & spill
wb.set_number(0, 0, 0, 1.0)
wb.set_formula(0, 0, 1, "=A1")
wb.recalc()
wb.precedents(0, 0, 1) # -> [CellNode(sheet=0, row=0, col=0)]
wb.dependents(0, 0, 0) # -> [CellNode(sheet=0, row=0, col=1)]
wb.set_formula(0, 5, 0, "=SEQUENCE(3)")
wb.recalc()
wb.spill_info(0, 5, 0) # -> SpillInfo(engaged=True, rows=3, cols=1, ...)
Function catalog (static; needs no workbook handle)
Workbook.function_count() # number of registered functions
Workbook.function_metadata("SUM", 0) # FunctionMetadata or None
Sheet view / protection / calc policy -- get_sheet_view /
set_sheet_zoom / set_sheet_freeze / set_sheet_tab_hidden,
get_sheet_columns / set_column_width / set_row_height (and the
hidden / outline variants), get_sheet_protection /
set_sheet_protection, calc_mode / set_calc_mode, excel_profile_id
/ set_excel_profile_id, plus get_external_links().
Values and errors
Value exposes kind, number, boolean, text, error_code, plus
to_python() which converts to the natural Python type
(None / float / bool / str) or returns the Value itself for
errors and reserved kinds.
FormulonError is raised only for host-side problems (NULL handle,
parser crash inside Workbook.load, out-of-range index, OOM). Excel cell
errors travel inside Value(kind=ValueKind.ERROR). The one absent
lookup is get_comment, which returns None (not an error) when no
comment is anchored at the requested cell.
Not exposed
recalc() is always serial in this no-pthread WASM wheel: the parallel
scheduler requires a pthread runtime that wasmtime does not provide.
Thread-capable native surfaces can opt in separately (the CLI uses
formulon recalc --threads N), while Python intentionally exposes no
parallel-recalc method. Result fidelity is identical.
The iterative-solver progress callback (fm_workbook_set_iterative_progress
in the C ABI) is intentionally not bound -- it takes a C function
pointer that the host cannot synthesise into the WebAssembly module's
function table through wasmtime. Configure iterative calculation via
set_iterative(enabled, max_iterations, max_change) instead; only the
per-sweep callback is unavailable.
The structured-log sink (fm_set_log_sink) is unavailable for the same
reason: it too takes a C function pointer. The threshold half of that
surface is bound as the module-level set_log_min_level(level), which is
process-wide state rather than a Workbook method. The default is
LogLevel.OFF, so a caller that never touches it sees no engine output;
raising it to LogLevel.WARN or below sends one JSON record per line to
stderr.
Worksheet XML is read through the DOM parser only. The native CLI switches to a streaming parser for worksheets past 256 KiB; that implementation costs binary size the WASM budget does not have, so loading a workbook here needs memory proportional to the largest single worksheet's XML rather than a fixed window. Sheets are read one at a time, so the peak is per worksheet, and the practical ceiling is the 32-bit WASM address space. Results are identical either way.
Building from source
# From the repository root:
make wasm-capi # builds build-wasm-capi/formulon_capi.wasm (Emscripten)
make python-package # stages the wasm into packages/python/formulon/_wasm/
make python-test # runs the smoke tests against the staged package
make python-wheel # produces a py3-none-any build-py/dist/formulon-*.whl
packages/python/scripts/stage.py is the entry point; it just copies
the pre-built formulon_capi.wasm into the package data directory --
no compilation happens inside the Python build.
The wheel is intentionally not built with pip install from source:
that would require Emscripten on the user's machine. CI builds the wheel
once on Linux and publishes it to PyPI as py3-none-any.
Project
Documentation — guides, compatibility notes, and the per-runtime API reference — is at https://formulon.libraz.net. Source, design notes, and the oracle test suite live at https://github.com/libraz/formulon.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file formulon-0.10.0-py3-none-any.whl.
File metadata
- Download URL: formulon-0.10.0-py3-none-any.whl
- Upload date:
- Size: 965.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef9ac01a2895db0a174a6f930fa17a6672dd135a09d495da154ecb6167305c28
|
|
| MD5 |
a99f89e5347b458ad9bd4a469a1caf8c
|
|
| BLAKE2b-256 |
eccb918fd548c119596856a7ea978b7f4f8801f895b3497ba86607d8cef3010f
|
Provenance
The following attestation bundles were made for formulon-0.10.0-py3-none-any.whl:
Publisher:
release.yml on libraz/formulon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
formulon-0.10.0-py3-none-any.whl -
Subject digest:
ef9ac01a2895db0a174a6f930fa17a6672dd135a09d495da154ecb6167305c28 - Sigstore transparency entry: 2501523268
- Sigstore integration time:
-
Permalink:
libraz/formulon@37ad028bfe83a2536d37746e66ddfd9a1ac8036b -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/libraz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@37ad028bfe83a2536d37746e66ddfd9a1ac8036b -
Trigger Event:
push
-
Statement type: