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 VespperUpdated Read as Markdown
On this page

python-docx 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:

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.

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:

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:

from docx import Document

document = Document("proposal.docx")

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

For this example, the output is:

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 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:

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:

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:

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 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 methodResultDeadline formatting
Original documentThe draft is due on Friday.Friday is bold; three runs.
Assign to paragraph.textThe draft is due on Monday.Monday is plain; one run.
Assign to run.textThe draft is due on Monday.Monday is bold; three runs.

Reproduce that result with the complete formatting example. 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, paragraph edit, and run edit. 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:

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:

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 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 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.

Your requirementWhere to start
Generate a report or make a known edit in a controlled templatepython-docx, with explicit Python logic for the document structures you use.
Add review comments to selected textpython-docx 1.2.0's comment API.
Let an agent edit an existing document through read, search, and edit toolsVespper DOCX MCP, connected to your agent framework.
Return the agent's text edits as tracked changesA 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 with your own DOCX. For a task pane inside Word, see Build an AI Word add-in with Office.js and Vespper.