| name | run2_docx-conditional-sections |
| description | Handle {{IF_X}}...{{END_IF_X}} conditional blocks in Word .docx templates — keep or remove content, with correct ordering relative to placeholder replacement. |
Conditional Section Handling in Word Templates (Improved)
Pattern
Templates use conditional blocks like:
{{IF_RELOCATION}}You qualify for a ${{RELOCATION_AMOUNT}} relocation package for {{RELOCATION_DAYS}} days.{{END_IF_RELOCATION}}
Critical: Process Conditionals BEFORE General Placeholder Replacement
Conditionals must be resolved first because:
- The inner content may contain placeholders (e.g.,
{{RELOCATION_AMOUNT}}) that need
to be replaced only if the section is kept.
- If you replace placeholders first and then try to remove the section, the markers
remain and the content is already substituted in.
Step 1: Inspect the Template
Always inspect before coding — the marker positions affect which handler to use:
from docx import Document
doc = Document('template.docx')
for i, para in enumerate(doc.paragraphs):
if 'IF_' in para.text or 'END_IF' in para.text:
print(f'Para[{i}]: {repr(para.text)}')
for j, run in enumerate(para.runs):
print(f' Run[{j}]: {repr(run.text)}')
Case A: Same Paragraph (markers + content in one paragraph)
Identified when para.text contains both {{IF_X}} and {{END_IF_X}}.
def handle_conditional_same_para(para, condition_key, should_include, data):
"""
Handle a conditional block that is entirely within one paragraph.
Word may put markers in separate runs, but para.text concatenates them,
so string slicing on para.text works correctly.
"""
start = '{{IF_' + condition_key + '}}'
end = '{{END_IF_' + condition_key + '}}'
text = para.text
if start not in text or end not in text:
return False
if should_include:
inner_start = text.index(start) + len(start)
inner_end = text.index(end)
inner = text[inner_start:inner_end]
for key, value in data.items():
inner = inner.replace('{{' + key + '}}', str(value))
prefix = text[:text.index(start)]
suffix = text[text.index(end) + len(end):]
new_text = prefix + inner + suffix
else:
new_text = ''
if para.runs:
para.runs[0].text = new_text
for run in para.runs[1:]:
run.text = ''
return True
Case B: Multi-Paragraph Block (markers on separate lines)
Identified when markers are in different paragraphs.
def handle_conditional_multi_para(doc, condition_key, should_include, data):
"""
Handle a conditional block spanning multiple paragraphs.
Note: Clearing runs leaves empty paragraphs (blank lines). To fully remove
paragraphs from the document XML, use the lxml approach shown below.
"""
start = '{{IF_' + condition_key + '}}'
end = '{{END_IF_' + condition_key + '}}'
paras = doc.paragraphs
start_idx = next((i for i, p in enumerate(paras) if start in p.text), None)
end_idx = next((i for i, p in enumerate(paras) if end in p.text), None)
if start_idx is None or end_idx is None:
return
if should_include:
_clear_para(paras[start_idx])
_clear_para(paras[end_idx])
for para in paras[start_idx + 1:end_idx]:
text = para.text
for key, value in data.items():
text = text.replace('{{' + key + '}}', str(value))
if text != para.text and para.runs:
para.runs[0].text = text
run para.runs[:]:
run.text =
:
para paras[start_idx:end_idx + ]:
_clear_para(para)
():
run para.runs:
run.text =
Auto-Detection and Dispatch
def handle_conditional(doc, condition_key, should_include, data):
"""Auto-detect single vs multi-paragraph conditional and handle accordingly."""
start = '{{IF_' + condition_key + '}}'
end = '{{END_IF_' + condition_key + '}}'
for para in doc.paragraphs:
if start in para.text and end in para.text:
handle_conditional_same_para(para, condition_key, should_include, data)
return
elif start in para.text or end in para.text:
handle_conditional_multi_para(doc, condition_key, should_include, data)
return
Complete Example
import json
from docx import Document
with open('employee_data.json') as f:
data = json.load(f)
doc = Document('template.docx')
relocation_yes = data.get('RELOCATION_PACKAGE', '').strip().lower() == 'yes'
handle_conditional(doc, 'RELOCATION', relocation_yes, data)
for para in doc.paragraphs:
replace_in_paragraph(para, data)
doc.save('output.docx')
Key Insights from Real Templates
- Even in the "same paragraph" case, the markers (
{{IF_X}}, {{END_IF_X}}) are
often in separate runs. This is fine because we use para.text for detection
and string slicing — run structure doesn't matter.
- The
{{RELOCATION_PACKAGE}} key in data is a flag field (value "Yes"/"No"),
not a placeholder in the template. Check it separately to decide should_include.
- Always test with
RELOCATION_PACKAGE = "No" case too.