{"slug": "openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell", "title": "Openpyxl silently drops cached formula values when you edit an unrelated cell", "summary": "A test by an unnamed developer found that openpyxl, a Python library for reading and writing Excel files, silently drops cached formula values when editing an unrelated cell, while Kookerella.FsOpenXmlDsl preserves them. In the test, appending a row below a total cell left the formula intact but emptied the cached value, causing data_only=True readers like pandas.read_excel to return None instead of 450. The bug occurs because openpyxl's Cell class has only one value slot, so loading a workbook in formula mode discards the cached number.", "body_md": "# The Silent Bug: How an AI Agent Can Quietly Blank Out Your Excel Formulas\n\nHere’s a bug that doesn’t look like a bug. Ask an AI agent to add one line to an existing\nreport. It does. The file opens fine in Excel - Excel recalculates every formula the moment\nit opens a workbook, so nothing looks wrong. But hand that same file to a *second* automated\nsystem - a PDF export, a dashboard pipeline, another script reading it with `pandas`\n\n- and a\nnumber that used to be there is now blank, or zero, or `None`\n\n. Nobody touched that cell.\n\nI ran a real, minimal test to find out exactly when and why this happens.\n\n## The setup\n\nA tiny report: three line items and a `Total`\n\ncell holding `=SUM(B2:B4)`\n\n, saved with its\ncorrect cached value (`450`\n\n) already baked in - exactly what a real `.xlsx`\n\nlooks like after\nsomeone has opened and saved it in Excel at least once.\n\n```\nItem      Amount\nWidgets   100\nGadgets   150\nGizmos    200\nTotal     =SUM(B2:B4)   [cached: 450]\n```\n\nThe raw XML for that cell confirms it, before anything is touched:\n\n```\n<c r=\"B5\"><f>SUM(B2:B4)</f><v>450</v></c>\n```\n\nBoth `<f>`\n\n(the formula) and `<v>`\n\n(the last-calculated value) are there, as siblings - this\nis what makes a `.xlsx`\n\nformula cell readable two different ways: recalculate it yourself, or\njust read `<v>`\n\nif you don’t have a formula engine at all.\n\n## The edit\n\nA completely unrelated, deliberately harmless change: append one new row *below* the total.\nNothing about the formula, its range, or the cells it reads is touched.\n\n**Arm A (openpyxl):**\n\n``` python\nimport openpyxl\nwb = openpyxl.load_workbook(\"formula_report.xlsx\")\nws = wb[\"Report\"]\nws.append([\"Note\", \"Reviewed\"])\nwb.save(\"formula_arm_a_edited.xlsx\")\n```\n\n**Arm B (Kookerella.FsOpenXmlDsl):**\n\n``` js\nlet wb = Workbook.load \"formula_report.xlsx\"\nlet sheet = wb.Sheets |> List.find (fun s -> s.Name = \"Report\")\nlet editedSheet = { sheet with Cells = sheet.Cells @ [ /* the new row's cells */ ] }\nWorkbook.save \"formula_arm_b_edited.xlsx\" { wb with Sheets = [ editedSheet ] }\n```\n\n## What’s objectively in each result\n\nThe same `B5`\n\ncell, in the same file, after the same category of edit:\n\n```\nOriginal:            <c r=\"B5\"><f>SUM(B2:B4)</f><v>450</v></c>\nArm A (openpyxl):     <c r=\"B5\"><f>SUM(B2:B4)</f><v />       </c>\nArm B (Kookerella):   <c r=\"B5\"><f>SUM(B2:B4)</f><v>450</v></c>\n```\n\n`<v>`\n\nis still there in Arm A’s result - it’s just empty. The formula survived. The number\ndidn’t.\n\n## Why this is worse than it looks\n\nExcel itself won’t show this. Open either file in real Excel and every formula recalculates\non load - `450`\n\nreappears, and you’d never know anything happened. The bug only surfaces when\nsomething reads the file *without* recalculating it. That’s not a rare edge case anymore -\nit’s exactly what a second automated consumer does:\n\n```\nwb = openpyxl.load_workbook(\"formula_arm_a_edited.xlsx\", data_only=True)\nwb[\"Report\"][\"B5\"].value\n# None\n```\n\n`data_only=True`\n\nis the mode that reads whatever’s cached instead of the formula text - the\nsame mechanism `pandas.read_excel`\n\nuses under the hood. Feed this file into a second script,\na dashboard, or any headless pipeline, and the total is silently `None`\n\nwhere it used to be\n`450`\n\n. No exception, no warning - just a missing number propagating into whatever runs next.\n\nDoing the identical edit through Kookerella.FsOpenXmlDsl leaves `B5`\n\nbyte-for-byte intact:\n`data_only=True`\n\nstill reads back `450`\n\n.\n\n## Why this happens\n\nNot a guess - openpyxl’s own `Cell`\n\nclass only has one value slot:\n\n``` python\ndef _bind_value(self, value):\n    ...\n    self._value = value\n```\n\nThere’s no separate field for “the formula” and “its last cached result” at the same time.\nWhen you load a workbook normally (`data_only=False`\n\n- the mode you need if you want formulas\nto stay editable rather than frozen as numbers), a formula cell’s `.value`\n\nbecomes the formula\n*text*. The cached number is never read into memory at all in that mode - `data_only=True`\n\nis\na separate, mutually exclusive way of loading the same file that substitutes the cached value\nin place of the formula. So the moment you open a workbook the normal way to edit it, the\ncached value has nowhere to live - it’s gone before your edit even happens, and saving just\nwrites back what’s left: the formula, with an empty `<v>`\n\n.\n\nThis isn’t an openpyxl bug exactly - it’s a structural consequence of a cell model built around “one value per cell,” which is a reasonable design for a library that never evaluates formulas itself. It just means every save silently costs you the one thing a headless reader downstream actually depends on.\n\n## Why this matters more now than it used to\n\nThe old safety net was: someone opens this in real Excel eventually, and Excel fixes it. That assumption gets weaker every time more of a pipeline is agent-to-agent rather than human-in-the-loop - one AI produces a report, a second automated system consumes it, and nobody opens Excel in between. That’s the exact shape of workflow AI agents are increasingly used for, and it’s exactly the shape this bug is invisible inside.\n\n## Try it\n\n`Kookerella.FsOpenXmlDsl`\n\nmodels a formula cell as `Formula(expression, cachedValue: float option)`\n\nexplicitly, precisely because of this failure mode - see its own README for the\nreasoning. The MCP server built on it is [Kookerella.FsOpenXmlDsl.Mcp](/products/fsopenxmldsl-mcp/):\n\n```\ndotnet tool install -g Kookerella.FsOpenXmlDsl.Mcp\n```\n\nDon’t want a .NET dependency at all? Download a standalone build for your platform from the\n[latest release](https://github.com/Kookerella-Ltd/Kookerella.FsOpenXmlDsl/releases/tag/v0.6.3)\ninstead - the runtime is bundled into the executable, so it’s unzip and run, no install\nrequired.\n\n```\n{\n  \"mcpServers\": {\n    \"fsopenxmldsl\": {\n      \"command\": \"fsopenxmldsl-mcp\"\n    }\n  }\n}\n```\n\nIt’s usable entirely through JSON via its `create_workbook_from_json`\n\n/`generate_json`\n\ntools\ntoo - no .NET required on the calling side. See the [product page](/products/fsopenxmldsl-mcp/)\nfor the full tool list.", "url": "https://wpnews.pro/news/openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell", "canonical_source": "https://kookerella.com/posts/ai-agent-silently-breaks-formula-values/", "published_at": "2026-08-30 13:05:41+00:00", "updated_at": "2026-08-30 13:21:45.830084+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["openpyxl", "Kookerella.FsOpenXmlDsl", "pandas"], "alternates": {"html": "https://wpnews.pro/news/openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell", "markdown": "https://wpnews.pro/news/openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell.md", "text": "https://wpnews.pro/news/openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell.txt", "jsonld": "https://wpnews.pro/news/openpyxl-silently-drops-cached-formula-values-when-you-edit-an-unrelated-cell.jsonld"}}