A Python alternative to Word mail-merge (that handles conditional paragraphs and file naming)

Mail-merge cannot do 'show this paragraph only if there is a bonus' or 'name each file after the person'. A 60-line Python pattern can.

Word mail-merge is fine for one letter with 40 names. It breaks on the two things office work actually needs:

Both are trivial in Python and impossible in mail-merge without macros.

The pattern

from docx import Document
import re

def render(template, row):
    def sub(m):
        kind, field, body = m.group(1), m.group(2), m.group(3)
        has = str(row.get(field, "")).strip() not in ("", "None")
        if kind == "#": return body if has else ""
        if kind == "^": return "" if has else body
    # conditional blocks first, repeatedly for nesting
    text = re.sub(r"\{\{([#^])([A-Za-z0-9_]+)\}\}(.*?)\{\{/\2\}\}",
                  sub, template, flags=re.S)
    # then plain values
    return re.sub(r"\{\{([A-Za-z0-9_]+)\}\}",
                  lambda m: str(row.get(m.group(1), "")), text)

doc = Document("tpl.docx")
for p in doc.paragraphs:                      # also loop tables / headers / footers
    full = "".join(r.text for r in p.runs)
    if "{{" in full:
        new = render(full, row)
        p.runs[0].text = new                  # keeps the first run's formatting
        for r in p.runs[1:]: r.text = ""
doc.save(f"out/{row['name']}-letter.docx")

Two details that matter

Runs split text. Word stores a paragraph as a list of runs with different formatting, so {{na and me}} can be two runs. Join the paragraph text first, replace, then write the result back into the first run — otherwise your placeholder never matches.

Formatting the values is the product. 13943 is not an offer letter; 13,943.00 is. Same for 2026-07-22 versus 22 July 2026. Build a small filter syntax ({{salary|money}}, {{start|date:%d %B %Y}}) rather than formatting in the data — the template is what people edit.

Encoding: the silent killer

Excel exports Chinese CSVs as GBK or adds a UTF-8 BOM. Read defensively: utf-8-sig first, then gbk. If you skip this, half your documents contain ??? and you find out after sending 200 of them.

Packaged version

DocFiller implements all of the above — filters (upper/title/money/date:/pad:), conditional sections, images, per-row filenames with subfolders, optional PDF conversion, and a _manifest.csv audit trail. Demo template and data included, 29 USDT.

DocFiller

One Word template + one spreadsheet = hundreds of finished documents. No mail-merge.

Get it — 29 USDT