Find and replace text in DOCX tables with Python

Find text in table-cell paragraphs, including nested tables, without treating merged cells as separate values or overwriting the whole cell.

Maintained by VespperUpdated Read as Markdown
On this page

document.paragraphs does not walk into every table cell. A replacement loop that only visits body paragraphs can finish successfully while leaving all the text in tables unchanged. Traverse table cells explicitly, and inspect the paragraphs inside each cell.

Before changing content, separate traversal from replacement. First print the paragraphs your code will visit. Then apply a replacement function with a known scope, such as the text-run example.

Traverse cells and nested tables

This example for python-docx 1.2.0 visits direct body paragraphs, then table-cell paragraphs. It does not preserve the reading order between body paragraphs and tables.

from docx import Document

def paragraphs_in(container, seen_cells=None):
    if seen_cells is None:
        seen_cells = set()
    yield from container.paragraphs
    for table in container.tables:
        for row in table.rows:
            for cell in row.cells:
                # A merged cell can appear at several grid positions.
                if cell._tc in seen_cells:
                    continue
                seen_cells.add(cell._tc)
                yield from paragraphs_in(cell, seen_cells)

doc = Document()
table = doc.add_table(rows=1, cols=2)
table.cell(0, 0).text = "Client: Example Co"
table.cell(0, 1).text = "Status: Draft"
assert [p.text for p in paragraphs_in(doc)] == [
    "Client: Example Co", "Status: Draft"
]

_tc is a private XML attribute. This example uses its element identity to avoid visiting the same merged cell repeatedly; pin your dependency and test against your fixtures. Repeated merged-cell output is a longstanding source of confusion.

Replace content at the right level

Assigning cell.text replaces the cell's existing contents with a new paragraph and run. That is appropriate when creating a plain table from scratch. It is unsuitable when an existing cell contains several paragraphs, bold labels, hyperlinks, or an embedded table that must survive.

A phrase can still span several runs inside a cell. Finding the right cell does not solve the formatting problem. Count matches before saving, and use row labels and column headings to distinguish repeated values such as “Pending.”

Scope an agent's table edit

For an agent, “change the delivery date for Phase 2” is a better target than “replace the second occurrence of Friday.” Read the surrounding table, resolve the row and column, and retain its structure when constructing the edit. With Vespper, use the exact current HTML returned by reading or searching; do not rebuild the whole table from a plain-text extraction.

This traversal does not include headers, footers, text boxes, or revision-contained paragraphs. Test those separately if they are part of your input contract.

Preserve merged-cell structure

Several grid positions can refer to one logical merged cell. Deduplicating those positions prevents repeated writes, but does not reconstruct the complete visual geometry. The python-docx table guide covers merged, omitted, and nested cells.

For a text update, retain the table, rows, cells, and merge structure. Rebuilding a table from extracted strings can lose widths, borders, alignment, or nested content. With Vespper, retain the current returned HTML structure and classes. If candidate cells are identical, use distinguishing adjacent context; a page number alone does not make an identical anchor unique.

Verify a horizontal merge, a vertical merge, repeated labels, and a nested table. Check that the target changes once and unrelated cells keep their content, then inspect widths, merges, and page-spanning behavior in the target viewer. Correct text alone cannot establish correct table layout.