Fix
Getting Started With Python Excel AutomationDeep dive

Find #REF!, #DIV/0! and Other Formula Errors in a Workbook

pandas turns formula errors into NaN, so a broken workbook can look merely incomplete. Scan every sheet with openpyxl for cells whose cached value is an error, catch #REF! inside formula text even without a recalculation, and fail a report job before it sends a workbook full of errors.

A workbook with a dozen broken formulas still opens, still has numbers in most cells, and reads into pandas without a complaint. That last part is the problem: pandas' Excel reader converts every error value — #DIV/0!, #N/A, #REF! — into NaN, so a lookup that failed for half the rows looks exactly like data that was never filled in. A report job that reads, aggregates and emails such a file will send wrong totals with a straight face. This page, part of Working with Excel Formulas in Python, shows how to find every error in a workbook with openpyxl, including the ones that have not been calculated yet.

The same cells in Excel and in a DataFrame Left: the Sales sheet as a spreadsheet shows #DIV/0! in the price column for South and #N/A in the vs target column where the lookup failed. Right: the same range read with pandas shows NaN in both places, indistinguishable from empty cells. A B C 1 region price vs target 2 North 100 1000 3 South #DIV/0! #N/A 4 East 105.6 900 A B C 1 region price vs target 2 North 100 1000 3 South NaN NaN 4 East 105.6 900 in the workbook in the DataFrame pd.read_excel()

Prerequisites

Bash
pip install openpyxl pandas
# LibreOffice, for recalculating files written by Python (Step 3)

The examples use report.xlsx: a Sales sheet with a price formula that divides by a zero quantity and a lookup against a Targets sheet that is missing one region, a text-plus-number formula in H2, and a Summary sheet with one formula whose reference has been lost. The setup recalculated it with LibreOffice, so it contains cached results just like a file saved by Excel.

Step 1 — See pandas hide the errors

Python
import pandas as pd

sales = pd.read_excel("report.xlsx", sheet_name="Sales")
print(sales[["region", "price", "vs target"]])
print(sales["price"].isna().sum(), "missing prices")

The South row shows NaN for both the price and the target comparison. The DataFrame gives no hint that one of those is a division by zero and the other a failed lookup, and a sales["price"].mean() would silently skip the row. pandas does this on purpose — its reader maps error cells to NaN — so no reading option brings the error codes back. You need openpyxl.

Step 2 — Scan cached values for error cells

When a workbook is saved by a spreadsheet application, each formula cell stores its last result alongside the formula. Open the file with data_only=True and error results appear as strings like '#DIV/0!', with the cell's data_type set to 'e'. Open it a second time without data_only to get the formulas that produced them:

Python
from openpyxl import load_workbook

def find_errors(path):
    values = load_workbook(path, data_only=True)
    formulas = load_workbook(path)
    found = []
    for ws in values.worksheets:
        for row in ws.iter_rows():
            for cell in row:
                if cell.data_type == "e":
                    found.append({"sheet": ws.title, "cell": cell.coordinate, "error": cell.value,
                                  "formula": formulas[ws.title][cell.coordinate].value})
    return pd.DataFrame(found, columns=["sheet", "cell", "error", "formula"])

errors = find_errors("report.xlsx")
print(errors.to_string(index=False))

Each row names the sheet, the cell, the error and the formula behind it, which is usually enough to see the cause at a glance: =C3/B3 with a zero in B3, a VLOOKUP for a region the Targets sheet does not have, text added to a number. Checking data_type rather than matching strings means a cell that genuinely contains the text "#N/A" — typed by a person as a placeholder — is not reported as an error.

Scanning a workbook for formula errors Four steps. Recalculate the workbook if it has no cached values. Load it twice with openpyxl, once with data_only for results and once for formulas. Visit every cell and keep those whose data_type is e. Report sheet, cell, error code and formula, then fail the job if anything was found. Recalculate if needed files written by Python Load twice data_only=True and formulas Keep data_type "e" error results only Report or fail sheet, cell, error, formula

Step 3 — Recalculate files that have no cached values

Workbooks written by openpyxl or pandas contain formulas but no results — nothing has calculated them yet. Opened with data_only=True, every formula cell reads as None, and the scan finds nothing:

Python
print(load_workbook("report_raw.xlsx", data_only=True)["Sales"]["D3"].value)
# None — never calculated

Recalculate before scanning. On a server without Excel, LibreOffice in headless mode opens the file, calculates it and saves a copy with cached results:

Python
import subprocess
from pathlib import Path

def recalculated(path, outdir="recalc"):
    subprocess.run(["soffice", "--headless", "--convert-to", "xlsx", "--outdir", outdir, str(path)],
                   check=True, capture_output=True, timeout=120)
    return Path(outdir) / Path(path).name

print(find_errors(recalculated("report_raw.xlsx"))[["sheet", "cell", "error"]].to_string(index=False))

Spreadsheet applications do not all report every failure with the same code — LibreOffice, for instance, exports some errors that Excel would show as #NUM! as #VALUE! — so treat the code as a hint and the formula as the evidence. Recalculate Excel Formulas Without Excel in Python compares the ways to get cached values.

Step 4 — Find #REF! in formula text

One class of error is visible without any calculation. When a sheet, row or column that a formula refers to is deleted, Excel rewrites the reference inside the formula as #REF! — =SUM(Old!B2:B9) becomes =SUM(#REF!). The broken reference is then part of the formula text in the saved file, so a plain text search finds it:

Python
def find_broken_refs(path):
    wb = load_workbook(path)
    hits = []
    for ws in wb.worksheets:
        for row in ws.iter_rows():
            for cell in row:
                if isinstance(cell.value, str) and cell.value.startswith("=") and "#REF!" in cell.value.upper():
                    hits.append(f"{ws.title}!{cell.coordinate}: {cell.value}")
    return hits

print(find_broken_refs("report_raw.xlsx"))

This check is cheap and works on any file, recalculated or not, which makes it a good first gate for templates and workbooks received from other people. The case-insensitive comparison matters: some applications rewrite the token in lower case when they save. References to other workbooks are a related source of silent breakage; Find External Links and Broken References in a Workbook with Python covers them.

Step 5 — Summarise and fail the job

For a report job, the useful output is a short summary in the log and a hard stop when anything is wrong:

Python
def assert_no_errors(path):
    errors = find_errors(path)
    broken = find_broken_refs(path)
    if errors.empty and not broken:
        return
    summary = errors.groupby(["sheet", "error"]).size().rename("cells").reset_index()
    message = [f"{len(errors)} error cell(s) and {len(broken)} broken reference(s) in {path}"]
    message += [f"  {r.sheet}: {r.cells} x {r.error}" for r in summary.itertuples()]
    message += [f"  {b}" for b in broken[:10]]
    raise ValueError("\n".join(message))

try:
    assert_no_errors("report.xlsx")
except ValueError as exc:
    print(exc)

Counting by sheet and error type keeps the message readable when a single bad lookup has failed in five hundred rows. Put the check at the end of the job, after the workbook is written and recalculated and before it is emailed or published; Validate an Excel Report Before Sending It shows it alongside the other pre-send checks.

Common error values and what usually causes them A table of error values. #DIV/0! comes from dividing by zero or an empty cell. #N/A comes from a lookup that found no match. #VALUE! comes from arithmetic on text or a wrong argument type. #NAME? comes from an unknown function or name. #REF! comes from a reference to a deleted sheet, row or column. Each has a typical fix. Usual cause Typical fix #DIV/0! divide by zero or blank guard the divisor, or IFERROR by design #N/A lookup found no match add the key or handle the gap #VALUE! text in arithmetic clean the input column #NAME? unknown function or name fix spelling or add _xlfn. #REF! deleted sheet or range rebuild the reference

Decide which errors are allowed

Not every error is a bug. A price per unit is undefined when no units were sold, and a report may legitimately show that. The honest way to allow it is in the formula, where readers see the decision: =IF(B3=0,"",C3/B3) or =IFERROR(C3/B3,""). Avoid wrapping everything in IFERROR to make the scan pass — it converts a missing lookup key, a broken reference or text in a number column into a blank, which is precisely the silent failure this page set out to catch. Allow specific, expected cases; let everything else fail. Fix #NAME? in Formulas Written from Python covers the most common cause of #NAME? in generated workbooks.

Common pitfalls

  • Scanning a workbook that was never calculated. Every formula reads as None; recalculate first.
  • Matching the string "#N/A". Use data_type == "e" so typed placeholders are not reported.
  • Trusting the DataFrame. NaN after read_excel may be an error, not a gap.
  • Hiding errors with blanket IFERROR. It makes scans pass and reports wrong.

Performance notes

The scan loads the workbook twice and visits every cell, which takes a few seconds for sheets with hundreds of thousands of cells. For large files, use read_only=True for both loads and iterate the same sheets in step, or restrict the scan to the sheets and columns that contain formulas. LibreOffice recalculation is usually the slowest part — several seconds per file — so run it once and scan the result, rather than recalculating repeatedly.

Conclusion

pandas turns error cells into NaN, so finding broken formulas needs openpyxl: load with data_only=True, keep cells whose data_type is 'e', and pair each with its formula from a second load. Recalculate Python-written files first so there are results to scan, search formula text for #REF! to catch deleted references without calculating, and fail the job with a per-sheet summary. Allow only the errors you have decided are legitimate, in the formula itself.

Frequently asked questions

Why does pandas show NaN where Excel shows pandas' openpyxl reader converts every error cell to NaN deliberately. That is convenient for analysis but makes a failed formula indistinguishable from an empty cell. Scan with openpyxl when you need to know about errors.

Why does openpyxl return None instead of the error? The file has no cached results. Workbooks written by openpyxl or pandas contain formulas but no calculated values until a spreadsheet application recalculates and saves them. Recalculate first, for example with LibreOffice in headless mode.

Can I find Often, yes. When a referenced sheet, row or column is deleted, Excel rewrites the reference in the formula text as #REF!, so searching formula strings finds those cells in any saved file, calculated or not.

Should I wrap formulas in IFERROR to get rid of errors? Only where an error is an expected case, such as dividing by a zero count. Wrapping everything hides real problems like missing lookup keys and broken references. Fix the cause and keep IFERROR for the cases you have decided to allow.

More on formulas in workbooks

More on formulas in workbooks →