WolfDocx - migration playbook
Move one python-docx workflow to WolfDocx
List the python-docx calls one workflow uses, map each to its WolfDocx equivalent below, and keep python-docx for the calls marked unsupported. Migrate the narrowest path first and prove the output before you widen it.
Two entry points
DocumentBuilder.open gives python-docx-shaped access: paragraphs, runs, tables, and cell text. A Document session applies guarded edits that refuse stale or ambiguous targets, and one save publishes them. Both are imported from wolfdocx and require WolfDocx Commercial access, described at the end of this page.
Call mapping
| python-docx | WolfDocx |
|---|---|
| Document("in.docx") | DocumentBuilder.open("in.docx") for python-docx-shaped reads and edits, or Document("in.docx") from wolfdocx for a guarded edit session |
| Document() | DocumentBuilder() |
| document.paragraphs, paragraph.text | DocumentBuilder.paragraphs, DocumentParagraph.text (read only) |
| paragraph.runs, run.text (read) | DocumentParagraph.runs, DocumentRun.text |
| run.text = value | Document.set_run_text(paragraph, run, text, expected_old_text=...). Here the paragraph and run indexes start at 0. Paragraphs count only direct body paragraphs, so a paragraph inside a table is not counted, and runs count only direct runs of the paragraph, so a run inside a hyperlink is not counted. The other session methods below count paragraphs from 1. |
| paragraph.text = value | Not supported. DocumentParagraph.text has no setter. Use set_run_text or find_replace. |
| Custom loop to replace text across runs | Document.find_replace(pattern, replacement). The pattern is a regular expression, so wrap literal text in re.escape. A pattern that matches nothing is refused. For reviewed, hash-bound edits see replace text in a Word document. |
| document.add_paragraph(text, style) | DocumentBuilder.write_paragraph(text, style=...) or Document.append_paragraph(text, style=...) |
| paragraph.add_run(text, bold=True) | DocumentParagraph.add_run(text, bold=True) |
| document.add_heading(text, level) | Not supported. No add_heading method. Pass a heading style to write_paragraph(text, style="Heading 1"). |
| document.add_table(rows, cols) | DocumentBuilder.add_table(rows, cols) or DocumentBuilder.insert_table(rows); in a session, Document.insert_table(paragraph, rows, grid_widths), which places the table after paragraph number paragraph, counting from 1 |
| document.tables, table.rows, table.cell(r, c).text | DocumentBuilder.tables, DocumentTable.rows, DocumentTable.cell(r, c).text. Indexes start at 0. |
| cell.text = value | DocumentTableCell.text = value through the builder, or Document.set_cell_text(table=, row=, cell=, text=, expected_old_text=) in a session. Session coordinates start at 1. See update table cells in a Word document. |
| table.add_row() | Document.insert_table_row(table, row, cell_texts=[...]) inserts after the row you name (table and row count from 1). Document.delete_table_row(table, row) removes one. |
| Fill {{placeholder}} text in a template | Document.find_replace for single values. To repeat a placeholder table row once per record, see fill a Word template table. |
| document.add_picture(path, width, height) | DocumentBuilder.append_image(path, width=, height=) with sizes in EMU. There is no add_picture method. |
| document.core_properties.title = value | DocumentBuilder.set_core_properties(title=...) |
| document.save(path) | DocumentBuilder.save(path) (a .docx path) or Document.save(path), which refuses an existing destination and returns a report with passed and changed_parts |
| paragraph.style = value, paragraph.alignment = value | Not supported. Builder paragraphs have no style or alignment property. In a session use Document.format_paragraph(paragraph, style=..., alignment=...), where paragraph counts from 1. |
| run.bold = True, run.font.size = Pt(12), run.font.color.rgb = value | Not supported. Builder runs are read only and have no font object. Pass bold=True, font_size=, or color= to add_run, or call Document.format_text(paragraph, start, end, bold=True) on existing text, where start and end are character offsets. font_size is written to w:sz unchanged, so it counts half-points. |
| from docx.shared import Inches, Pt, RGBColor | There is no wolfdocx.shared. Inches(1) is an integer in EMU, so it works for width= and height= on append_image. Pt and RGBColor have no counterpart. Pass half-point integers to font_size and hex strings such as "FF0000" to color. |
| table.style = value | Not supported. Set the style when you create the table: DocumentBuilder.add_table(rows, cols, style="Table Grid"). |
| table.add_column(width) | Document.insert_table_column(table, column, cell_texts=[...]) inserts after the column you name. |
| paragraph.insert_paragraph_before(text); remove a paragraph through its XML element | Document.insert_paragraph(paragraph, text, position="before") and Document.delete_paragraph(paragraph). Here paragraph counts from 1, unlike set_run_text. |
| section.header.paragraphs[0].text = value | Document.edit_header_footer("header", "create", paragraphs=[{"text": value}]) |
| document.add_page_break() | Not supported. There is no page-break method. DocumentBuilder.add_section() and Document.insert_section_break(after) start a new section on a new page. |
| document.inline_shapes, document.styles | Not supported. DocumentBuilder has neither property. |
Index bases differ by call. Builder paragraphs, runs, and cell(r, c) start at 0, and so do the paragraph and run arguments of set_run_text. Every other session argument that names a paragraph, table, row, cell, column, or section starts at 1.
Verified sample
The sample assumes a template.docx with a {{customer}} placeholder in the first paragraph, a second paragraph whose second run reads Draft, and a table with a header row and two data rows.
import re
from wolfdocx import Document, DocumentBuilder
# Read the way python-docx reads: paragraphs, tables, cell text.
doc = DocumentBuilder.open("template.docx")
print([p.text for p in doc.paragraphs])
print(doc.tables[0].cell(1, 1).text)
# Edit in one guarded session, then save a new file.
with Document("template.docx") as session:
found = session.find_replace(re.escape("{{customer}}"), "Acme Corp")
assert found.status == "applied", found.code
cell = session.set_cell_text(table=1, row=2, cell=2, text="12", expected_old_text="10")
assert cell.status == "applied", cell.code
row = session.insert_table_row(1, 3, cell_texts=["LATAM", "30"])
assert row.status == "applied", row.code
session.set_run_text(1, 1, "Final", expected_old_text="Draft")
report = session.save("report.docx")
assert report.passed, report.code
print(report.changed_parts)When the edits touch only the body, report.changed_parts lists word/document.xml and the other package parts keep their original bytes.
Not supported
- Assigning
paragraph.textorrun.texton builder objects. Useset_run_textorfind_replacein a session. - The python-docx method names
add_heading,add_paragraph,add_picture, andadd_page_breakonDocumentBuilder, and theinline_shapesandstylesproperties. Use the mapped calls above where one exists. - Assigning
style,alignment,bold,font, or a tablestyleon builder objects. The table above names the call to use instead. set_cell_textedits one simple cell. It refuses a cell it cannot edit and reports the reason incode.- WolfDocx does not lay out pages and makes no general Word compatibility promise. See the python-docx alternative overview for when to keep python-docx.
Migration checklist
- List every python-docx call in the workflow and mark each as mapped or unsupported using the table.
- Freeze one representative template and keep the original file unchanged. Write every test result to a new path.
- Replace one code path and keep python-docx for the calls that stay unsupported.
- Assert the intended text and table values by reopening the saved file with
DocumentBuilder.open. - Compare ZIP parts before and after, and check that
changed_partsmatches the parts you meant to edit. - Run the migrated path on a small cohort of real files and keep the old path available for rollback.
Get WolfDocx
WolfDocx Commercial is an organization-scoped subscription. Commercial package access uses the authenticated distribution channel. The package credential is issued on the checkout success page after payment and can be reissued through https://wolfxl.com/recover. There is no public free package.
Review the scoped before-and-after memo files in the reporting walkthrough, or ask about your document before purchasing if compatibility is uncertain.