Skip to main content

docx4j for Python

docx4j for Python: the whole of ECMA-376 WordprocessingML as typed objects, generated from docx4j's own schema tree; docx4j's Open Packaging engine written by hand over them (.docx in, typed parts and relationships, .docx out, every part you did not touch written back byte for byte); a content API in the vocabulary of Office JS's Word.Body, Paragraph, Range, Table, comments, tracked changes and lists, built for agents; and a python-docx facade so code already written runs. The classes carry docx4j's names (P, R, PPr, Tbl, Document, Styles), so fifteen years of docx4j documentation and examples read across; the design is the same as docx4j-core-ts.

pip install docx4j

The distribution is docx4j; the import name is docx4j_py, as python-docx imports as docx. Python 3.12 or later.

Open, edit, save

from docx4j_py import load

pkg = load("in.docx")                                # a WordprocessingMLPackage
body = pkg.body                                      # Word.Body over word/document.xml

print(body.text)                                     # a paragraph per line
for paragraph in body.paragraphs:                    # tables and content controls descended into
    print(paragraph.style, paragraph.text)

title = body.insert_paragraph("Report", location="Start", style="Heading 1")
title.alignment = "Centered"
hit = body.search("quick brown fox")[0]              # matches span runs freely
hit.font.italic = True                               # the runs are split at the boundaries
hit.insert_text("slow red fox")                      # Replace is a Range's default location
body.paragraphs[-1].insert_paragraph("The end.")     # After is a Paragraph's

pkg.save("out.docx")                                 # only the parts you touched are re-marshalled

A part that is never touched is written back byte for byte; reading a part's contents --- which pkg.body does for word/document.xml --- marks it for re-marshalling, and a re-marshalled part is canonically identical to its source. Nothing is dropped silently: what lenient parsing could not place is reported per part (part.skipped), and LoadOptions(strict=True) raises instead.

From nothing:

from docx4j_py import create_package

pkg = create_package()                       # docx4j's createPackage: A4, one section, the default styles
body = pkg.body
body.insert_paragraph("Created by docx4j", style="Heading 1")
paragraph = body.insert_paragraph("One paragraph, three runs")
paragraph.search("three runs")[0].font.italic = True
table = body.insert_table(3, 2, values=[["Name", "Value"], ["a", "1"]], style="TableGrid")
table.header_row_count = 1
table.add_rows(1, values=[["b", "2"]])
pkg.save("hello.docx")

For agents

An agent cannot hold a Python object across tool calls, and a 200-page document does not fit in its context window. So the API gives it addresses --- strings that survive an edit --- and budgets on everything it reads. Four calls are the loop: read the outline, find the text, edit by address, check the report.

from docx4j_py import load

pkg = load("in.docx")

outline = pkg.outline()                       # small, structured, enough to choose an address from
outline.to_markdown()                         # the cheapest thing to show a model
outline.to_json()                             # under 64 KB for a 200-page document

hits = pkg.find("quick brown fox")            # matches with their addresses and 40 characters either side
hits[0].address, hits[0].snippet              # ('w14:5A2B1C3D', '… over the quick brown fox, which …')

paragraph = pkg.paragraph_at(hits[0].address) # 'w14:5A2B1C3D', 'body/3', or contains='Chapter 1'
paragraph.insert_paragraph("Added by an agent.", location="After")

pkg.last_change.to_json()                     # what that call did, for the tool result
pkg.save("out.docx")

w14:5A2B1C3D is the w14:paraId Word writes and survives every edit; body/3 is the ordinal. Every mutating call records a ChangeReport; pkg.dry_run() applies calls to a copy and throws it away; describe() reads what the document offers (its styles, page, parts, authors) without unmarshalling anything; every error carries a code and a hint naming what to do instead.

An agent should leave the trail Word already has: tracked changes for what it did, and a comment for why, where the human who opens the document will see them.

from docx4j_py import load
from docx4j_py.model.content import Author

pkg = load("in.docx")
pkg.author = Author("Claude", initials="C", email="claude@example.com")
pkg.change_tracking_mode = "TrackAll"          # every edit from here is a revision

count = pkg.body.replace_text("document", "report")     # a w:del and a w:ins per hit
pkg.find("report")[0].range(pkg.body).insert_comment(
    "Changed 'document' to 'report': the brief asks for a report."
)
[c.to_dict() for c in pkg.get_tracked_changes()]        # for the tool result
pkg.body.to_markdown(view="markup")                     # {--deleted--}{++inserted++}{>>a comment<<}
pkg.save("out.docx")

Markdown goes both ways: pkg.to_markdown(addresses=True) puts each block's address in an HTML comment before it, and body.insert_markdown(text) writes CommonMark plus GFM tables through the document's own styles, creating the numbering part when there is none.

python-docx code runs

docx4j_py.docx is a structural subset of python-docx's public API --- python-docx's names and semantics, this engine underneath. Its promise is a committed member list derived from python-docx 1.2.0: 305 of 312 members, the rest refused with a reason each. python-docx is not a dependency.

from docx4j_py.docx import Document, Pt

doc = Document("in.docx")                             # or Document() for a new one
doc.add_heading("Report", level=1)                    # a built-in style it lacks is defined
p = doc.add_paragraph("Plain, ")
p.add_run("bold").bold = True
p.runs[1].font.size = Pt(14)
doc.add_table(rows=2, cols=2, style="Table Grid").cell(0, 0).text = "a"
for paragraph in doc.paragraphs:
    print(paragraph.style.name, paragraph.text)

print(doc.package.last_change.to_dict())              # what python-docx does not have: a ChangeReport,
doc.save("out.docx")                                  # parts untouched byte for byte, the typed tree

The object model and the engine

Every element has a constructor in its namespace's el module, with p, r, t and tbl as sugar; wml(...) parses a fragment with docx4j's prefixes declared; to_xml is the inverse. The tree stays reachable from every view (paragraph.element is the typed P).

from docx4j_py.wml import el, p, r, to_xml, wml

para = p("Hello ", r("World", bold=True), style="Heading1")
same = wml('<w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>Hello</w:t></w:r></w:p>')
print(to_xml(el.p(content=[el.r(content=[el.t("Hello World")])]), pretty=True))

Under docx4j_py.openpackaging: OpcPackage, WordprocessingMLPackage, PartName, ContentTypeManager, RelationshipsPart, the typed WordprocessingML parts, PartStore and PartSink (zip, directory, memory, flat OPC); docx4j_py.model has docx4j's PropertyResolver, StyleUtil, numbering emulator and font selection, with zero differences on docx4j's 45 parity goldens.

from docx4j_py import load
from docx4j_py.openpackaging import LoadOptions, MemoryPartSink
from docx4j_py.wml import warm_up

warm_up()                                             # a server does this once, before the first request
with load("in.docx", options=LoadOptions(strict=True)) as pkg:
    settings = pkg.get_part("/word/settings.xml").contents   # typed, lazily, on first access
    pkg.save_to(MemoryPartSink())

What is and is not in 0.1

WordprocessingML only. .pptx and .xlsx load and save through the generic OPC path (every part byte for byte), but nothing in them is typed: PresentationML and SpreadsheetML are CR-001 Phase D (the object model) and CR-002 Phase C (the engine), and their content APIs are CR-004 and CR-005. Everything is synchronous.

The wheel is large for pure Python --- about 1 MB compressed, 238 modules, 6 MB installed --- because it is the whole of WordprocessingML and what it embeds (DrawingML, OMML, VML, the w14 to w16 extensions) as typed classes, 148,000 lines of generated code. Importing docx4j_py imports all of it, about 0.6 s once per process; a long-running process is the intended host.

What 0.x promises, surface by surface:

Surface Stability in 0.x
docx4j_py.load, create_package, WordprocessingMLPackage, the parts, PartStore / PartSink Stable: docx4j's names and behaviour; departures are recorded in CR-002 section 12.3
The content API (Body, Paragraph, Range, Table, ..., the agent surface, markdown) Stable in shape: Office JS's vocabulary is borrowed, not invented; a member may be added in a minor, none renamed
docx4j_py.docx, the python-docx facade Stable: its promise is the committed member list, 305 of 312
The generated model (docx4j_py.wml and the rest, el) Regenerates with the schema; class and field names are docx4j's and do not move, but a schema refresh adds fields and classes in a minor
docx4j_py.model (resolver, StyleUtil, fonts, numbering) Stable: docx4j's, zero differences on the goldens

Apache-2.0, as docx4j is; the generated model imports docx4j-xsdata, a fork of xsdata (MIT). Every fence above is executed by the test suite (tests/test_readme.py).

Release files for docx4j 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for docx4j 0.1.0
File Size Uploaded
docx4j-0.1.0.tar.gz 3.5 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for docx4j 0.1.0
File Interpreter ABI Platform
docx4j-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 4.5 MB

Release files / docx4j-0.1.0.tar.gz

Download URL docx4j-0.1.0.tar.gz
Size 3.5 MB
Tags Source
SHA-256 checksum
How to use checksums
1af13e522dd0b3e7dcc748f64fe4c032c2eb4d86c63768b54a5777e5c6bdf99e
BLAKE2b-256 checksum
How to use checksums
cdf5065728a112924c011e6e398ea31e22222aa6dd8f72bce0b00a8741400e53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release files / docx4j-0.1.0-py3-none-any.whl

Download URL docx4j-0.1.0-py3-none-any.whl
Size 1.0 MB
Tags Python 3
SHA-256 checksum
How to use checksums
e7b9efe969b984f5def1b546ca0c02738a1d67a0dd2ab8e7740cab545aeb1dc3
BLAKE2b-256 checksum
How to use checksums
24060112fb80bf8a8a0a2b94aa267169948b2238775f480b2fe28e7e9b6d466e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page