# python-docx: create and edit Word documents in Python

> Install python-docx, read and edit existing Word files, and replace text while keeping run formatting. Runnable examples, comments, and tracked-change limits.

Maintained by Vespper. Updated 2026-09-27.

Canonical page: https://www.vespper.com/guides/python-docx

Topic: [Python DOCX troubleshooting](https://www.vespper.com/guides#python). [Start the learning path](https://www.vespper.com/guides#start-here).

[`python-docx`](https://python-docx.readthedocs.io/en/stable/) is an open-source Python library for reading, creating, and modifying `.docx` files. Use it to generate a report, inspect paragraphs and tables, or make a known change to an existing Word document.

This guide starts with a small document you can create locally. You'll read it back, replace a deadline, and see why the way you replace text affects its formatting. We'll also cover comments and the extra work involved when an AI agent needs to return Word tracked changes.

## Install python-docx

Use Python 3.9 or newer. In your project's virtual environment, install the package with the same Python interpreter that will run your script:

```bash
python -m pip install "python-docx==1.2.0"
```

The package name is **python-docx**; the Python import is **docx**. These examples were run with version 1.2.0, the [current PyPI release when this guide was published](https://pypi.org/project/python-docx/).

```python
from docx import Document
```

You don't need Microsoft Word installed to run the library. You'll need a document viewer to inspect the resulting layout. The library works on `.docx` files; convert legacy `.doc` files to `.docx` before opening them.

## Create a Word document in Python

Save this as `create_document.py`, then run `python create_document.py`. It creates `proposal.docx` in your current directory:

```python
from docx import Document

document = Document()
title = document.add_paragraph()
title.add_run("Project proposal").bold = True

paragraph = document.add_paragraph("The draft is due on ")
paragraph.add_run("Friday").bold = True
paragraph.add_run(".")

document.add_paragraph("Prepare a draft for review", style="List Bullet")
document.add_paragraph("Collect feedback", style="List Bullet")
document.save("proposal.docx")
```

The deadline paragraph contains three **runs**: the opening text, the bold word “Friday,” and the period. A run holds text and its character formatting. A paragraph can contain many runs, even when it looks like one continuous sentence in Word.

Starting with `Document()` uses the library's default template. To reuse your own document's styles and layout, open it with `Document("template.docx")` instead.

## Read an existing DOCX file

Open the file and iterate over its body paragraphs:

```python
from docx import Document

document = Document("proposal.docx")

for paragraph in document.paragraphs:
    print(paragraph.text)
```

For this example, the output is:

```text
Project proposal
The draft is due on Friday.
Prepare a draft for review
Collect feedback
```

`document.paragraphs` isn't a complete text extractor for every part of a Word file. For example, table cells have their own paragraphs. The [Document API](https://python-docx.readthedocs.io/en/stable/api/document.html) also notes that paragraphs inside revision elements don't appear in this list. Decide which parts of a document your application needs to read before treating extracted text as the whole file.

### Read and update a table

This example adds a small table to the proposal, updates a cell, and reads the rows back:

```python
from docx import Document

document = Document("proposal.docx")
table = document.add_table(rows=2, cols=2)
table.style = "Table Grid"
table.cell(0, 0).text = "Milestone"
table.cell(0, 1).text = "Status"
table.cell(1, 0).text = "Draft"
table.cell(1, 1).text = "Planned"

table.cell(1, 1).text = "In review"

for row in table.rows:
    print([cell.text for cell in row.cells])

document.save("proposal-with-table.docx")
```

For an existing table, select it from `document.tables`. Assigning a cell's `.text` replaces its content; use its paragraphs and runs when you need finer control over text that is already formatted. This example uses ordinary, unmerged cells.

## Replace text without removing run formatting

For a known phrase contained in a plain-text run, edit that run's text. This changes Friday to Monday in the proposal while keeping the deadline bold:

```python
from docx import Document

document = Document("proposal.docx")
replacements = 0

for paragraph in document.paragraphs:
    for run in paragraph.runs:
        if "Friday" in run.text:
            replacements += run.text.count("Friday")
            run.text = run.text.replace("Friday", "Monday")

if replacements != 1:
    raise ValueError(f"Expected one deadline, found {replacements}")

document.save("proposal-run-edit.docx")
```

The count check catches a missing or ambiguous match before saving. Keeping the original and output filenames different also makes it easy to inspect the result against the source.

**Scope of this example:** it visits direct runs in body paragraphs. It doesn't search table cells, headers, footers, hyperlinks, text boxes, or revision markup. Setting `run.text` preserves run formatting but replaces the run's content, so don't apply this blindly to runs containing drawings or fields.

### Why paragraph.text can remove bold and italic

This shorter replacement changes the text, but removes the separate bold run:

```python
from docx import Document

document = Document("proposal.docx")

for paragraph in document.paragraphs:
    if "Friday" in paragraph.text:
        paragraph.text = paragraph.text.replace("Friday", "Monday")

document.save("proposal-paragraph-edit.docx")
```

The [`Paragraph.text` setter](https://python-docx.readthedocs.io/en/stable/api/text.html#docx.text.paragraph.Paragraph.text) replaces the paragraph's content with a single run. Its paragraph style remains, but the original run-level bold and italic formatting is removed. In our sample, Monday is plain text after the paragraph assignment and bold after the run assignment.

| Editing method             | Result                      | Deadline formatting         |
| -------------------------- | --------------------------- | --------------------------- |
| Original document          | The draft is due on Friday. | Friday is bold; three runs. |
| Assign to `paragraph.text` | The draft is due on Monday. | Monday is plain; one run.   |
| Assign to `run.text`       | The draft is due on Monday. | Monday is bold; three runs. |

Reproduce that result with the [complete formatting example](https://www.vespper.com/guides/python-docx/formatting-example.py). After installing `python-docx`, save the script in an empty working directory and run `python formatting-example.py`. It writes three files there; rerunning it replaces those same example files.

You can also inspect the outputs directly: [original proposal](https://www.vespper.com/guides/python-docx/proposal.docx), [paragraph edit](https://www.vespper.com/guides/python-docx/proposal-paragraph-edit.docx), and [run edit](https://www.vespper.com/guides/python-docx/proposal-run-edit.docx). We reopened these files to check their text and run formatting, and rendered each for a visual check. This demonstrates the behavior on this small sample, not on every possible Word document.

### What if the phrase spans several runs?

A per-run replacement won't find a phrase split across runs. You can reproduce that case without loading a file:

```python
from docx import Document

document = Document()
paragraph = document.add_paragraph()
paragraph.add_run("Fri").bold = True
paragraph.add_run("day")

print(paragraph.text)  # Friday
print([run.text for run in paragraph.runs])  # ['Fri', 'day']

for run in paragraph.runs:
    run.text = run.text.replace("Friday", "Monday")

print(paragraph.text)  # Still Friday
```

The full word exists in the paragraph, but neither run contains it. Handling that case means mapping character positions back to runs and deciding which formatting the replacement should inherit. For example, if only “Fri” is bold, should all of “Monday” be bold? That decision belongs in your editing logic.

When your script controls the template, you can arrange placeholders to make replacements predictable. For documents uploaded by users, test split runs and the other document structures your workflow accepts.

## Add a comment with python-docx

Version 1.2.0 includes a public API for adding comments. This anchors a review note to the deadline run:

```python
from docx import Document

document = Document("proposal.docx")
deadline = next(
    run
    for paragraph in document.paragraphs
    for run in paragraph.runs
    if run.text == "Friday"
)

document.add_comment(
    runs=deadline,
    text="Please confirm this deadline before sending.",
    author="Reviewer",
    initials="RV",
)
document.save("proposal-commented.docx")
```

Comments attach to run boundaries. The [comment API reference](https://python-docx.readthedocs.io/en/stable/api/document.html#docx.document.Document.add_comment) explains how a run or sequence of runs defines the commented range. A comment adds a review note; it doesn't turn a text replacement into a tracked insertion and deletion.

## Does python-docx support tracked changes?

**The public API in python-docx 1.2.0 doesn't provide a method that records ordinary text edits as Word tracked changes.** The project's [tracked-changes feature request](https://github.com/python-openxml/python-docx/issues/340) remains open as of this guide's publication.

The replacements above write the new text directly. Returning a redline requires another layer: for example, code that creates the appropriate revision markup, or a document-editing service that produces tracked changes. Writing the XML yourself also means handling revision authorship and checking how your edits interact with existing revisions.

That limitation doesn't mean every save discards all existing document features. The important questions are which structures your code reads, which it changes, and whether the output preserves what your workflow requires. Test representative input documents, including ones that already contain revisions.

## Using python-docx in an AI document-editing agent

`python-docx` is useful when your application knows exactly what operation to perform. An agent can call your own Python functions to update a field, add a paragraph, or fill a table. Your application still needs to choose the target, handle formatting boundaries, and validate the result.

For an instruction such as “update the delivery terms in this proposal and let me review the changes,” the editing layer has more work to do. The agent must locate the relevant passage, make the change in the existing file, and return a usable document for review.

Vespper's **DOCX MCP Server** gives an agent three document tools: `read_document`, `search_document`, and `edit_document`. The agent works with an HTML representation; Vespper applies edits to the DOCX and can return them as Word tracked changes. You can connect it from Python using the [Vespper SDK](https://www.vespper.com/blog/vespper-sdks).

| Your requirement                                                            | Where to start                                                                     |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Generate a report or make a known edit in a controlled template             | `python-docx`, with explicit Python logic for the document structures you use.     |
| Add review comments to selected text                                        | `python-docx` 1.2.0's comment API.                                                 |
| Let an agent edit an existing document through read, search, and edit tools | Vespper DOCX MCP, connected to your agent framework.                               |
| Return the agent's text edits as tracked changes                            | A workflow that creates native revisions, such as Vespper's tracked-edit workflow. |

`python-docx` runs locally and is MIT-licensed. Vespper uses a hosted service and requires a Vespper API key; an AI agent also needs its model-provider configuration. Choose based on the actual editing and review workflow you need to build.

To try the agent workflow, follow the [Python integration in the Vespper quickstart](https://docs.vespper.com/quickstart) with your own DOCX. For a task pane inside Word, see [Build an AI Word add-in with Office.js and Vespper](https://www.vespper.com/guides/build-ai-word-add-in).

## Next steps

- [Handle phrases split across runs](https://www.vespper.com/guides/python-docx-replace-across-runs): Extend a simple replacement to text that crosses formatting boundaries.
- [Choose the tools for your agent](https://www.vespper.com/guides/docx-libraries-for-agents): Compare application-controlled Python operations with templates, conversion, and an MCP editing interface.

## Related guides

- [Preserving DOCX formatting when an AI agent edits a file](https://www.vespper.com/llms/preserve-docx-formatting.md)
- [DOCX tracked changes API: redline a contract with an agent](https://www.vespper.com/llms/tracked-changes-api.md)

