python-docx creates and edits Microsoft Word .docx files. pip install python-docx. New: from docx import Document; doc = Document(). Open: doc = Document("existing.docx"). Heading: doc.add_heading("Title", level=1). Paragraph: p = doc.add_paragraph("Hello world"). Bold run: run = p.add_run(" bold"); run.bold = True. Italic: run.italic = True. Font size: from docx.shared import Pt; run.font.size = Pt(14). Color: from docx.shared import RGBColor; run.font.color.rgb = RGBColor(0xFF,0,0). Table: table = doc.add_table(rows=3, cols=4, style="Table Grid"). Cell: table.cell(0,0).text = "Header". Picture: from docx.shared import Inches; doc.add_picture("logo.png", width=Inches(2)). Page break: doc.add_page_break(). List bullet: doc.add_paragraph("Item", style="List Bullet"). List number: doc.add_paragraph("Step", style="List Number"). Style: p.style = doc.styles["Heading 2"]. Paragraph format: p.paragraph_format.space_after = Pt(6). Alignment: from docx.enum.text import WD_ALIGN_PARAGRAPH; p.alignment = WD_ALIGN_PARAGRAPH.CENTER. Header: section = doc.sections[0]; section.header.paragraphs[0].text = "Report". Footer: section.footer.paragraphs[0].text = "Page". Core props: doc.core_properties.author = "Claude". Save: doc.save("output.docx"). Save to bytes: from io import BytesIO; buf = BytesIO(); doc.save(buf). Claude Code generates python-docx reports, invoices, letters, and branded document templates.
CLAUDE.md for python-docx
## python-docx Stack
- Version: python-docx >= 1.1 | pip install python-docx
- Create: doc = Document() | doc = Document("template.docx")
- Content: doc.add_heading(text, level) | doc.add_paragraph(text, style)
- Runs: p.add_run(text); run.bold=True; run.font.size=Pt(12)
- Table: doc.add_table(rows, cols, style="Table Grid"); table.cell(r,c).text = val
- Save: doc.save("out.docx") | doc.save(BytesIO()) for in-memory bytes
python-docx Document Generation Pipeline
# app/docx_gen.py — python-docx headings, tables, formatting, images, and reports
from __future__ import annotations
import io
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from typing import Any
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor
from docx.oxml import OxmlElement
# ─────────────────────────────────────────────────────────────────────────────
# 1. Low-level formatting helpers
# ─────────────────────────────────────────────────────────────────────────────
def set_run_style(
run,
bold: bool = False,
italic: bool = False,
underline: bool = False,
size_pt: float | None = None,
color_rgb: tuple[int, int, int] | None = None,
font_name: str | None = None,
) -> None:
"""Apply formatting to a paragraph Run."""
if bold:
run.bold = bold
if italic:
run.italic = italic
if underline:
run.underline = underline
if size_pt is not None:
run.font.size = Pt(size_pt)
if color_rgb:
run.font.color.rgb = RGBColor(*color_rgb)
if font_name:
run.font.name = font_name
def add_colored_heading(
doc: Document,
text: str,
level: int = 1,
color_rgb: tuple[int, int, int] = (0x1F, 0x49, 0x7D),
) -> None:
"""Add a heading with a custom color override."""
p = doc.add_heading(text, level=level)
for run in p.runs:
run.font.color.rgb = RGBColor(*color_rgb)
def add_paragraph(
doc: Document,
text: str = "",
bold: bool = False,
italic: bool = False,
size_pt: float | None = None,
alignment: str = "left",
space_after_pt: float = 6.0,
style: str | None = None,
) -> Any:
"""Add a paragraph with optional run-level and paragraph-level formatting."""
kw = {}
if style:
kw["style"] = style
p = doc.add_paragraph(**kw)
if text:
run = p.add_run(text)
set_run_style(run, bold=bold, italic=italic, size_pt=size_pt)
align_map = {
"left": WD_ALIGN_PARAGRAPH.LEFT,
"center": WD_ALIGN_PARAGRAPH.CENTER,
"right": WD_ALIGN_PARAGRAPH.RIGHT,
"justify": WD_ALIGN_PARAGRAPH.JUSTIFY,
}
p.alignment = align_map.get(alignment, WD_ALIGN_PARAGRAPH.LEFT)
p.paragraph_format.space_after = Pt(space_after_pt)
return p
def set_cell_shading(cell, fill_color: str = "1F497D") -> None:
"""Set cell background color (hex string without #)."""
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"), fill_color)
tcPr.append(shd)
# ─────────────────────────────────────────────────────────────────────────────
# 2. Table helpers
# ─────────────────────────────────────────────────────────────────────────────
def add_data_table(
doc: Document,
headers: list[str],
rows: list[list[Any]],
col_widths_inches: list[float] | None = None,
header_color: str = "1F497D",
alternate_row_color: str = "DCE6F1",
style: str = "Table Grid",
bold_headers: bool = True,
) -> Any:
"""
Add a formatted data table.
header_color / alternate_row_color: hex strings without #.
Example:
add_data_table(doc,
headers=["Name", "Score", "Grade"],
rows=[["Alice", 95, "A"], ["Bob", 82, "B"]],
col_widths_inches=[2.5, 1.0, 1.0],
)
"""
n_cols = len(headers)
n_rows = len(rows)
table = doc.add_table(rows=1 + n_rows, cols=n_cols, style=style)
# Header row
hdr_cells = table.rows[0].cells
for i, header in enumerate(headers):
hdr_cells[i].text = str(header)
run = hdr_cells[i].paragraphs[0].runs[0]
run.bold = bold_headers
run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
set_cell_shading(hdr_cells[i], header_color)
# Data rows
for row_idx, row_data in enumerate(rows):
row_cells = table.rows[1 + row_idx].cells
for col_idx, val in enumerate(row_data):
row_cells[col_idx].text = str(val)
if alternate_row_color and row_idx % 2 == 1:
set_cell_shading(row_cells[col_idx], alternate_row_color)
# Column widths
if col_widths_inches:
for col_idx, width in enumerate(col_widths_inches):
for cell in table.columns[col_idx].cells:
cell.width = Inches(width)
return table
# ─────────────────────────────────────────────────────────────────────────────
# 3. Header / footer helpers
# ─────────────────────────────────────────────────────────────────────────────
def set_header(
doc: Document,
left_text: str = "",
center_text: str = "",
right_text: str = "",
logo_path: str | Path | None = None,
logo_width_inches: float = 1.0,
) -> None:
"""Set page header with optional left/center/right text and logo."""
section = doc.sections[0]
header = section.header
p = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
p.clear()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
if logo_path and Path(logo_path).exists():
run = p.add_run()
run.add_picture(str(logo_path), width=Inches(logo_width_inches))
p.add_run(" ")
if left_text:
p.add_run(left_text)
if center_text or right_text:
tab_run = p.add_run()
if center_text:
p.add_run(center_text)
if right_text:
p.add_run("\t" + right_text)
def set_footer(
doc: Document,
text: str = "",
include_page_number: bool = True,
) -> None:
"""Set page footer with optional page number field."""
section = doc.sections[0]
footer = section.footer
p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
p.clear()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
if text:
p.add_run(text)
if include_page_number:
p.add_run(" — Page ")
elif include_page_number:
p.add_run("Page ")
if include_page_number:
# Insert PAGE field
fld = OxmlElement("w:fldChar")
fld.set(qn("w:fldCharType"), "begin")
p.runs[-1]._r.append(fld)
instrText = OxmlElement("w:instrText")
instrText.text = " PAGE "
run = p.add_run()
run._r.append(instrText)
fld2 = OxmlElement("w:fldChar")
fld2.set(qn("w:fldCharType"), "end")
run._r.append(fld2)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Report generators
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ReportSection:
title: str
body: str
table_headers: list[str] = field(default_factory=list)
table_rows: list[list[Any]] = field(default_factory=list)
def generate_report(
title: str,
subtitle: str = "",
sections: list[ReportSection] | None = None,
company: str = "My Company",
author: str = "Claude Code",
report_date: date | None = None,
template_path: str | Path | None = None,
) -> bytes:
"""
Generate a formatted Word report and return it as bytes.
Example:
sections = [
ReportSection("Summary", "Q1 performance exceeded targets.",
table_headers=["Metric", "Target", "Actual"],
table_rows=[["Revenue", "$1M", "$1.2M"], ["Churn", "5%", "3.8%"]]),
]
pdf_bytes = generate_report("Q1 Report", company="Acme Corp", sections=sections)
with open("q1_report.docx", "wb") as f: f.write(pdf_bytes)
"""
rpt_date = report_date or date.today()
if template_path and Path(template_path).exists():
doc = Document(str(template_path))
else:
doc = Document()
# Metadata
doc.core_properties.author = author
doc.core_properties.company = company
doc.core_properties.title = title
# Title page
add_colored_heading(doc, title, level=1)
if subtitle:
add_paragraph(doc, subtitle, size_pt=13, alignment="left", italic=True)
add_paragraph(doc, f"{company} · {rpt_date.strftime('%B %d, %Y')}", size_pt=10)
doc.add_page_break()
# Sections
for section in sections or []:
add_colored_heading(doc, section.title, level=2)
if section.body:
add_paragraph(doc, section.body, size_pt=11, space_after_pt=8)
if section.table_headers and section.table_rows:
add_data_table(doc, section.table_headers, section.table_rows)
add_paragraph(doc, "") # spacing after table
set_footer(doc, company, include_page_number=True)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
@dataclass
class InvoiceItem:
description: str
quantity: float
unit_price: float
@property
def total(self) -> float:
return self.quantity * self.unit_price
def generate_invoice(
invoice_number: str,
invoice_date: date,
due_date: date,
client_name: str,
client_address: str,
items: list[InvoiceItem],
company: str = "My Company",
tax_rate: float = 0.10,
notes: str = "",
) -> bytes:
"""
Generate a Word invoice.
Example:
items = [InvoiceItem("API Integration", 40, 150.0),
InvoiceItem("Code Review", 8, 150.0)]
data = generate_invoice("INV-2024-001", date.today(), date.today(), "Acme", "123 St", items)
"""
doc = Document()
doc.core_properties.title = f"Invoice {invoice_number}"
doc.core_properties.author = company
# Header
title_p = doc.add_paragraph()
title_p.alignment = WD_ALIGN_PARAGRAPH.LEFT
run = title_p.add_run(company)
set_run_style(run, bold=True, size_pt=20, color_rgb=(0x1F, 0x49, 0x7D))
add_paragraph(doc, f"Invoice #{invoice_number}", bold=True, size_pt=14)
add_paragraph(doc, f"Date: {invoice_date.strftime('%B %d, %Y')}", size_pt=11)
add_paragraph(doc, f"Due: {due_date.strftime('%B %d, %Y')}", size_pt=11)
add_paragraph(doc, "")
add_paragraph(doc, "Bill To:", bold=True, size_pt=11)
add_paragraph(doc, client_name, size_pt=11)
for line in client_address.split("\n"):
add_paragraph(doc, line, size_pt=11, space_after_pt=2)
add_paragraph(doc, "")
# Line items table
headers = ["Description", "Qty", "Unit Price", "Total"]
rows = [[i.description, i.quantity,
f"${i.unit_price:,.2f}", f"${i.total:,.2f}"] for i in items]
add_data_table(doc, headers, rows, col_widths_inches=[3.2, 0.6, 1.2, 1.2])
add_paragraph(doc, "")
# Totals
subtotal = sum(i.total for i in items)
tax = subtotal * tax_rate
total = subtotal + tax
for label, amount in [
("Subtotal", subtotal),
(f"Tax ({tax_rate*100:.0f}%)", tax),
("Total Due", total),
]:
p = add_paragraph(doc, f"{label}: ${amount:,.2f}",
bold=(label == "Total Due"),
size_pt=11,
alignment="right",
space_after_pt=3)
if notes:
add_paragraph(doc, "")
add_paragraph(doc, "Notes:", bold=True, size_pt=11)
add_paragraph(doc, notes, size_pt=11)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
from datetime import date
print("=== Report generation ===")
sections = [
ReportSection(
title="Executive Summary",
body="Q1 revenue exceeded targets by 20%. Customer retention improved significantly.",
table_headers=["Metric", "Q1 Target", "Q1 Actual", "Delta"],
table_rows=[
["Revenue", "$1,000,000", "$1,200,000", "+20%"],
["Churn Rate", "5.0%", "3.8%", "-1.2pp"],
["NPS Score", "45", "52", "+7"],
],
),
ReportSection(
title="Product Updates",
body="Three major features shipped: API v2, mobile app redesign, batch processing.",
),
]
report_bytes = generate_report(
"Q1 2024 Business Review",
subtitle="Confidential — Internal Distribution Only",
sections=sections,
company="Acme Corp",
author="Finance Team",
)
Path("/tmp/q1_report.docx").write_bytes(report_bytes)
print(f" Report: /tmp/q1_report.docx ({len(report_bytes):,} bytes)")
print("\n=== Invoice generation ===")
items = [
InvoiceItem("API Integration Development", 40, 150.00),
InvoiceItem("Code Review and Testing", 8, 150.00),
InvoiceItem("Deployment and Documentation", 4, 100.00),
]
invoice_bytes = generate_invoice(
"INV-2024-042",
date(2024, 4, 1),
date(2024, 4, 30),
"Acme Corp",
"123 Business Ave\nSan Francisco, CA 94102",
items,
company="Claude Code Agency",
notes="Net 30. Wire transfer or ACH preferred.",
)
Path("/tmp/invoice.docx").write_bytes(invoice_bytes)
print(f" Invoice: /tmp/invoice.docx ({len(invoice_bytes):,} bytes)")
subtotal = sum(i.total for i in items)
print(f" Total: ${subtotal * 1.1:,.2f}")
For the reportlab / weasyprint alternative — reportlab and weasyprint generate PDF output; python-docx generates editable Word .docx files that clients can modify, add comments to, and submit through workflows that expect Word format — use python-docx when deliverables need to remain editable in Microsoft Word, WeasyPrint/fpdf2 when you need a fixed, printable PDF. For the openpyxl / xlsxwriter alternative — openpyxl and xlsxwriter target Excel .xlsx files with spreadsheet structures; python-docx targets Word .docx files with flowing text, headings, and formatted paragraphs — use python-docx for reports, letters, contracts, and invoices; Excel writers for data-heavy tabular output. The Claude Skills 360 bundle includes python-docx skill sets covering set_run_style() for bold/italic/size/color, add_colored_heading() with RGB override, add_paragraph() with alignment and spacing, set_cell_shading() OxmlElement helper, add_data_table() with header color and alternating rows, set_header()/set_footer() with page number field, generate_report() with sections and metadata, generate_invoice() with line items and tax, and BytesIO in-memory save. Start with the free tier to try Word document generation code generation.