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

text = body.text                                     # Office JS's body.text: a mark \r per paragraph
print(text.replace("\r", "\n"))                      # split paragraphs with split("\r"), not splitlines()
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

The text is Office JS's, in Word's own characters: body.text ends each paragraph with \r and writes a table's rows as Word does (cells a \t apart, each row ended \r\n); in a paragraph a line break is U+000B and a page break U+000C, and hidden text is left out. Every text writer takes those characters back as the elements they read from.

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.

The text an agent reads is one text model, the port of docx4j's (docx4j_py.model.text): text boxes after their anchors, notes, hidden text left out, the views by resolution. to_markdown and outline() read through it, and so can a tool:

from docx4j_py import load
from docx4j_py.model.text import extract, markdown, plain_text

pkg = load("in.docx")
result = extract(pkg, view="markup")          # the block model, CriticMarkup in this view
plain_text(result)                            # plain text, as the specification writes it
markdown(result, pkg)                         # docx4j-markdown's Markdown of the same reading
[block.to_dict() for block in result.blocks]  # the blocks as JSON

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

Metadata

Release files for docx4j 0.2.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.2.0
File Size Uploaded Provenance Actions
docx4j-0.2.0.tar.gz 7.8 MB With provenance Details for docx4j-0.2.0.tar.gz

Built distribution (wheel)

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

Total release size: 9.2 MB

Release files / docx4j-0.2.0.tar.gz

Download URL docx4j-0.2.0.tar.gz
Size 7.8 MB
Tags Source
SHA-256 checksum
How to use checksums
95f879e1f8804a57559622847db976807446ad6811efd80318030f0c36b7dc7d
BLAKE2b-256 checksum
How to use checksums
ca1825979f4847673b512b72448ad3cd8e4d63e5fa52a249728f4f889b102b93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 25, 2026.

Transparency log

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

Download URL docx4j-0.2.0-py3-none-any.whl
Size 1.5 MB
Tags Python 3
SHA-256 checksum
How to use checksums
47a0f75b74b7cfa1b520546289c6e4cc459cada26e139315ba9301c115ddfe30
BLAKE2b-256 checksum
How to use checksums
a8ed4d4ad8a0d9caba3124034ae80ab4f616ebdd405e0009018a7b0680d9a57f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 25, 2026.

Transparency log

Security for docx4j 0.2.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for docx4j-0.2.0.tar.gz

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 Oct 25, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

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