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:
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:
The regions are ascii, hAnsi, eastAsia and cs. Without a prefix, the
name is set for ascii and hAnsi — the two Latin ones.
Hyperlinks¶
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:
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 }}
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.