Skip to content

Fragments and document assembly

Template syntax expresses content that varies. It expresses structure that varies badly — a findings section that is a heading and a bulleted list for one customer and a table for another is a page of nested tags nobody wants to maintain in Word.

A Fragment is the escape hatch. Build the content with the ordinary python-docx-ng API and drop it in at a tag:

from docxtpl import Template

tpl = Template("report.docx")

findings = tpl.new_fragment()
findings.document.add_heading("Findings", level=2)
for finding in results:
    findings.document.add_paragraph(finding.title, style="List Bullet")
    findings.document.add_paragraph(finding.detail)

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

In the template, in a paragraph of its own:

{{p findings }}

Why the p prefix

{{p findings }} is scoped to the paragraph: the paragraph is replaced by the fragment's block-level content. A fragment produces paragraphs and tables, and those cannot live inside another paragraph.

An unprefixed {{ findings }} works too — the paragraph is divided around the inserted content — but {{p }} says what you mean and leaves nothing behind.

Lists, bullets and numbering

A list item is not formatting on a run — it is a paragraph property, pointing at a numbering definition in word/numbering.xml. So it is not something a Text can express, and add_list() is on the fragment:

findings = tpl.new_fragment()
findings.add_list(
    ["Access control", ["No MFA on the console", "Shared service account"], "Logging"],
    numbered=True,
)

tpl.render({"findings": findings})
  1. Access control a. No MFA on the console b. Shared service account
  2. Logging

Nesting is the level. A string is an item; a sequence is a sublist one level deeper. OOXML allows nine, and a tenth is an error rather than a silent flatten.

numbered=False — the default — gives bullets. bullets=("–", "·") and formats=(WD_NUMBER_FORMAT.UPPER_ROMAN,) choose the glyph or the counter per level, cycling when shorter than the depth. style="List Paragraph" names a paragraph style for the look; the numbering comes from the call whatever the style says.

add_list() returns the paragraphs it made, so anything else you want to do to them is the ordinary API.

Each list starts at 1

add_list() defines its own list and the definition travels with the content. Two consequences, both of which are the point:

  • a loop producing one list per item gets a list per item, each starting at 1, with no w:startOverride and no bookkeeping;
  • the template needs no list in it. Nothing has to be pre-authored in Word for a definition to point at, and nothing depends on the template having a List Bullet style.

For a numbering scheme this does not cover — cumulative 1.1.1 level text, locale-specific formats, a list continuing across fragments — fragment.document.numbering is python-docx-ng's whole numbering model, and paragraph.set_numbering(num_id, level) puts a paragraph in a list you defined yourself.

Merging an existing document

terms = tpl.new_fragment("terms-and-conditions.docx")
tpl.render({"terms": terms})

The body of that file is inserted. Its section definition is not: page size and margins belong to the document being rendered into.

new_fragment() takes a path, anything path-like, an open stream, or a docx.document.Document — and Fragment.from_file() is the same thing spelled as a classmethod, for code that already uses it.

Composing from documents another template rendered

A document that is already open needs no round trip through a file. Pass it straight in:

section = Template("section.docx")
section.render({"client": client})

report = Template("report.docx")
report.render({"body": report.new_fragment(section.document)})

The document need never have been saved — one built by hand with docx.Document() works the same way, images, hyperlinks and lists included.

It is also not copied. Content is read out of it when the fragment renders, so anything added in between arrives, and the document is yours to go on using afterwards. Fragment.from_document() is the same thing under a name that says so.

Render the pieces first

A fragment carries content, not template syntax. A {{ }} still sitting in the source document is inserted as the characters {{ }}, because the fragment's markup is spliced in after the tags of the outer template have already been read. Render each piece with its own Template before inserting it, as above.

What comes with it

Assembling documents that were written separately is the fiddliest thing in this package, because the content refers to things by id and the ids mean different things in the two files. Three kinds of reference are carried across:

Relationships — images and hyperlinks. Re-created in the part being rendered, so they point at something. An image identical to one the target already has is not stored twice.

Numbering — the definitions behind list formatting. Always renumbered: two documents that each numbered their lists from 1 would otherwise share a definition, and the merged list would carry on the target's counter, starting at 4 because the target's own list ended at 3.

Styles — target wins. A style the source defines and the target already has keeps the target's definition, so merged content looks like the document it was merged into rather than like the one it came from. A style the target does not have is copied across, because the alternative is content that silently renders as Normal.

Target-wins is a choice, and it can surprise

If both documents define Heading 2 and they differ, the merged headings look like the target's. That is what you want when assembling a report from fragments and not what you want when quoting another document verbatim. For the second case, rename the style in the source before merging.

Documents a converter wrote

Pandoc, LibreOffice and every reporting tool's "export to Word" produce a .docx that Word did not, and they are a large share of what people actually merge. They work, and the suite says so rather than assuming: tests/test_foreign.py inserts a LibreOffice-produced document into a Word-produced one — twice over, which is when ids collide — and checks the content, the table, the image, the link, the numbering and the schema.

Three things are handled that a naive merge gets wrong, and all three are routine in converter output:

Namespace prefixes. A converter may bind the wordprocessing namespace to any prefix it likes; <ww:p> and <w:p> are the same element. Content is parsed rather than concatenated, so the prefix a document happens to use is not part of what is carried.

Embedded objects. Two documents written separately both call their first embedded object oleObject1.bin. The second is renamed to oleObject2.bin and its relationship follows it, so both objects survive. An object identical to one already there is reused rather than duplicated.

Drawing ids. Two wp:docPr elements claiming one id is among the few things Word calls unreadable rather than merely wrong. They are renumbered across the whole part after every merge.

The compatibility name

Subdoc is the same object, and tpl.new_subdoc() returns one:

sd = tpl.new_subdoc()                             # -- empty --
sd = tpl.new_subdoc("terms-and-conditions.docx")  # -- from a file --
sd = tpl.new_subdoc(other.document)               # -- from a document in memory --