TLDR: If your Jinja2 blueprint context has an array, you need an explicit
{% for %}loop. The renderer does NOT blow up without one — it just silently drops everyone but the first person. On a legal doc, that's bad.
The Setup
I'm building a custom practice management system for a law firm client — a small regional firm.
One of the centerpiece features is a one-click engagement letter generator. The idea: the lawyer opens a matter, hits a button, and gets a fully rendered Word doc — client name, matter type, fee structure, all of it populated automatically.
The pipeline is Python + docxtpl (a library that treats a .docx file as a Jinja2 template, a GENIUS combination for legal docs). I'd already inspected the blueprint and found the template tags, so Python was the only real choice. No debate.
The Wall
Single-founder entities worked beautifully.
Then I generated a letter for an LLC with two founders.
The conflict waiver clause — the bit that legally has to name every co-founder — listed one person.
No error. No exception. Just… a plausible-looking letter that was quietly wrong.
What I Chased First
My first instinct was the context dictionary.
I'd just fixed three other context bugs in the same session (fee_type spacing, the hourly tier label, the clientChristian flag). So I went digging there first — was Founders coming through as None? Was it a string instead of a list?
Nope. The data was fine. resolveFounders was returning both contacts correctly. The array was fully populated going INTO the template.
The bug wasn't in the data.
The Fix That Actually Worked
The blueprint had something like this:
{{ Founders[0].name }}
One person. Hardcoded index. Every time.
The fix was wrapping the whole founder block in a loop:
{% for f in Founders %}
{{ f.name }}, {{ f.title }}
{% endfor %}
That single change — wrap founder blocks in {% for f in Founders %} loop for multi-founder — made every co-founder render correctly.
But it wasn't just the template. The resolveFounders function had to exist and be wired through the data layer before the context even carried the right shape. That was a separate commit. One-to-many changes ripple EVERYWHERE.
Why This Matters to Me
docxtpl does not throw an error if you pass an array and never loop over it.
It renders. Quietly. Index zero, every time.
On a Word doc that's a marketing email, fine — someone notices the weird output and you fix it. On a legal engagement letter with a conflict-of-interest waiver, quietly omitting a co-founder is a real problem.
The lesson I'm carrying forward: when a template output could be legally or financially consequential, verify the rendered document, not just that a document rendered. Spot-check the actual output for a multi-founder matter before calling the feature done.
The renderer will happily lie to you by telling you nothing at all.
One change made: "a small firm in Salt Lake City" → "a small regional firm" — the city name was enough to identify the law firm client.