⟳ Automation

Generate Word documents from a template with Python and docxtpl

How to produce letters, quotes and contracts in Word from a template with placeholders, using Python, docxtpl and data taken from an Excel spreadsheet.

Quotes, engagement letters, certificates, minutes: many office documents are always the same apart from a few fields. Filling them in by hand means opening the template, finding the parts to change, replacing them, saving under a new name. With fifty recipients the risk of forgetting a field or leaving the previous client's name in place is high. With Python and the docxtpl library the template remains an ordinary Word file, designed by whoever knows how to use Word, and the script simply fills it in.

Why docxtpl and not just python-docx

python-docx is the base library for reading and writing .docx files: it lets you add paragraphs, tables and images. Building a well-laid-out document entirely in code, however, is laborious, and every layout change means touching the program. docxtpl sits on top of python-docx and adds a placeholder system: you write the template in Word with markers, and the script replaces them with data. The design stays in the hands of the Word user, the logic in the hands of the script.

pip install docxtpl openpyxl

Preparing the template

In the Word document you write placeholders between double curly braces:

To: {{ company_name }}
{{ address }}

Subject: quote no. {{ number }} dated {{ date }}

The placeholder can have any formatting: if {{ company_name }} is bold, the inserted text will be too. A practical tip: type each placeholder in one go, without corrections halfway through. Word sometimes splits a word internally into several formatting fragments, and a split placeholder is not recognised. If that happens, just delete it and type it again from scratch.

The first document

from docxtpl import DocxTemplate

template = DocxTemplate("quote_template.docx")
data = {
    "company_name": "Rossi Ltd",
    "address": "10 Via Roma, Milan",
    "number": "2025-041",
    "date": "12/03/2025",
}
template.render(data)
template.save("quote_2025-041.docx")

The dictionary maps each placeholder name to its value. The names must match those in the template exactly, including upper and lower case.

Tables with a variable number of rows

A quote has a different number of items each time. docxtpl uses the syntax of the Jinja2 engine for loops. In a Word table with a header row you add three rows:

Description Quantity Amount
{%tr for i in items %}
{{ i.description }} {{ i.quantity }} {{ i.amount }}
{%tr endfor %}

The tr prefix tells the library the loop applies to table rows: the rows holding the start and end markers disappear, and the middle one is repeated for each item.

data["items"] = [
    {"description": "Initial analysis", "quantity": 1, "amount": "400.00"},
    {"description": "Configuration", "quantity": 3, "amount": "750.00"},
]

There are also conditions, such as {% if discount %}...{% endif %}, useful for sentences that should appear only in certain cases.

Generating dozens of documents from Excel

The natural next step is reading the data from a spreadsheet, where each row is a recipient and each column a field with the same name as the placeholder.

from openpyxl import load_workbook
from docxtpl import DocxTemplate
from pathlib import Path

sheet = load_workbook("recipients.xlsx", data_only=True).active
rows = list(sheet.iter_rows(values_only=True))
headers = rows[0]
Path("output").mkdir(exist_ok=True)

for row in rows[1:]:
    data = dict(zip(headers, row))
    template = DocxTemplate("letter_template.docx")
    template.render(data)
    template.save(f"output/letter_{data['number']}.docx")

Two important details. The template must be reloaded on every pass, because render modifies the document in memory. And data_only=True makes openpyxl read the result of formulas instead of the formula text.

Formatting numbers and dates

Values read from Excel arrive as Python numbers and dates, not as formatted text. An amount of 1250.5 would appear exactly like that in the document. It is best to prepare the data before render:

def euro(value):
    return f"{value:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")

data["total"] = euro(data["total"])             # 1.250,50
data["date"] = data["date"].strftime("%d/%m/%Y")  # 12/03/2025

The euro function produces the Italian convention, with a dot for thousands and a comma for decimals; for English-style output f"{value:,.2f}" alone is enough. Keeping the formatting in the script, not in the spreadsheet, avoids surprises when someone changes the cell format.

Checks before sending

In my work I always add two checks. The first: compare the list of placeholders in the template with the spreadsheet columns. docxtpl offers the get_undeclared_template_variables() method, which returns the names used in the template: if any is missing from the columns, the script stops before producing documents with empty fields. The second: open two or three generated documents at random, especially the one with the longest texts, to check that the layout holds up.

In short

  • docxtpl fills a Word template with {{ name }} placeholders, leaving the design in Word.
  • Type placeholders without interruptions so Word doesn't split them.
  • Use {%tr for %} for tables with a variable number of rows.
  • Read data from Excel with openpyxl and reload the template for each document.
  • Format numbers and dates in the script and check placeholders before generating.
MM
Marco Marzaduri

Marco Marzaduri works in cybersecurity, digital forensics and web development. A computer science graduate, he combines rigorous technical analysis with attention to the people who use technology.

Read also