Clause 4.3 of a Word contract holds another reviewer's tracked change. After a python-docx 1.2.0 rewrite it reads interest at, a blank, per month: w:ins and w:del go from 1 to 0 and no error is raised. paper-docx 0.2.1 refuses the edit with UnsupportedStructureError.

paper-docx vs python-docx openpyxl python-pptx AI edits: which library keeps an Office file intact?

An agent edits a customer's contract, model or deck, the file opens fine, and something in it is gone. We ran the same three edits through both library families to see which saves keep the file whole.

paper-docx vs python-docx openpyxl python-pptx AI edits: which keeps the file intact?#

For Excel, swapping openpyxl for paper-xlsx alone kept the sparkline; for Word and PowerPoint, only the forks' guarded APIs kept tracked changes, formatting and every occurrence. The paper-docx README calls it "an import-compatible hard fork of python-docx." So the swap is cheap to try.

But in our 17 October 2026 test, the Word and PowerPoint results under the swap matched upstream exactly. So the protection sits in new calls, not in the package name. Also, neither family wrote a fresh formula result into the saved workbook. So an agent that reads a value back after an edit needs a calculation step either way.

What survived one agent edit, per format and library path. Source: Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package.

FileUpstream codeFork, same codeFork's guarded API
Word4 runs collapsed to 1, another reviewer's tracked change deletedIdentical to python-docxTracked replace, still bold; unclear edits refused by name
Excel18 package parts touched, sparkline lost2 parts touched, sparkline kept2 parts touched, sparkline kept, receipt lists each removed cache
PowerPoint2 of 4 FY25 occurrences changedIdentical to python-pptx4 of 4 occurrences changed

What does a silently damaged file cost a team running an agent?#

A save that deletes another reviewer's tracked change leaves a contract that opens cleanly and says something nobody agreed, and no error is raised. Because nothing fails, nothing alerts anyone, so someone has to diff every edited file by hand.

A refusal-first edit step replaces that work with a check of the refusals and receipts the library returns. For example, in our 17 October 2026 test the python-docx rewrite of clause 4.3 hit that case. It took tracked insertions and deletions from 1 each to 0. Under paper-docx, an edit across that pending change was refused. Then the source file stayed byte-identical.

DiagramAn agent edit with and without a refusal-first library. Source: Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package.

How was the test run, and what can it not tell you?#

Each format got the same edits run idiomatically, as a drop-in fork swap, and through the guarded API, with every result measured from the saved package rather than in Office. First, on 17 October 2026 we tested openpyxl 3.1.5, python-docx 1.2.0 and python-pptx 1.0.2 in one Python environment. Then the forks paper-xlsx 0.2.2, paper-docx 0.2.1 and paper-pptx 0.2.1 ran in another, because they share the upstream import names.

Results come from counting XML parts and elements in each saved package, so they show what the file holds, not how Word, Excel or PowerPoint renders or repairs it. Every Office file is a zip archive of XML parts, in a format called Office Open XML. But no copy of Microsoft Office or LibreOffice ran during the test.

The fixtures came from neither family. For example, xlsxwriter, a separate Python writer, built the Excel model. It held 13 formulas, a chart, a sparkline, a table and a note. Also, the Word contract split one clause across 4 runs. And the deck for the 17 October 2026 test held the year label FY25 in a title, a split run and a table cell.

Library versions and fixtures in the test. Source: Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package.

FileUpstream libraryForkFixture
Wordpython-docx 1.2.0paper-docx 0.2.1A contract with one clause split across 4 runs and another reviewer's tracked change
Excelopenpyxl 3.1.5paper-xlsx 0.2.2A model built by xlsxwriter: 13 formulas, a chart, a sparkline, a table and a note
PowerPointpython-pptx 1.0.2paper-pptx 0.2.1A deck with FY25 in a title, a split run and a table cell

What happens to a Word contract when python-docx rewrites a clause?#

Rewriting a split clause through paragraph.text collapsed 4 runs into 1, dropped the bold, and deleted another reviewer's tracked change without a warning. In our 17 October 2026 test, those two losses came from two clauses. A run is a stretch of text with one format, and clause 4.2 spread "thirty calendar days" over 4 runs.

But clause 4.3 is the worse case. When a paragraph holds a tracked insertion, python-docx's paragraph.text leaves that text out. So in the same 17 October 2026 test, clause 4.3 read "interest at per month", with the inserted rate missing. Then writing the text back removed the revision and left a clause with no rate at all.

Show data table
Runs in clause 4.2 and tracked revisions in the saved word/document.xml, before and after each edit path. The python-docx rewrite of clause 4.3 took tracked insertions and deletions from 1 each to 0. Source: Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package.
Dimension Before the edit After the save
Runs, python-docx 4 count1 count
Runs, paper-docx 4 count6 count
Insertions, paper-docx 1 count4 count
Deletions, paper-docx 1 count3 count

python-docx collapsed clause 4.2 to 1 run, while the guarded edits added tracked revisions.

Figure Runs in clause 4.2 and tracked revisions in the saved word/document.xml, before and after each edit path. The python-docx rewrite of clause 4.3 took tracked insertions and deletions from 1 each to 0. Source: Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package. Atyantik Technologies hands-on test, 17 October 2026

paper-docx's additions guide says to "Use tracked=True with author=... to author a new w:ins/w:del redline". Those are Word's tracked insertion and deletion tags. In our test, that call made "thirty" a tracked deletion and "forty-five" a tracked insertion, still bold. Also, three other edits in that 17 October 2026 test were refused by name: UnsupportedStructureError, TargetNotFoundError and AmbiguousTargetError.

What does openpyxl drop from an Excel model that paper-xlsx keeps?#

Changing one input cell made openpyxl remove, add or rewrite 18 package parts and delete the sparkline, while paper-xlsx rewrote 2 parts and kept it. In our 17 October 2026 test, openpyxl removed 3 parts, added 2 and rewrote 13. And it warned that the sparkline extension "is not supported and will be removed".

Show data table
Package parts removed, added or rewritten by one save, one input cell changed from 5% to 7%, Atyantik Technologies hands-on test, 17 October 2026
Item Value
openpyxl 3.1.5 18
paper-xlsx 0.2.2, same code 2
paper-xlsx 0.2.2, save with receipt 2

The same save touched 18 parts under openpyxl and 2 under paper-xlsx.

Figure Package parts removed, added or rewritten by one save, one input cell changed from 5% to 7%, Atyantik Technologies hands-on test, 17 October 2026 Atyantik Technologies hands-on test, 17 October 2026

The openpyxl tutorial gives the cause: "openpyxl does currently not read all possible items in an Excel file so" shapes are lost on save. In short, it rebuilds the package from what it understands. However, the paper-xlsx preserve mode page describes the opposite design. There, a save "patches the parts owned by supported edits and copies everything else unchanged". That held with unchanged code in our 17 October 2026 test, so the swap alone helped here.

Does either library recalculate formulas after an agent changes an input?#

Neither recalculates: all 13 cached formula values were gone after the save under both libraries, so an agent reading the result cell back got None. In our 17 October 2026 test, result cell B8 held 1215.50625 before the edit. After the save, it read as None through data_only=True on every path.

Formulas carrying a cached value13 down to 0

13

Before the save

0

After the save, on every path

A read with data_only=True returned None until something recalculated the file.

Counted in xl/worksheets/sheet1.xml under openpyxl 3.1.5 and paper-xlsx 0.2.2, with and without a receipt.

Formulas carrying a cached value (formulas carrying a cached value, of 13)
Optionformulas carrying a cached value, of 13
Before the save13
After the save, on every path0

Source: Atyantik Technologies hands-on test, 17 October 2026, method above

The openpyxl tutorial says data_only returns "the value stored the last time Excel read the sheet". After a save without Excel, no such value exists. Meanwhile, the paper-xlsx page is explicit about this. Its save "removes affected cached formula results and requests a full recalculation on open". But only its receipt listed each removed cache in our 17 October 2026 test, while openpyxl said nothing.

Why did the python-pptx run loop miss half the FY25 occurrences?#

A run-by-run replace changed 2 of 4 slide occurrences, missing the split run and the table cell, while paper-pptx's replace_text changed all 4. In our 17 October 2026 test, one textbox held "FY2" and "5" in two runs. So no single run ever contained FY25.

Show data table
Slide occurrences of FY25 changed, out of 4. Source: Atyantik Technologies hands-on test, 17 October 2026
Item Value
python-pptx 1.0.2, run loop 2
paper-pptx 0.2.1, same code 2
paper-pptx 0.2.1, replace_text 4

Only replace_text reached the split run and the table cell.

Figure Slide occurrences of FY25 changed, out of 4. Source: Atyantik Technologies hands-on test, 17 October 2026 Atyantik Technologies hands-on test, 17 October 2026

But assigning the whole paragraph's text loses formatting instead. A reply in python-pptx issue 285, opened 9 May 2017, calls Paragraph.text "a convenience property". Then it adds: "There is no general-case way to change the text while preserving the formatting". Instead, the paper-pptx guide says replace_text works "while preserving each run's formatting". Still, neither path touched the chart series name or the speaker notes in our test.

Is swapping the package enough, or does the code have to change?#

Swapping the package protected Excel content only; the Word and PowerPoint results were identical to upstream until the code called the forks' new APIs. In our 17 October 2026 test, paper-docx and paper-pptx under unchanged code repeated every upstream loss.

Try it
File type
Library path

The same edit, run three ways in the 17 October 2026 test.

4 → 1
1 → 0

python-docx 1.2.0: something did not survive the save

Lost

Edit made through paragraph.text rewrite.

Lost or missed

  • Clause 4.2 collapsed from 4 runs to 1 and the bold was dropped
  • Clause 4.3 lost the other reviewer's tracked change and now has no rate at all

Every file type and library path in the 17 October 2026 test
FilePathLibraryWhat the saved file showed
WordUpstream codepython-docx 1.2.04 runs collapsed to 1, the other reviewer's tracked change gone, no error raised
WordFork, same codepaper-docx 0.2.1identical to python-docx
WordFork's guarded APIpaper-docx 0.2.1tracked replace kept the bold and the redline; three unclear edits refused by name
ExcelUpstream codeopenpyxl 3.1.518 parts touched, sparkline deleted, every cached result gone
ExcelFork, same codepaper-xlsx 0.2.22 parts touched, sparkline kept, every cached result gone
ExcelFork's guarded APIpaper-xlsx 0.2.22 parts touched, sparkline kept, the receipt lists each removed cache
PowerPointUpstream codepython-pptx 1.0.22 of 4 changed; the split run and the table cell missed
PowerPointFork, same codepaper-pptx 0.2.1identical to python-pptx
PowerPointFork's guarded APIpaper-pptx 0.2.14 of 4 changed; the chart series name and the notes untouched
Pick a file type and a library path to see what survived the save. Atyantik Technologies hands-on test, 17 October 2026, measured from the saved package, not opened in Microsoft Office.

So the decision splits by file type. For Excel, swap the package, then add the receipt to see what each save changed. For Word and PowerPoint, rewrite the edit step around find, replace and refusal handling.

Which pages do you build an agent's edit step from?#

Build from the paper-docx additions guide for tracked edits, the paper-xlsx preserve mode page for receipts, and the paper-pptx additions guide for replace_text. Take them in this order:

  1. Page 1

    paper-docx additions guide: find_one, Span.replace with tracked=True, and PaperRefusal.

  2. Page 2

    paper-xlsx preserve mode page: save with receipt=True and the formula freshness rules.

  3. Page 3

    paper-pptx additions guide: replace_text and its refusals.

  4. Page 4

    openpyxl tutorial: the data_only flag for reading results.

The four pages: paper-docx additions guide, paper-xlsx preserve mode page, paper-pptx additions guide, openpyxl tutorial.

Also, the python-docx documentation covers the rest, since the fork keeps its object model.

What is the smallest guarded edit an agent loop can call?#

The smallest guarded edit finds one exact span, replaces it as a tracked change, and returns any refusal to the calling loop instead of guessing. Because the import name stays docx, it runs on paper-docx installed in place of python-docx.

python
from docx import Document
from docx.search import find_one
from docx.errors import PaperRefusal
def guarded_edit(src, dst, old, new, author="Contract agent"):
    doc = Document(src)  # Step 1: load the existing document
    try:
        span = find_one(doc, old)  # Step 2: exactly one match, or a refusal
        span.replace(new, tracked=True, author=author)  # Step 3: tracked change
    except PaperRefusal as refusal:  # Step 4: return the refusal, save nothing
        return {"ok": False, "refused": f"{type(refusal).__name__}: {refusal}"}
    doc.save(dst)
    return {"ok": True}
print(guarded_edit("contract.docx", "contract-edited.docx",
                   "thirty calendar days", "forty-five calendar days"))

For Excel, the paper-xlsx page shows the same shape: receipt = wb.save("output.xlsx", receipt=True). Then a loop reads the receipt, not the stale workbook values.

When are the paper forks the wrong tool?#

Stay on python-docx, openpyxl and python-pptx when the job only generates new files, because the guards protect existing content and the forks are young projects. Paper Instruments' repositories date paper-docx 0.2.1, paper-pptx 0.2.1 and paper-xlsx 0.2.2 to 23 September 2026. And a fresh document has no tracked change to lose.

Also, a pipeline that must read recalculated results needs Excel, LibreOffice or a formula engine, whichever library writes the file. For that, the paper-xlsx oracle module "uses headless LibreOffice". Finally, some teams cannot run a separate environment for the forks. For them, upstream plus a manual review of each file is the honest fallback.

When to stay upstream, and what to use instead. Source: Paper Instruments repositories, 23 September 2026; paper-xlsx preserve mode page.

CaseUse instead
The job only generates new filespython-docx, openpyxl and python-pptx
The pipeline must read recalculated resultsExcel, LibreOffice or a formula engine, whichever library writes the file
The team cannot run a separate environment for the forksUpstream plus a manual review of each file

Where to go from here#

Questions this post answers

Is paper-docx a drop-in replacement for python-docx?
Yes for imports, since paper-docx keeps the docx import name and object model. But in our 17 October 2026 test, unchanged python-docx code produced the same losses under the fork. The protection comes from find_one and Span.replace with tracked=True.
Does openpyxl recalculate formulas when it saves a workbook?
No, and in our 17 October 2026 test, openpyxl and paper-xlsx both removed all 13 cached formula results on save. So a read with data_only=True returned None until something recalculated the file.
Which is safer for AI agent edits, paper-docx or python-docx, openpyxl and python-pptx?
For editing existing files, the forks' guarded APIs were safer in our 17 October 2026 test. Instead of guessing, they refused unclear edits and kept tracked changes. For generating new files, the upstream libraries are mature and enough.

Keep reading