Optional native read-only mapping. The underlying file must not be
September 11, 2026 · View on GitHub
Formualizer for Python
Parse, evaluate, and mutate Excel workbooks at native speed from Python.
A Rust-powered spreadsheet engine with 400+ Excel-compatible functions, exposed through a clean Pythonic API. Tokenize formulas, walk ASTs, evaluate workbooks, and use SheetPort to treat spreadsheets as typed APIs.
Installation
pip install formualizer
Prebuilt stable-ABI (abi3) wheels are published for Python 3.10 and newer on Linux (glibc and musl), macOS, and Windows. No Rust toolchain required.
Documentation
Full documentation at formualizer.dev:
- Python Quickstart
- Python API Reference
- Function Reference — 400+ built-in functions
- SheetPort Guide — spreadsheets as typed APIs
- Workbook Edits and Batching
Quick start
Evaluate a workbook
import formualizer as fz
wb = fz.Workbook()
s = wb.sheet("Sheet1")
s.set_value(1, 1, fz.LiteralValue.number(1000.0)) # A1: principal
s.set_value(2, 1, fz.LiteralValue.number(0.05)) # A2: annual rate
s.set_value(3, 1, fz.LiteralValue.number(12.0)) # A3: periods
s.set_formula(1, 2, "=PMT(A2/12, A3, -A1)")
print(wb.evaluate_cell("Sheet1", 1, 2)) # ~85.61
Load an XLSX and evaluate
import formualizer as fz
wb = fz.load_workbook("financial_model.xlsx", strategy="eager_all")
print(wb.evaluate_cell("Summary", 1, 2))
# Optional native read-only mapping. The underlying file must not be
# destructively modified or truncated while it is loading.
mapped = fz.load_workbook(
"financial_model.xlsx",
path_source=fz.XlsxPathSource.DIRECT_MMAP,
)
Load and save XLSX bytes
import formualizer as fz
payload = open("financial_model.xlsx", "rb").read()
wb = fz.load_workbook_bytes(payload)
print(wb.evaluate_cell("Summary", 1, 2))
out = wb.to_xlsx_bytes()
Native Python builds use calamine by default for both path-based and byte-oriented XLSX loading. Pyodide currently defaults to umya, which also remains available explicitly on native builds. XLSX byte export uses umya because Calamine is read-only.
Recalculate XLSX cached values (writeback)
import formualizer as fz
# in-place
summary = fz.recalculate_file("financial_model.xlsx")
print(summary["status"], summary["evaluated"], summary["errors"])
# write to a new file
summary = fz.recalculate_file(
"financial_model.xlsx", output="financial_model.recalc.xlsx"
)
Formula text is preserved. Cached-value typing follows the active
umya-spreadsheetimplementation.
Cache-only XLSX recalculation
result = fz.recalculate_xlsx_bytes(payload)
assert isinstance(result["bytes"], bytes)
print(result["summary"]["status"], result["cache_cells_changed"])
# The file API snapshots input and atomically replaces the destination on success.
result = fz.recalculate_xlsx_file("model.xlsx", output="model.recalc.xlsx")
These APIs use the shared cache-only Rust implementation, retaining formula text
and unrelated package members. Safe core resource limits apply;
error_location_limit= only caps retained error locations.
Parse and analyze formulas
from formualizer import parse
from formualizer.visitor import collect_references, collect_function_names
ast = parse("=SUMIFS(Revenue,Region,A1,Year,B1)")
print(ast.pretty()) # indented AST tree
print(ast.to_formula()) # canonical Excel string
print(collect_references(ast)) # [Revenue, Region, A1, Year, B1]
print(collect_function_names(ast)) # ['SUMIFS']
Key features
| Capability | Description |
|---|---|
| Tokenization | Break formulas into structured Token objects with byte spans and operator metadata |
| Parsing | Produce a rich AST with reference normalization, source tracking, and 64-bit structural fingerprints |
| 400+ built-in functions | Math, text, lookup (XLOOKUP, VLOOKUP), date/time, financial, statistics, database, engineering |
| Workbook evaluation | Set values and formulas, evaluate cells/ranges, load XLSX/CSV/JSON |
| XLSX cache writeback | recalculate_file(path, output=None) recalculates formulas and writes cached values back |
| Batch operations | set_values_batch / set_formulas_batch for efficient bulk updates |
| Undo / redo | Optional changelog with automatic action grouping — single edits are individually undoable |
| Evaluation planning | Inspect the dependency graph and evaluation schedule before computing |
| SheetPort | Treat spreadsheets as typed functions with YAML manifests, schema validation, and batch scenarios |
| Deterministic mode | Inject clock, timezone, and RNG seed for reproducible evaluation |
| Visitor utilities | walk_ast, collect_references, collect_function_names for ergonomic tree traversal |
| Rich errors | Typed TokenizerError / ParserError / ExcelEvaluationError with position info |
Workbook evaluation
import formualizer as fz
wb = fz.Workbook()
s = wb.sheet("Data")
# Set values and formulas
s.set_value(1, 1, fz.LiteralValue.number(100.0))
s.set_value(2, 1, fz.LiteralValue.number(200.0))
s.set_value(3, 1, fz.LiteralValue.number(300.0))
s.set_formula(4, 1, "=SUM(A1:A3)")
s.set_formula(4, 2, "=AVERAGE(A1:A3)")
print(wb.evaluate_cell("Data", 4, 1)) # 600.0
print(wb.evaluate_cell("Data", 4, 2)) # 200.0
Custom functions
Register workbook-local callbacks without forking Formualizer:
import formualizer as fz
wb = fz.Workbook(mode=fz.WorkbookMode.Ephemeral)
wb.add_sheet("Sheet1")
wb.register_function(
"py_add",
lambda a, b: a + b,
min_args=2,
max_args=2,
)
wb.set_formula("Sheet1", 1, 1, "=PY_ADD(20,22)")
print(wb.evaluate_cell("Sheet1", 1, 1)) # 42
print(wb.list_functions())
wb.unregister_function("py_add")
Key semantics:
- Names are case-insensitive and stored canonically (
py_add->PY_ADD). - Custom functions are workbook-local and take precedence over global built-ins.
- Built-in override is disabled by default; set
allow_override_builtin=Trueto opt in. - Args are passed by value; range inputs arrive as nested Python lists.
- Return Python primitives, datetime/date/time/timedelta, dict error objects, or nested lists for array spill output.
- Python callback exceptions are sanitized and mapped to
#VALUE!.
Runnable example: python bindings/python/examples/custom_function_registration.py
Batch operations
# Bulk-set values (auto-grouped as one undo step when changelog is enabled)
s.set_values_batch(
1,
1,
3,
2,
[
[fz.LiteralValue.number(10.0), fz.LiteralValue.number(20.0)],
[fz.LiteralValue.number(30.0), fz.LiteralValue.number(40.0)],
[fz.LiteralValue.number(50.0), fz.LiteralValue.number(60.0)],
],
)
Undo / redo
The changelog is opt-in. Once enabled, every edit is tracked:
wb.set_changelog_enabled(True)
s.set_value(1, 1, fz.LiteralValue.number(10.0))
s.set_value(1, 1, fz.LiteralValue.number(20.0))
wb.undo() # back to 10
wb.redo() # back to 20
# Batch methods are auto-grouped as one undo step.
# For manual grouping of multiple calls:
wb.begin_action("update prices")
s.set_value(1, 1, fz.LiteralValue.number(100.0))
s.set_value(2, 1, fz.LiteralValue.number(200.0))
wb.end_action()
wb.undo() # reverts both values at once
Evaluation planning
Inspect what the engine will compute before running:
plan = wb.get_eval_plan([("Sheet1", 1, 2)])
print(f"Vertices to evaluate: {plan.total_vertices_to_evaluate}")
print(f"Parallel layers: {plan.estimated_parallel_layers}")
for layer in plan.layers:
print(f" Layer: {layer.vertex_count} vertices, parallel={layer.parallel_eligible}")
# By default this will build deferred workbook graphs if needed.
# Disable that behavior if you want planning to fail instead of mutating workbook state.
wb.get_eval_plan([("Sheet1", 1, 2)], build_graph_if_needed=False)
SheetPort: spreadsheets as typed APIs
Define a YAML manifest to treat a spreadsheet as a typed function with validated inputs/outputs:
from formualizer import SheetPortSession, Workbook
manifest_yaml = """
spec: fio
spec_version: "0.3.0"
manifest:
id: pricing-model
name: Pricing Model
workbook:
uri: memory://pricing.xlsx
locale: en-US
date_system: 1900
ports:
- id: base_price
dir: in
shape: scalar
location: { a1: Inputs!A1 }
schema: { type: number }
- id: final_price
dir: out
shape: scalar
location: { a1: Outputs!A1 }
schema: { type: number }
"""
wb = Workbook()
wb.add_sheet("Inputs")
wb.add_sheet("Outputs")
wb.set_formula("Outputs", 1, 1, "=Inputs!A1*1.2")
session = SheetPortSession.from_manifest_yaml(manifest_yaml, wb)
session.write_inputs({"base_price": 100.0})
result = session.evaluate_once(freeze_volatile=True)
print(result["final_price"]) # 120.0
API reference
Top-level functions
tokenize(formula: str, dialect: FormulaDialect = None) -> Tokenizer
parse(formula: str, dialect: FormulaDialect = None) -> ASTNode
load_workbook(path: str, strategy: str = None, *, path_source: XlsxPathSource | None = None, span_evaluation: bool | None = None) -> Workbook
load_workbook_bytes(data: bytes, strategy: str = None, backend: str | None = None, *, span_evaluation: bool | None = None) -> Workbook
recalculate_file(path: str, output: str | None = None) -> dict
recalculate_xlsx_bytes(data: bytes, *, error_location_limit: int | None = None) -> dict
recalculate_xlsx_file(path: str, output: str | None = None, *, error_location_limit: int | None = None) -> dict
Core classes
Workbook— create, load, evaluate, undo/redo. Supportsfrom_path(),from_bytes(),load_path(), andto_xlsx_bytes().Sheet— per-sheet facade forset_value,set_formula,get_cell, batch operations.LiteralValue— typed values:.int(),.number(),.text(),.boolean(),.date(),.time(),.datetime(),.duration(),.empty(),.error(),.array().Tokenizer— iterable token sequence with.render()and.tokens.ASTNode—.pretty(),.to_formula(),.fingerprint(),.children(),.walk_refs().CellRef/RangeRef/TableRef/NamedRangeRef— typed references.SheetPortSession— bind manifests to workbooks, read/write typed ports, evaluate.EvaluationConfig— tune parallel evaluation, warmup, range limits, date systems.
Visitor helpers (formualizer.visitor)
walk_ast(node, visitor_fn) # DFS with VisitControl (CONTINUE/SKIP/STOP)
collect_references(node) # -> list[ReferenceLike]
collect_function_names(node) # -> list[str]
collect_nodes_by_type(node, "Function") # -> list[ASTNode]
Full type stubs are included in the package (.pyi files) for IDE autocompletion and mypy.
Building from source
Requires Rust 1.93.0 (the pinned release toolchain; edition 2024) and maturin:
pip install maturin
cd bindings/python
maturin develop # debug build
maturin develop --release # optimized build
Using in Pyodide (browser / WebAssembly)
Native wheels are published to PyPI. Pyodide wheels are built and smoke-tested in CI and release workflows, then uploaded only as the wheels-pyodide Actions artifact; they are not uploaded to PyPI or attached to GitHub Releases. Download and extract the artifact (or build locally), then host the compatible wheel at a browser-accessible URL with suitable CORS headers. An Actions artifact ZIP is not a wheel URL:
import micropip
wheel_url = "<your-downloadable-wheel-url>"
await micropip.install(wheel_url)
import formualizer as fz
wb = fz.Workbook()
wb.add_sheet("Sheet1")
wb.set_value("Sheet1", 1, 1, 20)
wb.set_value("Sheet1", 2, 1, 22)
wb.set_formula("Sheet1", 1, 2, "=SUM(A1:A2)")
wb.evaluate_cell("Sheet1", 1, 2) # -> 42.0
Tested Pyodide target: CI and release smoke tests use Pyodide 0.29.3 and the wheel's derived ABI (currently pyodide_2025_0). Rebuild and smoke-test a wheel when targeting another runtime; no persistent public wheel URL is promised.
Pyodide-specific behavior:
EvaluationConfig()andWorkbook()defaultenable_parallel = Falseonsys.platform == "emscripten"(Pyodide has no threads). You can still opt in, but it falls back to single-threaded execution.- Native XLSX byte loading (
Workbook.from_bytes,load_workbook_bytes) defaults tocalamine; Pyodide defaults toumya. XLSX byte export usesumyaon all platforms. - Python UDFs registered via
Workbook.register_functionwork identically to native; single-cell refs arrive as scalars (Excel-native semantics).
Building a Pyodide wheel from source
For local development or targeting a Pyodide version without a retained Actions artifact:
./scripts/build-pyodide-wheel.sh
./scripts/smoke-pyodide-wheel.sh dist/pyodide/*-pyodide_*_wasm32.whl
The build script defaults to xbuildenv Pyodide 0.29.3, derives Python, ABI, Emscripten, and Rust toolchain values from pyodide config, installs Pyodide's custom wasm-EH Rust sysroot over the stock rustup target, and retags the output wheel to the platform tag Pyodide's micropip expects. pyodide-cli and pyodide-build are resolved through uvx and are not pinned by the script.
Testing
pip install formualizer[dev]
pytest bindings/python/tests
ruff check bindings/python
mypy bindings/python/formualizer
Workspace layout
formualizer/
crates/ # Rust core (parse, eval, workbook, sheetport)
bindings/python/
formualizer/ # Python package (helpers, visitor, type stubs)
src/ # PyO3 bridge (Rust -> Python)
The Python wheel links directly against the Rust crates — there is no runtime FFI overhead beyond the initial C-to-Rust boundary.
License
Dual-licensed under MIT or Apache-2.0, at your option. Both license texts ship inside the package.