# DOCX Authoring

> Creating Word documents programmatically with python-docx — headings, styles, tables, lists, images, and sections. Use when generating DOCX reports, proposals, contracts, letters, or any structured Word document from data or templates.

- Skill: `projectious-work/docx-authoring` (Agent Skill)
- Install (CLI): `npx skillmds add projectious-work/docx-authoring`
- Raw SKILL.md: https://api.skillmd.com/api/skills/projectious-work/docx-authoring/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: projectious-work (https://skillmd.com/u/projectious-work)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/projectious-work/docx-authoring

---


# DOCX Authoring

## Intro

Use `python-docx` to generate Word documents programmatically. The
library manipulates the underlying Office Open XML directly through a
Python API. Structure documents with paragraph styles, not ad-hoc
formatting — styles make documents consistent and editable in Word
after generation.

## Overview

### Setup

```python
# /// script
# dependencies = ["python-docx"]
# ///
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH

doc = Document()           # blank document
# doc = Document("template.docx")  # or start from a template
```

Install with `uv add python-docx` or `pip install python-docx`.

### Headings and paragraphs

```python
doc.add_heading("Document Title", level=0)   # Title style
doc.add_heading("Section 1", level=1)         # Heading 1
doc.add_heading("Subsection", level=2)        # Heading 2

p = doc.add_paragraph("Body text here.")
p = doc.add_paragraph("Intro sentence. ", style="Body Text")
run = p.add_run("Bold phrase.")
run.bold = True
run.font.size = Pt(12)
```

Level 0 maps to the built-in "Title" style; levels 1-9 map to
"Heading 1" through "Heading 9". Always use heading levels for
structural hierarchy — never bold a paragraph to fake a heading.

### Paragraph styles

Prefer named styles over inline formatting. Built-in style names
(case-sensitive): `"Normal"`, `"Body Text"`, `"List Bullet"`,
`"List Number"`, `"Quote"`, `"Intense Quote"`, `"Caption"`.

```python
p = doc.add_paragraph("Quoted passage.", style="Quote")
p = doc.add_paragraph("First bullet point.", style="List Bullet")
doc.add_paragraph("Second bullet point.", style="List Bullet")
```

To define a custom style, either embed it in a template DOCX or
create it programmatically via `doc.styles.add_style()`.

### Tables

```python
table = doc.add_table(rows=1, cols=3, style="Table Grid")
hdr = table.rows[0].cells
hdr[0].text = "Name"
hdr[1].text = "Value"
hdr[2].text = "Unit"

row = table.add_row().cells
row[0].text = "Latency p99"
row[1].text = "42"
row[2].text = "ms"

# Merge cells
table.rows[0].cells[0].merge(table.rows[0].cells[1])
```

Use table styles ("Table Grid", "Light Shading", "Medium List 1") for
consistent visual treatment. Access cell paragraph properties via
`cell.paragraphs[0].paragraph_format`.

### Images and sections

```python
doc.add_picture("chart.png", width=Inches(5.0))

# Add a page break
doc.add_page_break()

# Section properties (portrait → landscape)
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
section = doc.sections[-1]
section.orientation = WD_ORIENT.LANDSCAPE
section.page_width, section.page_height = section.page_height, section.page_width
```

### Headers, footers, and metadata

```python
section = doc.sections[0]
header = section.header
header.paragraphs[0].text = "Company Confidential"

footer = section.footer
p = footer.paragraphs[0]
p.text = "Page "
p.add_run().add_field("PAGE")   # auto-numbered page field via XML injection

doc.core_properties.author = "Generated by Agent"
doc.core_properties.title = "Quarterly Report"
```

### Saving

```python
doc.save("output.docx")
```

Generate to a `BytesIO` buffer to avoid writing to disk:
```python
from io import BytesIO
buf = BytesIO()
doc.save(buf)
buf.seek(0)
```

### Document structure checklist

Before saving, verify:
1. Title and all heading levels are set via styles, not bold/font-size
2. Tables have a named style (never `style=None`)
3. Images have explicit widths (Word's default sizing is unreliable)
4. `core_properties.title` and `core_properties.author` are set
5. The file opens correctly in Word and LibreOffice

## Gotchas

Agent-specific failure modes — provider-neutral pause-and-self-check items:

- **Applying bold/font-size directly instead of using paragraph styles.** A paragraph made bold with `run.bold = True` looks like a heading but is not — it breaks the document outline, navigation pane, and PDF export. Use `doc.add_heading(text, level=N)` or a named paragraph style for every structural element.
- **Starting from a blank `Document()` when the output must match a corporate template.** A blank document uses Word's default styles, which rarely match a client's brand. When the target is a templated document (letterhead, report cover), always open the template with `Document("template.docx")` and add content into it.
- **Not specifying image width.** `doc.add_picture("chart.png")` without `width=Inches(...)` inserts at the image's native resolution, which varies wildly. Always supply an explicit `width` or `height` so the image fits predictably within the page margins.
- **Generating tables with `style=None`.** Unstyled tables render with no borders and no shading, making them hard to read. Always pass a named table style — at minimum `"Table Grid"` for a simple bordered table.
- **Changing page orientation without swapping width and height.** Setting `section.orientation = WD_ORIENT.LANDSCAPE` alone does not resize the page — it just marks the orientation flag. You must also swap `section.page_width` and `section.page_height` for the layout to render correctly.
- **Saving to the same path the document was opened from without a copy.** `doc.save("template.docx")` overwrites the template used as the base, destroying it for the next run. Always save to a distinct output path, or load the template from a path separate from the output.
- **Not verifying the output opens in Word and LibreOffice.** `python-docx` can produce XML that is technically valid but triggers repair prompts or rendering differences in specific Word versions. Always open the generated file in at least one consumer before shipping.

## Full reference

### Page margins and layout

```python
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)
```

### Cell formatting

```python
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def set_cell_background(cell, hex_color):
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    shd = OxmlElement("w:shd")
    shd.set(qn("w:fill"), hex_color)
    shd.set(qn("w:val"), "clear")
    tcPr.append(shd)

set_cell_background(table.rows[0].cells[0], "4472C4")
```

### Numbered lists with custom start

```python
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

p = doc.add_paragraph("Step one.", style="List Number")
# To restart numbering or set start: manipulate numPr XML directly
```

### Hyperlinks

python-docx does not have a native hyperlink API. Inject via XML:

```python
from docx.opc.constants import RELATIONSHIP_TYPE as RT

def add_hyperlink(paragraph, text, url):
    part = paragraph.part
    r_id = part.relate_to(url, RT.HYPERLINK, is_external=True)
    hyperlink = OxmlElement("w:hyperlink")
    hyperlink.set(qn("r:id"), r_id)
    run = OxmlElement("w:r")
    rPr = OxmlElement("w:rPr")
    rStyle = OxmlElement("w:rStyle")
    rStyle.set(qn("w:val"), "Hyperlink")
    rPr.append(rStyle)
    run.append(rPr)
    t = OxmlElement("w:t")
    t.text = text
    run.append(t)
    hyperlink.append(run)
    paragraph._p.append(hyperlink)
    return hyperlink
```

### Conversion to PDF

python-docx cannot produce PDF directly. Options:
- `libreoffice --headless --convert-to pdf output.docx` (requires LibreOffice)
- `docx2pdf` library (requires Word on macOS/Windows)
- Upload to Google Docs API and export as PDF

