Skip to content

Rich text and hyperlinks

A plain string in the context becomes plain text in the document, formatted like the tag it replaced. When the formatting has to come from the data — a status in red, a phrase in bold in the middle of a sentence, a link — the value becomes a Text object and the tag gets an r prefix.

from docxtpl import Template, Text

tpl = Template("report.docx")

status = Text("Overdue", bold=True, color="cc0000")
status.add(" — payment was due 2026-07-01", italic=True)

tpl.render({"status": status})
tpl.save("report-acme.docx")

In the template:

Account status: {{r status }}

Why the r prefix

{{r status }} is scoped to the run it sits in: the run is replaced by the runs the Text renders to. Without the prefix the object would be substituted into the middle of a w:t, where the schema allows only characters.

Put the tag in a run of its own. A tag sharing a run with other text takes that text with it — the run is what gets replaced.

Formatting

Every argument is optional and applies to the segment it is given with.

Argument Effect
bold, italic, strike the obvious
subscript, superscript vertical alignment
underline True, or a style name such as "double" or "wave"
color six hex digits, no #
highlight a Word highlight name, such as "yellow"
size half-points, as the schema counts them: size=28 is 14pt
font a typeface name
style the id of a character style defined in the document

style is a character style, and its id

A run can carry a character style and nothing else. Text("x", style="Heading1") names a paragraph style, and Word silently ignores it — the document opens, renders, and is simply unstyled.

So it is refused rather than accepted, with the answer in the message:

StyleError: style 'Heading1' is a paragraph style, and a run can only carry
a character style, so Word would ignore it. Word pairs it with the character
style 'Heading1Char', which a run can carry. Found in document body: 'Note: x'

A style that does not exist is refused too, as is a style name where an id was wanted — "Heading 1" and "Heading1" differ by a space that is easy to spend an afternoon on, so the message names the id to use.

Only styles a render introduces are checked. A w:rStyle already in the template is left exactly as it was.

add() returns the object, so segments chain:

note = Text("Total: ").add("€1,240.00", bold=True).add(" (incl. VAT)", size=18)

Fonts for mixed scripts

One typeface name is not enough for text that mixes scripts: Word stores a separate font for Latin, for East Asian and for complex scripts. Name the one you mean with a prefix:

Text("客户名称", font="eastAsia:微软雅黑")

The regions are ascii, hAnsi, eastAsia and cs. Without a prefix, the name is set for ascii and hAnsi — the two Latin ones.

A hyperlink is a relationship in the part being rendered, and a value object cannot create one on its own. Ask the template for the id and pass it:

link = Text("the invoice", url_id=tpl.url_id("https://example.test/invoice/42"))
tpl.render({"link": link})

Template.url_id() is build_url_id() in the compatibility API.

Styling the link

The relationship makes it a link; it does not make it look like one. Word's built-in character style for that is Hyperlink:

Text("the invoice", style="Hyperlink", url_id=tpl.url_id(url))

Tags in the address

The other direction: the link is already in the template, and it is the address that depends on the data. Put the tag in the address field of Word's Insert Hyperlink dialog:

Track your order: here — where the address reads https://example.test/orders/{{ order.reference }}

tpl.render({"order": {"reference": "2026-0042"}})

An address is not stored in the document. It is the Target attribute of a relationship in the part's .rels, so it is rendered by a pass of its own — which works the same way in the body, in a header and in a footer, and needs no Text object and no url_id().

Escaping is left to the XML serialiser, so a value containing & produces the URL you wrote and not one with & in it.

One address per relationship, so not per loop iteration

Word stores one relationship for one address and points every use of it at the same id. A {%tr for %} loop copies the w:hyperlink elements but not the relationship, so every row of the loop gets the same rendered URL — and the loop variable is not in scope when the address is rendered, so it renders empty.

A per-iteration address is what url_id() above is for: build a Text per item, each with its own relationship.

Control characters

A Text segment expands the same escapes a plain value does: \n becomes a line break, \t a tab, \f a page break and \a a new paragraph. For text whose indentation matters as well, see Preformatted text.