Every company eventually needs to produce a Word document from code: an invoice, a compliance report, a contract with merged fields. The task sounds trivial until the first client opens the file in Word and sees broken tables, mangled fonts, or a document that refuses to open at all because the OOXML is technically valid but practically wrong.

The three libraries that dominate this space in 2026 are python-docx (Python, 5,746 stars), docx4j (Java, 2,387 stars) and PHPWord (PHP, 7,588 stars). All three are actively maintained, all three write real .docx files, and all three fail in completely different ways. This guide is code-first: every snippet below is runnable, every version number was pulled from the registry at publish time, and every limitation is one I hit in production.

TL;DR — The 30-Second Verdict

Pick python-docx if you generate reports from Python data, need results in minutes, and can live with the built-in styles. It has the cleanest API of the three and the smallest dependency surface.

Pick docx4j if you must manipulate existing .docx files at the XML level — merge documents, apply complex field codes, handle content controls, or convert Office formats in a JVM shop. It is the only one of the three with genuine OOXML round-tripping power, and it is also the only one that will make you read the ECMA-376 spec.

Pick PHPWord if your stack is PHP and you need template filling (TemplateProcessor), which is PHPWord’s killer feature and better than anything the other two ship out of the box.

Do not pick any of them for PDF output. None of the three renders documents. They write OOXML; a converter (LibreOffice headless, or a commercial engine) must do the rendering step.

Side-by-Side Comparison

Live data pulled from GitHub and the package registries on 2026-10-21:

Dimensionpython-docxdocx4jPHPWord
Latest release1.2.011.5.31.4.0
GitHub stars5,7462,3877,588
Last commit2026-08-012026-10-202026-10-08
LanguagePython 3.9+Java 11+PHP 8.1+
LicenseMITApache-2.0LGPL-3.0
Installpip install python-docxMavencomposer require phpoffice/phpword
Dependency weightlxml onlyJAXB + ~6 jars~4 Composer packages
Create from scratchExcellentGood (verbose)Good
Edit existing filesLimitedBest in classGood
Template fillingManualManual (content controls)Built-in TemplateProcessor
OOXML-level accessEscape hatch via oxmlNative, full object modelPartial
Read .docxYesYesYes
Render to PDFNoLegacy XSL-FO path onlyNo
Learning curveLowVery highLow–medium

Decision Matrix: Match the Tool to the Job

Your situationUse thisWhy
Generate 10,000 monthly statements from a Python pipelinepython-docxFast to write, low memory per document, MIT license
Fill a legal template with 40 named fieldsPHPWordTemplateProcessor does this in ~10 lines
Merge two .docx files and keep tracked changesdocx4jOnly library with real OOXML merge semantics
Read tables out of an uploaded .docxpython-docxdoc.tables is straightforward for well-formed files
Convert Word to PDF inside a Java servicedocx4j + LibreOfficedocx4j writes, LibreOffice renders
Legacy PHP app on PHP 8.1 with no ComposerPHPWordInstalls with Composer, runs without extensions beyond zip/xml
Need pixel-perfect corporate brandingnone of theseUse HTML + a rendering engine instead

python-docx 1.2.0 — The Pragmatic Default

python-docx wraps OOXML in Python objects that map almost one-to-one to Word’s UI concepts: Document, Paragraph, Run, Table. If you have ever used Word, the object model is obvious. That is its whole value proposition.

1
pip install python-docx==1.2.0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

doc = Document()
doc.add_heading("Quarterly Report", level=1)

p = doc.add_paragraph("Revenue grew ")
p.add_run("18%").bold = True
p.add_run(" year over year.")
p.alignment = WD_ALIGN_PARAGRAPH.LEFT

table = doc.add_table(rows=3, cols=3)
table.style = "Table Grid"
header = table.rows[0].cells
header[0].text, header[1].text, header[2].text = "Region", "Q1", "Q2"
rows = [("EMEA", "1.2M", "1.4M"), ("APAC", "0.9M", "1.1M")]
for i, (region, q1, q2) in enumerate(rows, start=1):
    cells = table.rows[i].cells
    cells[0].text, cells[1].text, cells[2].text = region, q1, q2

doc.save("quarterly-report.docx")

Where it shines: reading and writing structured content. Extraction is equally terse:

1
2
3
4
doc = Document("uploaded.docx")
for table in doc.tables:
    for row in table.rows[1:]:
        print([c.text.strip() for c in row.cells])

Where it hurts: styling. python-docx cannot create new styles — it can only apply styles that already exist in the document or in its default template. Header/footer page numbers require dropping into docx.oxml and hand-writing fldChar elements. And footnotes are not supported at all. The moment a designer hands you a brand template with custom paragraph styles, you either pre-load that template as your starting document or you start writing XML by hand.

That XML escape hatch is real and useful:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def add_page_number(paragraph):
    run = paragraph.add_run()
    for instr in ("begin", None, "end"):
        if instr:
            fld = OxmlElement("w:fldChar")
            fld.set(qn("w:fldCharType"), instr)
        else:
            fld = OxmlElement("w:instrText")
            fld.set(qn("xml:space"), "preserve")
            fld.text = "PAGE"
        run._r.append(fld)

It works. It is also the point where a “simple report generator” becomes a maintenance liability.

docx4j 11.5.3 — The Spec-Accurate Power Tool

docx4j is not a convenience wrapper; it is a JAXB binding of the entire WordprocessingML schema plus a pile of utilities on top. Anything Word can store in a .docx, docx4j can address as a Java object, because the object is the XML element.

1
2
3
4
5
<dependency>
  <groupId>org.docx4j</groupId>
  <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
  <version>11.5.3</version>
</dependency>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.Text;

WordprocessingMLPackage pkg = WordprocessingMLPackage.createPackage();
ObjectFactory factory = org.docx4j.jaxb.Context.getWmlObjectFactory();

P para = factory.createP();
Text text = factory.createText();
text.setValue("Generated by docx4j 11.5.3");
para.getContent().add(factory.createR().getValue());
para.getContent().set(0, factory.createR());
((org.docx4j.wml.R) para.getContent().get(0)).getContent().add(text);

pkg.getMainDocumentPart().getContent().add(para);
pkg.save(new java.io.File("out.docx"));

Yes, that is what “add a paragraph” costs you in docx4j. The verbosity is the price of fidelity: the same package lets you walk into a document, find every table cell containing a placeholder, and replace the run while preserving the surrounding run properties — something python-docx cannot do without losing formatting.

The genuinely unique capability is document assembly. WordprocessingMLPackage instances can be merged, sections copied, styles imported:

1
2
3
4
5
WordprocessingMLPackage target = WordprocessingMLPackage.load(new java.io.File("master.docx"));
WordprocessingMLPackage source = WordprocessingMLPackage.load(new java.io.File("appendix.docx"));
target.getMainDocumentPart().getContent().addAll(
    source.getMainDocumentPart().getContent());
target.save(new java.io.File("merged.docx"));

Two warnings. First, merging without also importing the source’s styles and numbering definitions produces documents that look fine in LibreOffice and wrong in Word — you must handle styles.xml and numbering.xml explicitly. Second, PDF export: the old docx4j-export-fo path (XSL-FO to PDF) has been deprecated for years and produces mediocre output for modern layouts. In 2026 the sane architecture is docx4j for the file, LibreOffice headless for the render.

JVM startup cost is the other tax: a Spring Boot service using docx4j typically carries 200–300 MB of heap headroom, versus python-docx’s ~40 MB per document in a worker process.

PHPWord 1.4.0 — Best Template Engine of the Three

PHPWord occupies the same conceptual space as python-docx — sections, elements, styles — with one feature that neither competitor matches: TemplateProcessor.

1
composer require phpoffice/phpword:^1.4
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
require 'vendor/autoload.php';

use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\IOFactory;
use PhpOffice\PhpWord\SimpleType\Jc;

$phpWord = new PhpWord();
$section = $phpWord->addSection();
$section->addTitle('Quarterly Report', 1);
$section->addText(
    'Revenue grew 18% year over year.',
    ['bold' => true, 'size' => 12, 'name' => 'Calibri'],
    ['alignment' => Jc::LEFT]
);

$table = $section->addTable(['borderSize' => 6, 'borderColor' => '999999']);
$table->addRow();
$table->addCell(2000)->addText('Region');
$table->addCell(2000)->addText('Q1');
$table->addCell(2000)->addText('Q2');
foreach ([['EMEA', '1.2M', '1.4M'], ['APAC', '0.9M', '1.1M']] as $row) {
    $table->addRow();
    foreach ($row as $value) {
        $table->addCell(2000)->addText($value);
    }
}

IOFactory::createWriter($phpWord, 'Word2007')->save('report.docx');

The template workflow is where PHPWord earns its keep. You author contract.docx in Word with ${client_name} placeholders, then:

1
2
3
4
5
6
7
8
$template = new \PhpOffice\PhpWord\TemplateProcessor('contract.docx');
$template->setValue('client_name', 'Acme GmbH');
$template->setValue('effective_date', '2026-11-01');
$template->cloneRow('line_item', 3);
$template->setValue('line_item#1', 'Consulting — 40h');
$template->setValue('line_item#2', 'Migration — 12h');
$template->setValue('line_item#3', 'Training — 6h');
$template->saveAs('contract-acme.docx');

cloneRow alone justifies the choice for anyone generating invoices or contracts: it duplicates a table row N times and rewrites the indexed placeholders, preserving the original formatting. Doing the same in python-docx means deep-copying CT_Row elements and renumbering runs yourself.

Trade-offs: PHPWord is LGPL-3.0, which matters if you link it into a closed-source product — dynamic linking via Composer is generally acceptable, but have counsel look at it. Memory usage also degrades on very large documents because the whole model is held in PHP objects; 500-page documents with heavy tables will hit your memory_limit.

Pitfalls That Will Bite You

1. OOXML validity is not fidelity. All three libraries can produce a file that opens without error and still looks wrong, because the missing piece is a style or numbering definition, not content. Always test in real Word, not only in LibreOffice.

2. Fonts are a deployment problem, not a library problem. A .docx stores font names, not glyphs. If Calibri is absent on the rendering host (a slim Docker image, a Linux CI runner), your PDF conversion silently substitutes and your line breaks shift. Install ttf-mscorefonts-installer or ship the brand fonts in the image.

3. TemplateProcessor placeholders break on Word autocorrect. Word sometimes splits ${client_name} across multiple runs when the user edits a template. The placeholder is visually intact and programmatically gone. Disable autocorrect while authoring, and re-save templates from Word rather than Google Docs.

4. Never build paths from user input. docx4j and PHPWord both happily load remote or arbitrary files. Zip-based formats are also a decompression-bomb vector: cap file size and entry count before parsing untrusted uploads.

5. Concurrency. python-docx objects are not thread-safe; one Document per worker. PHPWord’s static style registry leaks state between documents in long-running workers (Swoole, RoadRunner) — reset it or fork per job.

6. Version pinning matters more than usual. These libraries chase evolving OOXML quirks. Pin exact versions; python-docx 1.x changed table and style APIs versus 0.8.x, and PHPWord 1.x dropped several 0.18-era methods.

FAQ

Which of these three is fastest for generating many documents?

python-docx, in practice. Per-document cost is dominated by XML serialization, and Python’s lxml is fast. A worker process generating a 20-page report typically finishes in 100–250 ms. docx4j is comparable per document but pays a fixed JVM warm-up of a few seconds, so batch throughput is fine while cold-start latency is not. PHPWord is roughly 2–4× slower than python-docx on table-heavy documents and uses more memory per document.

Can python-docx edit an existing Word file without destroying its formatting?

Partly. Replacing the text of an existing run keeps its formatting; replacing whole paragraphs or inserting content usually loses surrounding properties, because python-docx has no concept of style inheritance across inserted elements. If you need surgical edits inside complex documents, docx4j is the correct tool.

Do I need Microsoft Word installed on the server?

No, for all three. They write OOXML directly. You only need Word (or LibreOffice) if you want to render to PDF, recalculate fields, or update a table of contents — those are application-level operations the libraries do not perform.

Is docx4j’s Apache-2.0 license safe for a commercial SaaS?

Yes. Apache-2.0 is permissive and includes an explicit patent grant, so embedding docx4j in a commercial service is fine. The historical caveat is that some docx4j add-ons (notably the PDF converter) are separately licensed by Plutext; check which artifacts you actually depend on.

Can I generate .docx without any library, using raw XML?

Yes — a minimal .docx is a ZIP containing [Content_Types].xml, _rels/.rels, and word/document.xml. It is a fine exercise and a terrible production strategy: you will reimplement styles, numbering, tables and relationships, and every Word version will find a new way to reject your output.

What about LibreOffice’s UNO API instead of these libraries?

It works and gives you full rendering plus PDF export, but it requires a running LibreOffice instance, is notoriously unstable under concurrency, and turns a 40 MB worker into a 400 MB one. Use it as a renderer behind a queue, not as your document authoring API.

If you are building a document pipeline, these guides cover the adjacent pieces: spreadsheet generation libraries compared for the XLSX side, self-hosted PDF generation engines for the rendering step, and document automation platforms if you would rather template than code.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com