How-to
Getting Started With Python Excel AutomationDeep dive

Turn an Excel Model into a Python Function

Keep a profit model in Excel where finance owns it, and call it from Python like a function: compile the workbook's formulas with the formulas library into a callable that takes the input cells and returns the output cells, check its answers against LibreOffice, run two thousand Monte Carlo scenarios in a few seconds, and catch functions the library cannot evaluate before trusting the results.

Many decisions run on an Excel model: a pricing sheet, a business case, a capacity calculation. The model belongs in Excel because the people who own the assumptions can read and change it there. But some questions need it run many times — what happens across two thousand combinations of volume and price, which inputs move profit the most, how likely a loss is — and recalculating a workbook two thousand times through Excel or LibreOffice is slow and fragile. The formulas library offers another route. It reads a workbook's formulas, builds the dependency graph between cells, and can compile the part of it between chosen input cells and output cells into an ordinary Python function. The workbook stays the single source of the logic; Python just calls it. This page compiles a small profit model, checks it against LibreOffice, runs a Monte Carlo simulation with it, and shows how to detect formulas the library cannot evaluate. It belongs to Working with Excel Formulas in Python.

Prerequisites

Bash
pip install formulas openpyxl numpy

The model is a single sheet: five inputs in B1:B5 — units, price, unit cost, fixed costs and tax rate — and formulas below them for revenue, costs, profit, tax and net margin. LibreOffice is used once to confirm the library's answers.

Step 1 — Load the model and calculate it once

Python
import formulas

model = formulas.ExcelModel().loads("profit_model.xlsx").finish()
solution = model.calculate()

def cell(ref):
    return f"'[profit_model.xlsx]MODEL'!{ref}"

for ref, label in [("B6", "Revenue"), ("B9", "Operating profit"), ("B11", "Net profit"), ("B12", "Net margin")]:
    print(f"{label:17} {solution[cell(ref)].value[0][0]:>12,.4f}")

loads parses the workbook, finish builds the dependency graph, and calculate evaluates every formula from the stored inputs. The results are keyed by fully qualified cell references — workbook, sheet and cell, with the sheet name in capitals — which is verbose, so a small helper builds the keys. Each value is a small two-dimensional array, because the library treats every reference as a range; [0][0] takes the single value. With the stored inputs the model gives revenue of 294,000, operating profit of 43,600 and net profit of 32,700, a net margin of about 11.1%.

Step 2 — Check the answers against a real spreadsheet engine

Bash
soffice --headless --convert-to xlsx --outdir checked profit_model.xlsx > /dev/null 2>&1
python -c "
from openpyxl import load_workbook
ws = load_workbook('checked/profit_model.xlsx', data_only=True)['Model']
print({ws[f'A{r}'].value: ws[f'B{r}'].value for r in (6, 9, 11, 12)})"

Before relying on a second implementation of Excel's calculation rules, compare it with an established one. LibreOffice recalculates the same workbook and saves the results, which openpyxl reads with data_only=True; the values match the library's exactly. It is worth repeating this check whenever the model changes, and especially when new functions appear in it, because the library implements Excel functions one by one and its coverage, while broad, is not complete. The recalculation route on its own is described in Recalculate Excel Formulas Without Excel in Python.

From workbook to function The formulas library parses the workbook and builds a graph of which cells depend on which. Compiling with chosen input and output cells keeps only the part of the graph between them and returns a Python function that takes the inputs and returns the outputs. profit_model.xlsx owned by finance Parse formulas loads + finish Dependency graph cell → cell compile(inputs, outputs) keep the path between f(units, price) a Python function

Step 3 — Compile the model into a function

Python
profit = model.compile(inputs=[cell("B1"), cell("B2")], outputs=[cell("B11"), cell("B12")])

def run(units, price):
    net, margin = profit(units, price)
    return net.value[0][0], margin.value[0][0]

print(run(12000, 24.5))
print(run(10000, 27.0))
print(run(6000, 22.0))

compile takes a list of input cells and a list of output cells and returns a callable. The arguments are the input values in the same order; everything not listed as an input — unit cost, fixed costs, tax rate — keeps the value stored in the workbook. The function evaluates only the formulas between the inputs and the outputs. Selling 10,000 units at 27.00 instead of 12,000 at 24.50 raises net profit from 32,700 to 37,500, because the higher price more than makes up for the lost volume. At 6,000 units and 22.00 the business makes an operating loss of 27,200, the MAX(0, …) in the tax formula keeps tax at zero, and the net margin comes out at about −20.6%.

Three calls to the compiled model At 12,000 units and 24.50 the net profit is 32,700 with an 11.1 percent margin. At 10,000 units and 27.00 it is 37,500 with a 13.9 percent margin. At 6,000 units and 22.00 there is a loss of 27,200 with no tax. A B C D 1 Units Price Net profit Net margin 2 12,000 24.50 32,700 11.1% 3 10,000 27.00 37,500 13.9% 4 6,000 22.00 −27,200 −20.6%

Step 4 — Run two thousand scenarios

Python
import time
import numpy as np

rng = np.random.default_rng(42)
units = rng.normal(11000, 1500, 2000).round()
prices = rng.uniform(22.0, 27.0, 2000).round(2)

t0 = time.perf_counter()
results = np.array([run(u, p) for u, p in zip(units, prices)])
elapsed = time.perf_counter() - t0

net = results[:, 0]
print(f"{len(net)} scenarios in {elapsed:.1f} s")  # about 1.7 s on the machine used for this page
print(f"median net profit {np.median(net):,.0f}; 5th–95th percentile {np.percentile(net, 5):,.0f} to {np.percentile(net, 95):,.0f}")
print(f"probability of a loss: {(net < 0).mean():.1%}")

With the model as a function, a simulation is a loop. Units are drawn from a normal distribution around 11,000 and prices uniformly between 22 and 27, and each pair goes through the compiled model. Here two thousand runs took under two seconds — much less than recalculating two thousand copies of the workbook with LibreOffice, where each recalculation means writing a file, starting a conversion and reading the result back. The output is the distribution of net profit: its median, the range covering 90% of scenarios, and the share of scenarios that make a loss — a median of about 23,800, a 90% range from roughly 1,100 to 51,600, and a loss in about 4% of scenarios. The same pattern gives a sensitivity table — vary one input at a time across a range and record the output — or feeds an optimiser looking for the price that maximises profit. Because the logic is still the workbook's, finance can change a formula, save the file, and the next simulation uses the new logic without any change to the Python.

Step 5 — Detect formulas the library cannot evaluate

Python
from openpyxl import Workbook

check = Workbook()
cs = check.active
cs["A1"] = '=_xlfn.XLOOKUP(2,{1,2,3},{10,20,30})'
cs["A2"] = '=WEBSERVICE("https://example.com/rate")'
cs["A3"] = '=_xlfn.STOCKHISTORY("MSFT",1)'
check.save("function_check.xlsx")

results = formulas.ExcelModel().loads("function_check.xlsx").finish().calculate()
for ref in ("A1", "A2", "A3"):
    v = results[f"'[function_check.xlsx]SHEET'!{ref}"].value[0][0]
    flag = "UNSUPPORTED" if str(v) == "#NAME?" else "ok"
    print(f"{ref:4} {v!s:8} {flag}")

When the library meets a function it does not implement, it does not raise an error — it returns #NAME?, the same error Excel shows for an unknown function, and that error then flows into every dependent cell. Modern functions such as XLOOKUP, LET and TEXTJOIN are supported, but functions that reach outside the workbook, such as WEBSERVICE, STOCKHISTORY and the cube functions, are not, and neither are a number of rarely used statistical and engineering functions. Scanning the results of a first calculate() for #NAME? — or any error value — before compiling is a cheap guard against a model that silently returns errors for every scenario.

When this is the right tool

Compiling suits models that are mostly arithmetic, lookups and logic, that finance wants to keep owning in Excel, and that need to be run far more often than a person would recalculate them: simulations, sensitivity tables, optimisation, or a web form that prices a quote. It suits less well models full of external data connections, macros or unsupported functions, and very large models where the first finish() is slow. When Python will own the logic from now on, rewriting the calculation in pandas is clearer still; the formula equivalents in Excel Formula Equivalents in pandas show how. And when the question is a handful of named scenarios rather than thousands, recalculating copies with LibreOffice, as in Run What-If Scenarios on an Excel Model with Python, is simpler and uses a full spreadsheet engine.

Compile with formulas or recalculate with LibreOffice Compiling the model with the formulas library suits thousands of runs of a model built from supported functions. Recalculating copies with LibreOffice suits a few named scenarios and models that use functions the library lacks. Compile with formulas thousands of runs in seconds in-process, no files written supported functions only check #NAME? first Recalculate with LibreOffice a few named scenarios full spreadsheet engine one file per scenario seconds per run

Common pitfalls

  • Short cell references. Keys are fully qualified: '[book.xlsx]SHEET'!B11, sheet in capitals.
  • Forgetting the array. Each value is a 2-D range; take [0][0] for a single cell.
  • Trusting it blindly. Compare a run with LibreOffice or Excel first.
  • Unsupported functions. They return #NAME? silently; scan for error values.
  • Inputs not listed. Cells not passed to compile keep their stored values.

Conclusion

To call an Excel model from Python many times, load it with formulas.ExcelModel().loads(path).finish(), confirm one calculation against LibreOffice, and compile it with the input and output cells you need into a function. That function runs thousands of scenarios in seconds while the logic stays in the workbook finance maintains. Check first that no cell evaluates to #NAME?, since unsupported functions fail silently.

Frequently asked questions

Can Python calculate Excel formulas without Excel? Yes; the formulas library parses and evaluates workbook formulas in pure Python, and LibreOffice can recalculate a workbook headlessly.

How do I run an Excel model with different inputs from Python? Compile it with model.compile(inputs=[...], outputs=[...]) and call the returned function with the input values.

What happens if the library does not support a function? The cell evaluates to #NAME?, which then spreads to dependent cells; scan the results for error values.

Is compiling faster than recalculating with LibreOffice? For many runs, yes: the compiled function evaluates in-process, while each LibreOffice recalculation writes, converts and reads a file.

More on formulas in workbooks

More on formulas in workbooks →