python-docx-template: Word documents as Jinja2 templates
Use a docx as a jinja2 template
At a glance
- What is it?
- You design the document in Microsoft Word, drop Jinja tags into the text, and let the library fill it from a dictionary. A close look at what the packaging files say about its shape.
- Who is it for?
- python-docx-template earns its place when a Word document has to stay a Word document, because it sidesteps the whole category of problems that come from generating docx files programmatically. Layout, headers, images and tables stay under the control of Word, and Python only substitutes text.
- Can I use it commercially?
- Yes, with conditions. LGPL-2.1 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 95 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Authoring the document in Word instead of in code
The pitch takes one paragraph and it is a good one. python-docx is capable of building documents, but not of modifying ones that already exist, so this package exists to fill that gap from the other direction. You make the document you want in Microsoft Word, however complicated it is, then you insert Jinja2 style tags into the running text while you are still editing. Save it, and the saved file is the template.
The README states this as a deliberate workflow rather than an API detail: pictures, index tables, footers, headers, variables, anything Word can do is available, because Word did the work. Python never touches the layout. It reads the saved document, finds the tags, and substitutes values.
pip install docxtplOnce installed, generating a document is a matter of handing a template path and a dictionary to the engine, and the same call can be repeated as many times as you have rows of data. For a reporting job, a contract generator or an invoice run, that means the expensive part of the work, visual design, happens once in a tool your colleagues already know, and the repetitive part happens in a script.
Three required dependencies and two optional extras
`pyproject.toml` is short and says almost everything about the design. The runtime dependencies are python-docx for the document model, jinja2 for the tags, and lxml, which is what makes the XML level substitution possible at all. A docx file is a zip of XML parts, and reaching into paragraph runs to replace text without destroying formatting means working at that level.
dependencies = [
"python-docx",
"jinja2",
"lxml",
]There are two optional dependency groups, and their names tell you what they are for. `subdoc` pulls in `docxcompose`, which is the library that merges one docx into another and handles the relationship and numbering conflicts that a naive merge creates. `docs` pulls in Sphinx and sphinxcontrib-napoleon for building the documentation site.
[project.optional-dependencies]
subdoc = ["docxcompose"]
docs = ["Sphinx", "sphinxcontrib-napoleon"]The package name on PyPI is `docxtpl`, not the repository name, and the distribution declares itself as a Python docx template engine with `jinja2` as its keyword. It requires Python 3.7 or newer and its classifier list runs from 3.7 through 3.14, so the supported range is current even though the minimum is old. Development status is declared as 4 - Beta, and the licence is LGPL-2.1-only.
Version numbers read out of docxtpl/__init__.py
The version is not written in `pyproject.toml`. The project declares `dynamic = ["version"]` and lets poetry-dynamic-versioning read the number out of the package source, which keeps a single place of truth inside `docxtpl/` rather than duplicating it across packaging files.
[tool.poetry-dynamic-versioning]
enable = true
[tool.poetry-dynamic-versioning.from-file]
source = "docxtpl/__init__.py"
pattern = '__version__ = "(.+)"'The pattern is a regular expression, so the version string in `docxtpl/__init__.py` is what ends up on the index page. `setup.py` still exists alongside this and does the same job with a hand written regular expression and a `get_version()` helper, which suggests the project has migrated most of its build to poetry without deleting the older path.
setup(
name="docxtpl",
version=get_version("docxtpl"),
description="Python docx template engine",
)The repository carries the usual set of lock files for that kind of transition: `poetry.lock`, `uv.lock` and a `Pipfile`, plus a `requirements.txt` that lists python-docx, docxcompose, jinja2, lxml and sphinx-book-theme. There is no published release history to read through, so the changelog lives in `CHANGES.rst` at the root.
Type checking configured for lxml internals
The `[tool.mypy]` section is more revealing than its size suggests. It turns on `pretty` output, pins `python_version = "3.9"` as the checking target rather than the minimum supported version, and enables `check_untyped_defs` and `warn_unused_ignores`. The last of those is the interesting setting: it means the project treats ignored type errors as errors, which is what you want in a library whose whole job is walking untyped XML.
The dev dependency group backs that up with `mypy >=1.18.2`, `lxml-stubs >=0.5.1` and `flake8 >=7.3.0`, all gated to Python 3.9 and above. Stub packages for lxml matter here because lxml ships no annotations of its own and the package leans on it for the low level document work.
There is also a `[[tool.mypy.overrides]]` entry for the `docxcompose.*` module that relaxes missing import handling. That is the sub document library being treated as untyped at the boundary rather than across the whole project. It is a small thing, and it tells you the type checking story here is genuine rather than decorative: the authors are annotating around third party code they do not control, which is exactly where docx generation gets messy.
What the README leaves to the documentation site
Here is the honest limit of the README. It explains the concept in a paragraph, names the two packages the project builds on, and then points you at docxtpl.readthedocs.org. It does not show the import, the call signature, the tag syntax, or a single end to end example. Everything you need to write a template is documented elsewhere.
The repository tree explains where the code lives. `docxtpl/` is the package itself, `tests/` holds the test suite, `docs/` holds the Sphinx sources that produce the site, and `.readthedocs.yaml` configures the build. A `CHANGES.rst` at the root and a `MANIFEST.in` for packaging round it out. There are no example projects at the top level, so the documentation site carries the weight of teaching this package.
The scale suggests a well used project with unfinished edges. The repository has 2708 stars and 454 forks, and 177 open issues against a package whose README is under a thousand words. That ratio is the most informative number here: the library is embedded in a lot of other people's code, and the conversation about it lives in issues rather than in documentation. Plan to read the issues for the feature you need.
Where it sits against building documents directly
If your output is a fixed report with no human styling involved, python-docx on its own is less machinery and fewer moving parts. You build the document in code, there is no template file to keep in sync with the code, and nothing depends on the tags surviving a Word save. The extra indirection only pays off once someone other than the author needs to change the layout.
The value here is that the designer is a lawyer, an accountant or a marketing person working in Word, and the automation is a script. That division is the whole reason the package is popular, and it is also its constraint. Because the template is a Word file, the tags live inside text runs, which means tag placement has to respect how Word splits text, and an author who reformats aggressively can break a template without any code change to show for it. The documentation site covers those escaping rules; the README does not.
The author keeps three related projects in the same ecosystem: django-listing for tables in Django, python-textops3 for chainable text operations, and django-robohash-svg for generated avatars. Same author, same taste for small focused libraries that plug into larger frameworks.
Editorial conclusion
python-docx-template earns its place when a Word document has to stay a Word document, because it sidesteps the whole category of problems that come from generating docx files programmatically. Layout, headers, images and tables stay under the control of Word, and Python only substitutes text. The package is small, LGPL licensed, and its three required dependencies are python-docx, jinja2 and lxml, with docxcompose available as an extra for merging sub documents. The cost is that the README is an introduction rather than a reference: tag syntax, escaping rules and the row and cell helpers live at docxtpl.readthedocs.org, and that is where you will spend your time. Install it as docxtpl, author one template in Word by hand, and read the documentation page for the tag syntax before you try a table.
Frequently asked questions
How do I make a DOCX template?
Create the document you want in Microsoft Word, including layout, headers and images, then type Jinja2 style tags directly into the text while you are still editing. Save the file as a .docx and treat that file as the template. python-docx-template reads the tags when it renders, so the Word file stays the source of the design.
How to create a DOCX File in python?
Two paths exist. python-docx creates documents programmatically and is the direct answer, while python-docx-template fills an existing Word file from a dictionary. The README states the reason the second package exists: python-docx is powerful for creating documents but not for modifying ones that already exist.
Why does python-docx-template depend on lxml?
A docx file is a zip of XML parts, and replacing text inside a paragraph without losing the run formatting means working at that XML level rather than through a high level document model. lxml provides that access, which is why it sits alongside python-docx and jinja2 in the required dependencies of pyproject.toml.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/elapouya-python-docx-template)