Recalculate Excel Formulas Without Excel in Python
ws["B10"].value returns '=SUM(B2:B9)' when you wanted 4823.50, and loading with data_only=True returns None. Neither is a bug: openpyxl reads what is in the file, and the file only contains a cached result if Excel has saved it. A workbook your script just wrote has no cache at all. This guide covers the three ways to get real numbers without Excel — a pure-Python calculation engine, a headless LibreOffice pass, and the restructuring that makes the question go away. It extends Working with Excel Formulas in Python.
Prerequisites
pip install openpyxl pandas formulas
The LibreOffice route needs the application itself, available as soffice on the path:
sudo apt-get install -y libreoffice-calc # Debian/Ubuntu
Confirm the diagnosis first
from openpyxl import load_workbook
formulas = load_workbook("report.xlsx")["Summary"]["B10"].value
values = load_workbook("report.xlsx", data_only=True)["Summary"]["B10"].value
print(repr(formulas), repr(values))
If the first prints a formula and the second prints None, the file has no cached results — it was written by a library and never opened in Excel. If the second prints a number, you already have what you need; the cache is there and no recalculation is required. The distinction is explored further in Read formula results with openpyxl data_only.
Option 1: calculate in pure Python
The formulas package parses a workbook, builds a dependency graph and evaluates it — no Excel, no LibreOffice, no external process:
"""Recalculate a workbook and read a result, in pure Python."""
import formulas
model = formulas.ExcelModel().loads("report.xlsx").finish()
solution = model.calculate()
for key, value in solution.items():
if key.upper().endswith("'[REPORT.XLSX]SUMMARY'!B10"):
print(key, "=", value)
It can also write the recalculated workbook back out, cached values included:
model.calculate()
model.write(dirpath="./calculated") # writes report.xlsx with results
The keys are fully qualified — book, sheet and cell — which is verbose but unambiguous in a workbook with cross-sheet references. Coverage is the caveat: formulas implements a large subset of Excel's function library, and a workbook that uses an unsupported function raises rather than silently guessing. Test it against your real file before building a pipeline on it.
Option 2: let LibreOffice recalculate
LibreOffice recalculates on load and can write the result back, which gives much broader function coverage at the cost of a heavier dependency:
"""Recalculate by round-tripping through headless LibreOffice."""
import subprocess
from pathlib import Path
def recalculate(path: Path, out_dir: Path) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(
["soffice", "--headless", "--norestore",
"--convert-to", "xlsx", "--outdir", str(out_dir), str(path)],
check=True, timeout=180,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
)
return out_dir / path.name
result = recalculate(Path("report.xlsx"), Path("calculated"))
Then read the cached values normally:
from openpyxl import load_workbook
wb = load_workbook(result, data_only=True)
print(wb["Summary"]["B10"].value) # a number, at last
Two operational notes. LibreOffice keeps a user profile directory and will refuse to run two instances against the same one, so pass -env:UserInstallation=file:///tmp/lo-$$ when running conversions in parallel. And always set a timeout: a malformed file can hang the process indefinitely, which is exactly the failure a nightly job cannot recover from. The same tool converts to PDF, as covered in Convert an Excel file to PDF with Python.
Option 3: do not put the calculation in the sheet
The fastest recalculation is the one that never happens. If your code generates the workbook, compute the numbers in Python and write values — optionally alongside the formula, so a reader can still see how a figure was derived:
import pandas as pd
from openpyxl import load_workbook
df = pd.read_excel("raw.xlsx", engine="calamine")
total = float(df["revenue"].sum())
wb = load_workbook("report.xlsx")
ws = wb["Summary"]
ws["B10"] = total # the number, computed and testable
ws["C10"] = "=SUM(B2:B9)" # the formula, for transparency
wb.save("report.xlsx")
This is the right default for generated reports. The calculation lives in code that can be unit-tested and reviewed, the delivered file opens with correct numbers everywhere, and nothing depends on the recipient's software recalculating anything.
Verify the recalculation
Whichever route you take, check a known figure rather than assuming:
from openpyxl import load_workbook
wb = load_workbook("calculated/report.xlsx", data_only=True)
ws = wb["Summary"]
expected = round(sum(ws.cell(row=r, column=2).value or 0 for r in range(2, 10)), 2)
actual = round(ws["B10"].value or 0, 2)
assert abs(expected - actual) < 0.01, f"total mismatch: {actual} vs {expected}"
An assertion like this belongs in the job, not just in a test: a recalculation that silently produced zeros — because a function was unsupported, or the process timed out — otherwise ships a plausible-looking report full of blanks.
Cache results so the recipient sees numbers immediately
There is a middle path worth knowing: write the formula and its cached result, so the file shows correct values the instant it opens and still recalculates if the reader edits an input. openpyxl does not expose the cache directly, but xlsxwriter does:
import xlsxwriter
with xlsxwriter.Workbook("report.xlsx") as book:
ws = book.add_worksheet("Summary")
values = [1200.0, 980.5, 1450.0, 610.25]
for row, value in enumerate(values, start=1):
ws.write_number(row, 1, value)
# formula string plus the result Excel would compute
ws.write_formula(len(values) + 1, 1, "=SUM(B2:B5)", None, sum(values))
The fifth argument to write_formula is the cached value. Excel shows it immediately and replaces it the moment anything recalculates, so the two can never disagree for long. Getting it wrong is worse than omitting it, though — a wrong cached value is displayed as fact until someone edits the sheet, so compute it with the same code that produced the data.
The PDF row is the one that catches teams out. A report exported to PDF without cached results prints blank cells wherever a formula sits, because no application ever recalculated the file — see Convert an Excel file to PDF with Python.
Common pitfalls and gotchas
- Expecting
data_only=Trueto compute. It reads a cache; it never calculates. - Losing formulas on save. Opening with
data_only=Trueand saving writes the cached values instead of the formulas, permanently. - Unsupported functions.
formulasraises on what it does not implement — catch it and fall back rather than shipping a partial recalculation. - Parallel LibreOffice runs. They collide on the shared user profile; give each an isolated
-env:UserInstallation. - Volatile functions.
NOW(),TODAY()andRAND()change on every recalculation, so an output diff will never be stable.
Performance and scale notes
formulas builds a dependency graph of the whole workbook, so its cost grows with the number of formula cells rather than the number of data rows — a sheet with ten thousand formula cells is genuinely slow, while one with ten formulas over a hundred thousand rows is fast. LibreOffice costs a process start of a second or two plus its own recalculation, which makes it a batch tool: convert many files in one run rather than starting the application per request. Computing in pandas is orders of magnitude faster than either and is the only option that scales linearly with data rather than with formulas. For workbooks large enough that this matters, the write-side techniques in Write large DataFrames to Excel with write-only mode pair well with computing values up front.
Conclusion
openpyxl does not calculate, and a file written by Python has no cached results to read. Pick the route that matches the workbook: formulas when it is simple and you want to stay in Python, headless LibreOffice when you have inherited a model that uses the full function library, and computed values when you own the report — which is faster, testable, and removes the dependency altogether. Whichever you choose, assert a known total before delivery so a silent recalculation failure cannot reach a reader.
Frequently asked questions
Why does openpyxl return None for my formula cell?
With data_only=True openpyxl returns the cached result Excel stored when it last saved the file. A workbook written by a Python library has never been opened by Excel, so there is no cache and the value is None.
Does the formulas library support every Excel function? No. It covers a large, useful subset — arithmetic, logic, lookup, text and common maths and date functions — but not everything, and not modern dynamic arrays. Test it against your actual workbook before depending on it.
Is LibreOffice a reasonable dependency on a server?
Yes, and it is a common one. Headless LibreOffice recalculates on load and can convert to .xlsx or PDF, which makes it the most compatible option short of Excel itself.
What is the fastest approach? Not recalculating at all. Compute the numbers in pandas and write values, optionally alongside formulas for transparency. That runs in milliseconds and needs no extra process.
Can I keep the formulas and still have values? Yes — write the formula string and set the cached value, or recalculate once with LibreOffice before delivery so the file carries both.
Related
- Up: Working with Excel Formulas in Python — writing, copying and reading formulas.
- Read formula results with openpyxl data_only — the cache this page works around.
- Convert Excel formulas to values with Python — freezing results once they are correct.
- Refresh Excel pivot tables and queries with Python — the same problem when Excel is available to drive.
- Convert an Excel file to PDF with Python — the other job headless LibreOffice does well.