Skip to content

Inspecting a template before rendering

"What does this template need?" is worth being able to answer before the render, not after: it is what lets an application build a form, validate a payload, or fail with a message naming what is missing instead of emitting a document full of blanks.

from docxtpl import Template

tpl = Template("invoice.docx")
tpl.undeclared_variables()
# {'customer', 'order', 'lines', 'total'}

The answer covers every part — a name used only in a footer or only in the document title is reported — and asking does not change the document: the inspection compiles a copy.

missing = tpl.undeclared_variables() - set(context)
if missing:
    raise ValueError(f"the template needs {sorted(missing)}, which the context lacks")
tpl.render(context)

What counts as needed

The answer comes from Jinja2's parsed syntax tree rather than from a search for {{, which is what makes it right in the cases that matter:

  • a name introduced by the template itself is not reported. line in {%tr for line in lines %} is the loop's own variable; lines is what the caller has to supply;
  • a name used only inside a branch is reported, because the branch might be taken;
  • a name used only as {{ customer.name }} is reported as customer, since that is what the context has to hold.

Is the template even valid?

The other question, and it needs no context at all:

report = tpl.validate()

report.ok            # -- False --
report.variables     # -- ['customer', 'lines', 'order', 'total'] --
report.problems      # -- [Problem(part='header 1', message='{% endfor %} is …')] --

validate() compiles and parses every part, which is where the two halves of being valid are found: compiling catches a tag whose scope prefix names an element that does not contain it, and parsing catches a {% for %} with no {% endfor %}. Nothing is rendered and nothing is written.

A part that did not compile contributes no variables, because there is no honest way to say what a template one cannot parse requires.

Requiring the context to be complete

A tag whose variable the context does not supply renders to nothing:

tpl.render({"a": "1"})     # -- template: "{{ a }} and {{ b }}" --
# -- "1 and " --

Which is often what a template means — an optional field. When it is not, strict=True makes it an error naming the variable and the part:

tpl.render({"a": "1"}, strict=True)
# RenderError: 'b' is undefined (in document body)

This is a better answer than a "was it fully rendered" flag checked afterwards, because nothing after the render can tell a missing value from one that was legitimately empty.

Errors that name where they came from

When a render does fail, the part is in the message — which a Jinja2 traceback has no way of knowing:

from docxtpl import RenderError

try:
    tpl.render(context)
except RenderError as error:
    print(error)        # -- 'order' is undefined (in footer 1) --
    print(error.part_name)  # -- 'footer 1' --
    raise error.__cause__ from None  # -- the original, if you want it --

Every error this package raises derives from DocxTemplateError, so one clause catches the lot:

Exception Raised when
TemplateSyntaxError a tag is unterminated, or scoped to an element that does not contain it
UnsupportedTagError a table directive is used outside a table, or given an argument it cannot use
StyleError a rendered value named a style a run cannot carry, or one that does not exist
ImageError an image source is not in a format that can be read
RenderError rendering failed — by Jinja2's doing or a context object's

StyleError, ImageError and MediaNotFoundError are ValueErrors as well, because that is what python-docx-ng and docxtpl raise for the equivalent mistakes, so an existing except ValueError keeps working.

A TemplateSyntaxError quotes the surrounding document text, so the tag can be found in Word:

{%tr endfor %} is scoped to w:tr, which does not contain it.
Found in: 'Totals per line {%tr endfor %}'

From the command line

The quickest way to find out what a template does — and the quickest way to reproduce a bug report, since it needs a template and a JSON file rather than a program:

docxtpl-ng invoice.docx context.json out.docx
Option Effect
--validate check the template and list what it needs; write nothing
--report FILE write the check as JSON to FILE, for CI. Implies --validate
--strict a variable the context does not supply is an error, not a blank
-o, --overwrite replace the output file; without it, an existing file is refused
-q, --quiet say nothing on success
--no-escape do not XML-escape values, as the compatibility API does not
--no-sandbox render without Jinja2's sandbox, which is on by default here

It exits non-zero and prints one line to stderr on failure, without a traceback: the errors this package raises are written for a person to act on.

The sandbox default is the opposite of the library's, and Security says why.

Checking a template in CI

$ docxtpl-ng --validate invoice.docx
invoice.docx is valid
requires: customer, lines, order, total

$ docxtpl-ng --validate broken.docx
docxtpl-ng: broken.docx: document body: {% endfor %} is scoped to w:tr, which does
  not contain it. Found in: 'Totals per line {%tr endfor %}'
$ echo $?
1

--validate takes a template and nothing else. --report writes the same answer as JSON, in a shape that changes only by gaining keys:

{
  "ok": false,
  "variables": ["customer", "order"],
  "problems": [
    {"part": "document body", "message": "{% endfor %} is scoped to w:tr, …"}
  ]
}