Skip to content

Modern API

The typed API, for new code.

Template

Template(source: Source)

A .docx file used as a Jinja2 template.

Source code in src/docxtpl/template.py
def __init__(self, source: Source) -> None:
    # -- the template's own bytes are kept so `reload()` can go back to it.
    # -- A stream is read once here rather than seeked later, since a caller
    # -- may hand over something that is not seekable --
    self._given_source = source
    self._source = bytes_of(source)
    self._document = open_document(io.BytesIO(self._source))
    self._is_rendered = False
    self._has_been_saved = False
    self._replacements = ReplacementRegistry()
    self._current_part: Optional[XmlPart] = None
    self._update_fields: Optional[bool] = None
    # -- survives `reload()`, which is the whole point: it describes the
    # -- template rather than the render --
    self._prepared: Dict[Tuple[bytes, Options], _Prepared] = {}
    self._environments: Dict[Options, Environments] = {}

document property

document: Document

The underlying document, usable with the normal python-docx-ng API.

source property

source: Source

The template this was loaded from, exactly as it was given.

is_rendered property

is_rendered: bool

True once render() has run at least once.

has_been_saved property

has_been_saved: bool

True once save() has been called.

current_part property

current_part: XmlPart

The package part being rendered, or the document part between renders.

Content objects need it: a w:drawing refers to its image by relationship id, and a relationship belongs to a part. An image in a header must be related from the header part, not from the document.

reload

reload() -> None

Discard the rendered document and start again from the template.

Rendering mutates the document in place, so rendering the same |Template| against a second context applies it to the first render's output. Reloading is what makes a loop over many contexts work:

for customer in customers:
    template.reload()
    template.render({"customer": customer})
    template.save(f"letter-{customer.id}.docx")

Pending binary replacements survive, since they describe the template rather than the render.

Source code in src/docxtpl/template.py
def reload(self) -> None:
    """Discard the rendered document and start again from the template.

    Rendering mutates the document in place, so rendering the same
    |Template| against a second context applies it to the first render's
    output. Reloading is what makes a loop over many contexts work:

        for customer in customers:
            template.reload()
            template.render({"customer": customer})
            template.save(f"letter-{customer.id}.docx")

    Pending binary replacements survive, since they describe the template
    rather than the render.
    """
    self._document = open_document(io.BytesIO(self._source))
    self._is_rendered = False
    self._has_been_saved = False
    self._current_part = None

render

render(
    context: Mapping[str, Any],
    *,
    jinja_env: Optional[Environment] = None,
    autoescape: bool = True,
    sandboxed: bool = False,
    strict: bool = False,
) -> None

Render every templatable part of the document against context.

strict makes a name the context does not supply an error instead of an empty string. Without it a tag whose variable is missing renders to nothing, and a document full of blanks looks exactly like a document whose data was legitimately empty.

sandboxed renders in a jinja2.sandbox.SandboxedEnvironment. A .docx template is executable input — that is what a template engine is — so anywhere one arrives from outside, see the security page before deciding what to do about it. The sandbox is a mitigation and not a boundary.

A jinja_env that is already sandboxed stays sandboxed whatever this says. One that is not cannot be made so — the sandbox is the class, not a setting — and asking for both raises ValueError rather than rendering unsandboxed for a caller who asked otherwise.

Source code in src/docxtpl/template.py
def render(
    self,
    context: Mapping[str, Any],
    *,
    jinja_env: Optional[Environment] = None,
    autoescape: bool = True,
    sandboxed: bool = False,
    strict: bool = False,
) -> None:
    """Render every templatable part of the document against `context`.

    `strict` makes a name the context does not supply an error instead of an
    empty string. Without it a tag whose variable is missing renders to
    nothing, and a document full of blanks looks exactly like a document
    whose data was legitimately empty.

    `sandboxed` renders in a `jinja2.sandbox.SandboxedEnvironment`. A .docx
    template is executable input — that is what a template engine is — so
    anywhere one arrives from outside, see
    [the security page](../user/security.md) before deciding what to do
    about it. The sandbox is a mitigation and not a boundary.

    A `jinja_env` that is *already* sandboxed stays sandboxed whatever this
    says. One that is not cannot be made so — the sandbox is the class, not
    a setting — and asking for both raises `ValueError` rather than
    rendering unsandboxed for a caller who asked otherwise.
    """
    options = self._options(jinja_env, autoescape, sandboxed, strict)
    environments = self._environments_for(jinja_env, autoescape, sandboxed, strict, options)
    for part in self._parts():
        self._render_part(part, context, environments, options)
    self._current_part = None
    self._is_rendered = True

save

save(target: Source) -> None

Write the document to target.

pre_processing() and post_processing() bracket the write, so a subclass can touch the document on its way out without overriding this.

Source code in src/docxtpl/template.py
def save(self, target: Source) -> None:
    """Write the document to `target`.

    `pre_processing()` and `post_processing()` bracket the write, so a
    subclass can touch the document on its way out without overriding this.
    """
    self.pre_processing()
    self._replacements.apply(self._document, allow_missing=self.allow_missing_replacements)
    if self._update_fields is not None:
        self._document.settings.update_fields_on_open = self._update_fields
    self._document.save(opened(target))
    self._has_been_saved = True
    self.post_processing(target)

pre_processing

pre_processing() -> None

Called by save() before the document is written. Does nothing.

A hook for subclasses: whatever it does happens after every render and before the bytes are produced, which is where a last look at self.document belongs.

Source code in src/docxtpl/template.py
def pre_processing(self) -> None:
    """Called by `save()` before the document is written. Does nothing.

    A hook for subclasses: whatever it does happens after every render and
    before the bytes are produced, which is where a last look at
    `self.document` belongs.
    """

post_processing

post_processing(target: Source) -> None

Called by save() after the document is written. Does nothing.

A hook for subclasses, given whatever save() was given — a path or the stream the document was written to.

Source code in src/docxtpl/template.py
def post_processing(self, target: Source) -> None:
    """Called by `save()` after the document is written. Does nothing.

    A hook for subclasses, given whatever `save()` was given — a path or the
    stream the document was written to.
    """

render_parts

render_parts(
    names: Iterable[str],
    context: Mapping[str, Any],
    *,
    jinja_env: Optional[Environment] = None,
    autoescape: bool = True,
    sandboxed: bool = False,
    strict: bool = False,
) -> None

Render only the parts in names, leaving the rest of the document.

The names are the ones docxtpl.parts yields — "document body", "header 1", "footnotes", "core properties" and so on. Useful when the context for one part is not ready when the others are; the compatibility surface's render_footnotes() and render_properties() are this with the names filled in.

Source code in src/docxtpl/template.py
def render_parts(
    self,
    names: Iterable[str],
    context: Mapping[str, Any],
    *,
    jinja_env: Optional[Environment] = None,
    autoescape: bool = True,
    sandboxed: bool = False,
    strict: bool = False,
) -> None:
    """Render only the parts in `names`, leaving the rest of the document.

    The names are the ones `docxtpl.parts` yields — ``"document body"``,
    ``"header 1"``, ``"footnotes"``, ``"core properties"`` and so on. Useful
    when the context for one part is not ready when the others are; the
    compatibility surface's `render_footnotes()` and `render_properties()`
    are this with the names filled in.
    """
    wanted = set(names)
    options = self._options(jinja_env, autoescape, sandboxed, strict)
    environments = self._environments_for(jinja_env, autoescape, sandboxed, strict, options)
    for part in self._parts():
        if part.name in wanted:
            self._render_part(part, context, environments, options)
    self._current_part = None
    self._is_rendered = True

undeclared_variables

undeclared_variables(
    *, jinja_env: Optional[Environment] = None
) -> Set[str]

Names the template reads from the context but never defines itself.

Source code in src/docxtpl/template.py
def undeclared_variables(self, *, jinja_env: Optional[Environment] = None) -> Set[str]:
    """Names the template reads from the context but never defines itself."""
    environment = build_environment(jinja_env)
    parts = list(self._parts())
    return _undeclared_variables(
        parts,
        environment,
        extra_sources=self._hyperlink_sources(parts),
    )

validate

validate(
    *, jinja_env: Optional[Environment] = None
) -> Report

Check the template without rendering it, and say what it needs.

Answers the two questions a caller has before there is any data: is this template valid, and what does it require. Neither needs a context, and neither should have to be found out by rendering — a tag scoped to an element that does not contain it, or a {% for %} with no {% endfor %}, is a mistake the author can fix in Word.

The document is not modified and nothing is written.

Source code in src/docxtpl/template.py
def validate(self, *, jinja_env: Optional[Environment] = None) -> Report:
    """Check the template without rendering it, and say what it needs.

    Answers the two questions a caller has before there is any data: *is
    this template valid*, and *what does it require*. Neither needs a
    context, and neither should have to be found out by rendering — a tag
    scoped to an element that does not contain it, or a `{% for %}` with no
    `{% endfor %}`, is a mistake the author can fix in Word.

    The document is not modified and nothing is written.
    """
    environment = build_environment(jinja_env)
    parts = list(self._parts())
    return _validate(
        parts,
        environment,
        extra_sources=self._hyperlink_sources(parts),
    )

new_fragment

new_fragment(
    source: Optional[FragmentSource] = None,
) -> Fragment

A |Fragment| bound to this template.

Empty by default, to be filled with the ordinary document API. Given a source it holds that document's body instead: a path, a stream, or a docx.document.Document already in memory — including one another |Template| has just rendered, which is how a document is assembled from pieces without a round trip through a file.

Source code in src/docxtpl/template.py
def new_fragment(self, source: Optional[FragmentSource] = None) -> Fragment:
    """A |Fragment| bound to this template.

    Empty by default, to be filled with the ordinary document API. Given a
    `source` it holds that document's body instead: a path, a stream, or a
    `docx.document.Document` already in memory — including one another
    |Template| has just rendered, which is how a document is assembled from
    pieces without a round trip through a file.
    """
    from docxtpl.content.fragment import Fragment

    return Fragment(self) if source is None else Fragment.from_file(self, source)

image

image(source: ImageSource, **size: Any) -> Image

An |Image| bound to this template. size takes width and height.

Source code in src/docxtpl/template.py
def image(self, source: ImageSource, **size: Any) -> Image:
    """An |Image| bound to this template. `size` takes `width` and `height`."""
    from docxtpl.content.image import Image

    return Image(self, source, **size)

url_id

url_id(url: str) -> str

A relationship id for url, for use as Text(url_id=…).

The relationship is added to the part being rendered when a render is in progress, and to the document part otherwise — which is where a caller building the context before calling render() means it to go.

Source code in src/docxtpl/template.py
def url_id(self, url: str) -> str:
    """A relationship id for `url`, for use as `Text(url_id=…)`.

    The relationship is added to the part being rendered when a render is in
    progress, and to the document part otherwise — which is where a caller
    building the context before calling `render()` means it to go.
    """
    return self.current_part.relate_to(url, RT.HYPERLINK, is_external=True)

update_fields

update_fields(enable: bool = True) -> None

Ask Word to recalculate every field when it opens the document.

A table of contents, a page count, a cross-reference and a DOCPROPERTY field all display the value cached when the template was last computed. Rendering changes the document, not the cache, so a rendered file shows a table of contents describing the template.

Nothing here can compute those values — every one of them depends on how Word lays the document out — so the only honest answer is to ask Word to, by setting w:updateFields in word/settings.xml.

Word asks the reader first. It shows "This document contains fields that may refer to other files. Do you want to update…?" and the reader may say no. There is no way to make Word update silently; LibreOffice does update silently. Anyone promising otherwise is promising something the format cannot express.

enable=False removes the setting, including from a template that arrived with it.

Applied at save time rather than immediately, like a binary replacement and for the same reason: it describes the output rather than the render, so it survives reload() and a loop over many contexts.

Source code in src/docxtpl/template.py
def update_fields(self, enable: bool = True) -> None:
    """Ask Word to recalculate every field when it opens the document.

    A table of contents, a page count, a cross-reference and a
    `DOCPROPERTY` field all display the value cached when the template was
    last computed. Rendering changes the document, not the cache, so a
    rendered file shows a table of contents describing the *template*.

    Nothing here can compute those values — every one of them depends on how
    Word lays the document out — so the only honest answer is to ask Word
    to, by setting `w:updateFields` in `word/settings.xml`.

    **Word asks the reader first.** It shows "This document contains fields
    that may refer to other files. Do you want to update…?" and the reader
    may say no. There is no way to make Word update silently; LibreOffice
    does update silently. Anyone promising otherwise is promising something
    the format cannot express.

    `enable=False` removes the setting, including from a template that
    arrived with it.

    Applied at save time rather than immediately, like a binary replacement
    and for the same reason: it describes the output rather than the render,
    so it survives `reload()` and a loop over many contexts.
    """
    self._update_fields = enable

replace_picture

replace_picture(
    embedded_name: str, source: ImageSource
) -> None

Replace the image stored in the package as embedded_name.

Source code in src/docxtpl/template.py
def replace_picture(self, embedded_name: str, source: ImageSource) -> None:
    """Replace the image stored in the package as `embedded_name`."""
    self._replacements.replace_picture(embedded_name, source)

replace_media

replace_media(
    embedded_name: str, source: ImageSource
) -> None

Replace a media part, including one referenced only from a header.

Source code in src/docxtpl/template.py
def replace_media(self, embedded_name: str, source: ImageSource) -> None:
    """Replace a media part, including one referenced only from a header."""
    self._replacements.replace_media(embedded_name, source)

replace_embedded

replace_embedded(
    embedded_name: str, source: ImageSource
) -> None

Replace an embedded object part, such as an OLE spreadsheet.

Source code in src/docxtpl/template.py
def replace_embedded(self, embedded_name: str, source: ImageSource) -> None:
    """Replace an embedded object part, such as an OLE spreadsheet."""
    self._replacements.replace_embedded(embedded_name, source)

replace_zip_member

replace_zip_member(
    member_name: str, source: ImageSource
) -> None

Replace an arbitrary zip member by its full path in the package.

Source code in src/docxtpl/template.py
def replace_zip_member(self, member_name: str, source: ImageSource) -> None:
    """Replace an arbitrary zip member by its full path in the package."""
    self._replacements.replace_zip_member(member_name, source)

reset_replacements

reset_replacements() -> None

Discard every pending binary replacement.

Source code in src/docxtpl/template.py
def reset_replacements(self) -> None:
    """Discard every pending binary replacement."""
    self._replacements.reset()

Text

Text(
    text: str = "",
    *,
    style: Optional[str] = None,
    color: Optional[str] = None,
    highlight: Optional[str] = None,
    size: Optional[int] = None,
    subscript: bool = False,
    superscript: bool = False,
    bold: bool = False,
    italic: bool = False,
    underline: Union[bool, str, None] = None,
    strike: bool = False,
    font: Optional[str] = None,
    url_id: Optional[str] = None,
    rtl: bool = False,
    lang: Optional[str] = None,
)

A run of text, or several, with formatting.

Segments are appended with add(); the constructor is a shorthand for creating the object and appending the first segment in one call.

Source code in src/docxtpl/content/richtext.py
def __init__(
    self,
    text: str = "",
    *,
    style: Optional[str] = None,
    color: Optional[str] = None,
    highlight: Optional[str] = None,
    size: Optional[int] = None,
    subscript: bool = False,
    superscript: bool = False,
    bold: bool = False,
    italic: bool = False,
    underline: Union[bool, str, None] = None,
    strike: bool = False,
    font: Optional[str] = None,
    url_id: Optional[str] = None,
    rtl: bool = False,
    lang: Optional[str] = None,
) -> None:
    self._segments: List[_Segment] = []
    if text or any(
        (style, color, highlight, size, subscript, superscript, bold, italic, underline,
         strike, font, url_id, rtl, lang),
    ):
        self.add(
            text,
            style=style,
            color=color,
            highlight=highlight,
            size=size,
            subscript=subscript,
            superscript=superscript,
            bold=bold,
            italic=italic,
            underline=underline,
            strike=strike,
            font=font,
            url_id=url_id,
            rtl=rtl,
            lang=lang,
        )

xml property

xml: str

The w:r sequence this object renders to.

add

add(
    text: str = "",
    *,
    style: Optional[str] = None,
    color: Optional[str] = None,
    highlight: Optional[str] = None,
    size: Optional[int] = None,
    subscript: bool = False,
    superscript: bool = False,
    bold: bool = False,
    italic: bool = False,
    underline: Union[bool, str, None] = None,
    strike: bool = False,
    font: Optional[str] = None,
    url_id: Optional[str] = None,
    rtl: bool = False,
    lang: Optional[str] = None,
) -> Text

Append a segment and return self, so calls chain.

Source code in src/docxtpl/content/richtext.py
def add(
    self,
    text: str = "",
    *,
    style: Optional[str] = None,
    color: Optional[str] = None,
    highlight: Optional[str] = None,
    size: Optional[int] = None,
    subscript: bool = False,
    superscript: bool = False,
    bold: bool = False,
    italic: bool = False,
    underline: Union[bool, str, None] = None,
    strike: bool = False,
    font: Optional[str] = None,
    url_id: Optional[str] = None,
    rtl: bool = False,
    lang: Optional[str] = None,
) -> Text:
    """Append a segment and return `self`, so calls chain."""
    properties: dict[str, object] = {
        "style": style,
        "font": font,
        "bold": bold,
        "italic": italic,
        "strike": strike,
        "color": color,
        "size": size,
        "highlight": highlight,
        "underline": _DEFAULT_UNDERLINE if underline is True else underline,
        "vertical_align": (
            "superscript" if superscript else "subscript" if subscript else None
        ),
        "rtl": rtl,
        "lang": lang,
    }
    self._segments.append(_Segment(text, properties, url_id))
    return self

Fragment

Fragment(template: Template)

Document content built or loaded separately and inserted at a tag.

Source code in src/docxtpl/content/fragment.py
def __init__(self, template: Template) -> None:
    self._template = template
    self._document = open_document()

document property

document: Document

The document being built, for use with the normal API.

template property

template: Template

The |Template| this fragment will be inserted into.

xml property

xml: str

The block-level content this fragment renders to.

Reading this has side effects on the document being rendered into: the relationships, styles and numbering the content depends on are carried across first, because markup referring to a relationship the target part does not have is markup Word cannot open.

from_file classmethod

from_file(
    template: Template, source: FragmentSource
) -> _FragmentT

A fragment holding the body of the .docx at source.

source is a path, a path-like, an open stream, or a docx.document.Document; the last is used as it is rather than being opened, so it need never have been saved.

Source code in src/docxtpl/content/fragment.py
@classmethod
def from_file(cls: type[_FragmentT], template: Template, source: FragmentSource) -> _FragmentT:
    """A fragment holding the body of the .docx at `source`.

    `source` is a path, a path-like, an open stream, or a
    `docx.document.Document`; the last is used as it is rather than being
    opened, so it need never have been saved.
    """
    fragment = cls(template)
    fragment._document = source if isinstance(source, Document) else open_document(
        opened(source),
    )
    return fragment

from_document classmethod

from_document(
    template: Template, document: Document
) -> _FragmentT

A fragment holding the body of document, used as it is.

The document is not copied. Content is copied out of it when the fragment renders, so changes made in between are picked up and the caller is free to go on using it afterwards.

Source code in src/docxtpl/content/fragment.py
@classmethod
def from_document(cls: type[_FragmentT], template: Template, document: Document) -> _FragmentT:
    """A fragment holding the body of `document`, used as it is.

    The document is **not** copied. Content is copied out of it when the
    fragment renders, so changes made in between are picked up and the
    caller is free to go on using it afterwards.
    """
    return cls.from_file(template, document)

add_list

add_list(
    items: Iterable[ListItem],
    *,
    numbered: bool = False,
    style: Optional[str] = None,
    bullets: Optional[Sequence[str]] = None,
    formats: Optional[Sequence[WD_NUMBER_FORMAT]] = None,
) -> List[Paragraph]

Append a bulleted or numbered list and return the paragraphs it made.

A list item is a paragraph property — w:numPr, holding a w:ilvl and a w:numId — so it is not something |Text| can express, and it is not something a value object rendering an XML string can express either: the w:numId refers into word/numbering.xml, and a definition has to exist for it to point at.

A fragment is a real document, so this call defines its own list and the definition travels with the content. That is what makes the answer to "a loop producing several lists" fall out: each call is its own list and each starts at 1, without a w:startOverride anywhere and without requiring the template to have contained a list of the right kind.

Nesting is the level. A string is an item; a sequence is a sublist one level deeper::

fragment.add_list(
    ["Findings", ["First", "Second"], "Recommendations"],
    numbered=True,
)

style names a paragraph style for the look — Word's own is "List Paragraph". The numbering comes from this call whatever the style says.

bullets and formats choose the glyph or the counter format per level, cycling when shorter than the depth. For anything beyond that, fragment.document.numbering is the whole model.

Source code in src/docxtpl/content/fragment.py
def add_list(
    self,
    items: Iterable[ListItem],
    *,
    numbered: bool = False,
    style: Optional[str] = None,
    bullets: Optional[Sequence[str]] = None,
    formats: Optional[Sequence[WD_NUMBER_FORMAT]] = None,
) -> List[Paragraph]:
    """Append a bulleted or numbered list and return the paragraphs it made.

    A list item is a **paragraph** property — `w:numPr`, holding a `w:ilvl`
    and a `w:numId` — so it is not something |Text| can express, and it is
    not something a value object rendering an XML string can express either:
    the `w:numId` refers into `word/numbering.xml`, and a definition has to
    exist for it to point at.

    A fragment is a real document, so this call **defines its own list** and
    the definition travels with the content. That is what makes the answer
    to "a loop producing several lists" fall out: each call is its own list
    and each starts at 1, without a `w:startOverride` anywhere and without
    requiring the template to have contained a list of the right kind.

    Nesting is the level. A string is an item; a sequence is a sublist one
    level deeper::

        fragment.add_list(
            ["Findings", ["First", "Second"], "Recommendations"],
            numbered=True,
        )

    `style` names a paragraph style for the *look* — Word's own is
    ``"List Paragraph"``. The numbering comes from this call whatever the
    style says.

    `bullets` and `formats` choose the glyph or the counter format per
    level, cycling when shorter than the depth. For anything beyond that,
    `fragment.document.numbering` is the whole model.
    """
    if isinstance(items, (str, bytes)):
        raise TypeError(
            "add_list() takes a sequence of items; a bare string would be read one"
            " character at a time. Pass [text] for a one-item list.",
        )
    numbering = self._document.numbering
    if numbered:
        definition = numbering.add_numbered_definition(formats=formats)
    elif bullets is not None:
        definition = numbering.add_bulleted_definition(bullets=bullets)
    else:
        definition = numbering.add_bulleted_definition()
    paragraphs: List[Paragraph] = []
    self._add_items(items, definition.num_id, 0, style, paragraphs)
    return paragraphs

Image

Image(
    template: Template,
    source: ImageSource,
    *,
    width: Optional[Length] = None,
    height: Optional[Length] = None,
)

An image to be placed inline where a tag appears.

Source code in src/docxtpl/content/image.py
def __init__(
    self,
    template: Template,
    source: ImageSource,
    *,
    width: Optional[Length] = None,
    height: Optional[Length] = None,
) -> None:
    self.template = template
    """The |Template| whose part this image adds its relationship to."""

    self.source = source
    """The path, bytes or stream the image comes from, exactly as given."""

    self.width = width
    """The width to draw it at, or `None` for the image's own."""

    self.height = height
    """The height to draw it at, or `None` for the image's own."""

    # -- a path is left to be opened at render time, because that is when a
    # -- missing file has always been reported and moving it would surprise
    # -- code that renders in a `try`. Anything else is read now: a stream
    # -- reads once, and this object may be rendered once per loop
    # -- iteration --
    self._blob = None if _is_path(source) else bytes_of(source)

template instance-attribute

template = template

The |Template| whose part this image adds its relationship to.

source instance-attribute

source = source

The path, bytes or stream the image comes from, exactly as given.

width instance-attribute

width = width

The width to draw it at, or None for the image's own.

height instance-attribute

height = height

The height to draw it at, or None for the image's own.

xml property

xml: str

The w:drawing this image renders to, with its relationship added.

The relationship is added to the part being rendered, which for an image in a page header is the header part and not the document. Adding it to the document part instead produces a header that Word shows as a red X.

The image itself is added by content, so the same picture used in twenty places is stored in the package once.

Preformatted

Preformatted(text: str)

Multi-line text rendered with its line breaks and indentation intact.

Source code in src/docxtpl/content/listing.py
def __init__(self, text: str) -> None:
    self._text = text

text property

text: str

The text this object was given, unchanged.

xml property

xml: str

The run sequence this text renders to.

One run, whose w:t carries xml:space="preserve" and whose newlines have become the markers phase 4 turns into w:br. Indentation survives because of the first; the line structure because of the second.

Errors

errors

Exception hierarchy for docxtpl.

Every error this package raises on its own behalf derives from |DocxTemplateError|, so a caller can catch the whole package with one clause. Errors raised by Jinja2 during rendering are wrapped in |RenderError| with the originating document part named, because a bare Jinja2 traceback gives no clue which header, footer or footnote the failing tag lived in.

DocxTemplateError

Bases: Exception

Base class for every error raised by docxtpl.

TemplateSyntaxError

Bases: DocxTemplateError

Template syntax in the .docx is malformed or cannot be placed.

Raised for a tag that is unterminated, a {%tr %} outside a table row, or a control tag whose scope prefix does not match the element containing it.

UnsupportedTagError

Bases: TemplateSyntaxError

A recognised tag prefix is not implemented for this element type.

MediaNotFoundError

Bases: DocxTemplateError, ValueError

A binary replacement named a package member that is not there.

Also a ValueError, which is what docxtpl raises for this, so an existing except ValueError around a save() keeps working. Set allow_missing_replacements (allow_missing_pics on the compatibility class) to skip the replacement instead of raising.

ImageError

Bases: DocxTemplateError, ValueError

An image source could not be read as an image.

Raised for a path, a stream or a bytes holding something that is not in a format the document model recognises. The underlying docx.image.exceptions.UnrecognizedImageError carries no message at all, which for a stream leaves nothing to act on; this one names what was given.

Also a ValueError, so a caller already catching one around a render keeps working.

StyleError

Bases: DocxTemplateError, ValueError

A style a rendered value asked for cannot be used where it was put.

A run can carry only a character style. Giving |Text| a paragraph style — Text("x", style="Heading1") — produces a document that opens, renders and is simply unstyled, which is the worst shape a mistake can take.

Also a ValueError, which is what python-docx-ng raises for the equivalent mistake made through the document API.

RenderError

RenderError(message: str, *, part_name: str | None = None)

Bases: DocxTemplateError

Rendering the compiled template failed.

Wraps the underlying jinja2.TemplateError and names the document part the failure came from. The original exception is available as __cause__.

Source code in src/docxtpl/errors.py
def __init__(self, message: str, *, part_name: str | None = None) -> None:
    self.part_name = part_name
    super().__init__(f"{message} (in {part_name})" if part_name else message)