DOCX: Word Open XML — The Technical Anatomy of the World's Most Common Document Format
DOCX is the file format used by Microsoft Word since 2007 and now the de facto standard for editable documents worldwide. Despite its ubiquity, most people treat DOCX as a black box. Understanding DOCX's internal structure — ZIP archive, XML components, relationship model — unlocks powerful automation capabilities: generating contracts programmatically, parsing documents at scale, building custom converters, and integrating Word files into server-side workflows without Microsoft Office.
What Is DOCX?
DOCX is an implementation of OOXML (Office Open XML), standardized as ISO/IEC 29500 and ECMA-376. The format was introduced in Microsoft Office 2007 as the replacement for the binary .doc format (Compound Document Binary Format). Despite the name "XML", DOCX is not a single XML file — it is a ZIP archive containing a structured hierarchy of XML files, binary assets, and relationship files.
DOCX File Structure: The ZIP Layout
Rename any .docx file to .zip, extract it, and you find:
document.docx (ZIP archive)
├── [Content_Types].xml ← Registry of all parts and their MIME types
├── _rels/
│ └── .rels ← Root relationships (points to word/document.xml)
├── word/
│ ├── document.xml ← Main document body: paragraphs, tables, images
│ ├── styles.xml ← Paragraph/character/table/numbering style definitions
│ ├── settings.xml ← Document settings (compat mode, track changes, etc.)
│ ├── numbering.xml ← List/numbering definitions
│ ├── fontTable.xml ← Font declarations used in document
│ ├── endnotes.xml ← Endnote content
│ ├── footnotes.xml ← Footnote content
│ ├── comments.xml ← Comment content
│ ├── theme/
│ │ └── theme1.xml ← Office theme (colors, fonts, effects)
│ ├── media/
│ │ ├── image1.png ← Embedded images
│ │ └── image2.jpg
│ └── _rels/
│ └── document.xml.rels ← Relationships for document.xml
└── docProps/
├── app.xml ← Application metadata (Word version, page count)
└── core.xml ← Core metadata (author, created, modified, title)
The _rels/*.rels relationship files are the glue of OOXML — every part that references another part does so through a typed relationship rather than a direct file path. This allows parts to be moved or renamed without breaking references.
document.xml: The Document Body
word/document.xml contains the actual content. The structure hierarchy is:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<w:body>
<!-- Paragraph -->
<w:p>
<w:pPr>
<!-- Paragraph properties: style, alignment, spacing, indentation -->
<w:pStyle w:val="Heading1"/>
<w:jc w:val="center"/>
<w:spacing w:before="240" w:after="120"/>
</w:pPr>
<!-- Run (contiguous text with same formatting) -->
<w:r>
<w:rPr>
<!-- Run properties: bold, italic, font, size, color -->
<w:b/> ← Bold
<w:i/> ← Italic
<w:sz w:val="48"/> ← 24pt font (sz is in half-points)
<w:color w:val="1F3864"/> ← Dark blue
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>
</w:rPr>
<w:t>This is a bold italic heading</w:t>
</w:r>
</w:p>
<!-- Table -->
<w:tbl>
<w:tblPr>
<w:tblW w:w="5000" w:type="pct"/> ← 100% width
<w:tblBorders>
<w:insideH w:val="single" w:sz="4" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:color="000000"/>
</w:tblBorders>
</w:tblPr>
<w:tr> ← Table row
<w:tc> ← Table cell
<w:tcPr>
<w:tcW w:w="2500" w:type="pct"/>
<w:shd w:val="clear" w:color="auto" w:fill="D6E4F0"/>
</w:tcPr>
<w:p><w:r><w:t>Cell content</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
<!-- Section properties (page size, margins, columns) — at end of body -->
<w:sectPr>
<w:pgSz w:w="12240" w:h="15840"/> ← Letter: 8.5×11 in at 1440 twips/inch
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
w:header="720" w:footer="720"/>
</w:sectPr>
</w:body>
</w:document>
Key units: OOXML uses twips (twentieth of a point, 1/1440 inch) for most measurements. Font sizes use half-points (24pt = <w:sz w:val="48"/>).
Tracked Changes in DOCX
Tracked changes store revisions in the XML alongside the original content:
<!-- Insertion tracked change -->
<w:ins w:id="1" w:author="John Smith" w:date="2024-01-15T10:30:00Z">
<w:r>
<w:t>inserted text</w:t>
</w:r>
</w:ins>
<!-- Deletion tracked change -->
<w:del w:id="2" w:author="Jane Doe" w:date="2024-01-15T11:00:00Z">
<w:r>
<w:delText>deleted text</w:delText>
</w:r>
</w:del>
<!-- Property change (e.g., bold added) -->
<w:rPrChange w:id="3" w:author="John Smith" w:date="2024-01-15T10:30:00Z">
<w:rPr>
<!-- Previous run properties before the change -->
</w:rPr>
</w:rPrChange>
Python: Reading and Writing DOCX with python-docx
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
import zipfile
import xml.etree.ElementTree as ET
# ── Reading a DOCX ──────────────────────────────
doc = Document('input.docx')
# Iterate paragraphs
for para in doc.paragraphs:
if para.style.name.startswith('Heading'):
print(f"[{para.style.name}] {para.text}")
elif para.text.strip():
print(f" {para.text[:80]}")
# Extract all text
full_text = '\n'.join(p.text for p in doc.paragraphs)
# Read tables
for i, table in enumerate(doc.tables):
print(f"\nTable {i+1}: {len(table.rows)} rows × {len(table.columns)} cols")
for row in table.rows:
print(' |', ' | '.join(cell.text for cell in row.cells), '|')
# Check for tracked changes (requires parsing XML directly)
body = doc.element.body
changes = body.findall('.//' + qn('w:ins')) + body.findall('.//' + qn('w:del'))
print(f"Tracked changes: {len(changes)}")
# ── Creating a DOCX ──────────────────────────────
doc = Document()
# Document margins
from docx.shared import Inches
section = doc.sections[0]
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1.25)
section.right_margin = Inches(1.25)
# Title paragraph
title = doc.add_heading('Project Report — Q1 2024', level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Styled paragraph with multiple runs
para = doc.add_paragraph()
run = para.add_run('Key Finding: ')
run.bold = True
run.font.color.rgb = RGBColor(0x1F, 0x38, 0x64)
run = para.add_run('revenue increased by ')
run = para.add_run('23.4%')
run.bold = True
run.font.color.rgb = RGBColor(0, 128, 0)
run = para.add_run(' year-over-year.')
# Add table
table = doc.add_table(rows=1, cols=3)
table.style = 'Table Grid'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
header_cells = table.rows[0].cells
for i, header in enumerate(['Quarter', 'Revenue', 'Growth']):
header_cells[i].text = header
header_cells[i].paragraphs[0].runs[0].bold = True
data = [('Q1 2024', '$4.2M', '+23.4%'), ('Q4 2023', '$3.4M', '+18.1%')]
for row_data in data:
row = table.add_row()
for i, value in enumerate(row_data):
row.cells[i].text = value
# Add image
doc.add_picture('chart.png', width=Inches(5.5))
# Save
doc.save('output.docx')
print("Created output.docx")
# ── Direct XML manipulation ──────────────────────
def add_page_break(doc):
"""Add a page break between paragraphs."""
para = doc.add_paragraph()
run = para.add_run()
br = OxmlElement('w:br')
br.set(qn('w:type'), 'page')
run._r.append(br)
def set_cell_background(cell, color_hex):
"""Set table cell background color."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), color_hex)
tcPr.append(shd)
# ── Extract raw XML from DOCX ───────────────────
def inspect_docx_xml(docx_path):
"""Read document.xml directly from the ZIP."""
with zipfile.ZipFile(docx_path, 'r') as z:
with z.open('word/document.xml') as f:
tree = ET.parse(f)
return tree.getroot()
Converting DOCX
# DOCX to PDF using LibreOffice (headless)
libreoffice --headless --convert-to pdf input.docx
# DOCX to Markdown using Pandoc
pandoc input.docx -t markdown -o output.md
# DOCX to HTML (preserving images)
pandoc input.docx -t html --extract-media=./images -o output.html
# DOCX to EPUB
pandoc input.docx -o output.epub
# Markdown to DOCX with template
pandoc README.md --reference-doc=template.docx -o output.docx
# DOCX to plain text
pandoc input.docx -t plain -o output.txt
DOCX vs DOC vs ODT vs RTF
| Feature | DOCX | DOC (binary) | ODT | RTF |
|---|---|---|---|---|
| Format | ZIP + XML | Binary | ZIP + XML (ODF) | Plain text |
| Standard | ISO/IEC 29500 | Proprietary (MS) | ISO/IEC 26300 (ODF) | Partial spec |
| Open | Partial | No | Yes | Yes |
| Max size | ~2 GB | ~512 MB | ~2 GB | ~512 MB |
| Track changes | Yes | Yes | Yes | Limited |
| Word compatibility | Native | Native | Good | Good |
| Linux/macOS | Via LibreOffice | Via LibreOffice | Native LibreOffice | Limited |
| Best for | MS Office workflow | Legacy Word | Open source workflow | Cross-app exchange |
Summary
DOCX is fundamentally a ZIP archive of XML files — once you understand this, the format becomes inspectable, automatable, and programmable. The python-docx library makes reading and writing DOCX straightforward for Python developers, while Pandoc handles format conversion to PDF, HTML, Markdown, and EPUB. Direct XML manipulation via python-docx's element API gives access to features not yet wrapped by the high-level API. For server-side document generation at scale, DOCX is the right target format when downstream consumers need Microsoft Word compatibility.
Related conversions
Document conversions that follow this topic naturally: