Jinja2 Templating
A comprehensive guide to Jinja2 template syntax, control structures, and patterns for configuration management.
Jinja2 Templating
A comprehensive guide to Jinja2 template syntax, control structures, and patterns for configuration management.
Overview
Jinja2 is a modern templating engine for Python, widely used in web frameworks (Flask, Django), configuration management tools (Ansible, SaltStack), and static site generators. It provides a powerful yet intuitive syntax for generating dynamic content from templates.
flowchart LR
A[Template File] --> B[Jinja2 Engine]
C[Context Data] --> B
B --> D[Rendered Output]
subgraph "Template Elements"
E["{{ variables }}"]
F["{% statements %}"]
G["{# comments #}"]
end
Key Features
- Sandboxed execution - Safe template rendering
- Template inheritance - Reusable layouts with blocks
- Automatic escaping - XSS protection for HTML
- Extensible - Custom filters, tests, and extensions
Template Syntax
Variables, expressions, and basic delimiters for template content.
Delimiters
| Delimiter | Purpose | Example |
|---|---|---|
{{ }} |
Output expressions | {{ user.name }} |
{% %} |
Statements/logic | {% if active %} |
{# #} |
Comments | {# TODO: fix this #} |
Variables and Expressions
{# Simple variable output #}
Hello, {{ username }}!
{# Attribute access (dot notation) #}
{{ user.email }}
{{ server.config.port }}
{# Dictionary access (bracket notation) #}
{{ user['email'] }}
{{ config['database']['host'] }}
{# Both notations are interchangeable #}
{{ user.name }} is the same as {{ user['name'] }}
{# Default values for undefined variables #}
{{ username | default('Guest') }}
{# Mathematical expressions #}
{{ price * quantity }}
{{ (subtotal + tax) * discount }}
{# String concatenation #}
{{ first_name ~ ' ' ~ last_name }}
{# Comparisons return boolean #}
{{ age >= 18 }}
Whitespace Control
{# Strip whitespace before tag #}
{%- if condition %}
{# Strip whitespace after tag #}
{% if condition -%}
{# Strip both sides #}
{%- if condition -%}
{# Practical example - clean YAML output #}
servers:
{%- for server in servers %}
- {{ server.name }}
{%- endfor %}
Control Structures
Conditionals, loops, and flow control for template logic.
flowchart TD
A[Control Structures] --> B[Conditionals]
A --> C[Loops]
A --> D[Assignments]
B --> B1["if/elif/else"]
C --> C1["for loops"]
C --> C2["loop variables"]
D --> D1["set statements"]
Conditionals
{# Basic if statement #}
{% if user.is_admin %}
<span class="admin-badge">Admin</span>
{% endif %}
{# if/else #}
{% if items %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
<p>No items found.</p>
{% endif %}
{# if/elif/else chain #}
{% if user.role == 'admin' %}
Full access granted
{% elif user.role == 'editor' %}
Edit access granted
{% elif user.role == 'viewer' %}
Read-only access
{% else %}
No access
{% endif %}
{# Inline conditional (ternary) #}
{{ 'active' if is_active else 'inactive' }}
{# Multiple conditions #}
{% if user and user.is_verified and user.age >= 18 %}
Access granted
{% endif %}
{# Negation #}
{% if not user.is_banned %}
Welcome back!
{% endif %}
{# Testing for defined/undefined #}
{% if variable is defined %}
{{ variable }}
{% endif %}
{% if optional_value is not defined %}
Using default configuration
{% endif %}
For Loops
{# Basic iteration #}
{% for item in items %}
{{ item }}
{% endfor %}
{# Iterating with index #}
{% for user in users %}
{{ loop.index }}. {{ user.name }}
{% endfor %}
{# Dictionary iteration #}
{% for key, value in config.items() %}
{{ key }}: {{ value }}
{% endfor %}
{# Empty loop handling #}
{% for item in items %}
{{ item }}
{% else %}
No items to display.
{% endfor %}
{# Conditional loop (filtering) #}
{% for user in users if user.is_active %}
{{ user.name }}
{% endfor %}
{# Loop controls (require the loop_controls extension: #}
{# Environment(extensions=['jinja2.ext.loopcontrols']); enabled by #}
{# default in Ansible) #}
{% for item in items %}
{% if item.skip %}
{% continue %}
{% endif %}
{% if item.stop %}
{% break %}
{% endif %}
{{ item.name }}
{% endfor %}
Loop Variables
| Variable | Description |
|---|---|
loop.index |
Current iteration (1-indexed) |
loop.index0 |
Current iteration (0-indexed) |
loop.revindex |
Iterations from end (1-indexed) |
loop.revindex0 |
Iterations from end (0-indexed) |
loop.first |
True if first iteration |
loop.last |
True if last iteration |
loop.length |
Total number of items |
loop.cycle() |
Cycle through values |
loop.depth |
Nesting level (starts at 1) |
loop.previtem |
Previous item (undefined on first) |
loop.nextitem |
Next item (undefined on last) |
{# Practical loop variable examples #}
<table>
{% for row in data %}
<tr class="{{ loop.cycle('odd', 'even') }}">
<td>{{ loop.index }}</td>
<td>{{ row.name }}</td>
</tr>
{% endfor %}
</table>
{# Comma-separated list #}
{% for tag in tags %}
{{ tag }}{% if not loop.last %}, {% endif %}
{% endfor %}
{# JSON array output #}
[
{% for item in items %}
"{{ item }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
Assignments
{# Simple assignment #}
{% set username = 'admin' %}
{# Multiple assignments #}
{% set x, y, z = 1, 2, 3 %}
{# Assignment from expression #}
{% set full_name = first_name ~ ' ' ~ last_name %}
{# Block assignment for complex content #}
{% set navigation %}
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
{% endset %}
{# Namespace for loop-scoped variables #}
{% set ns = namespace(total=0) %}
{% for item in items %}
{% set ns.total = ns.total + item.price %}
{% endfor %}
Total: {{ ns.total }}
Filters and Tests
Transform data and check conditions with built-in and custom filters.
Essential Filters
{# String manipulation #}
{{ name | upper }} {# JOHN #}
{{ name | lower }} {# john #}
{{ name | title }} {# John Smith #}
{{ name | capitalize }} {# John #}
{{ text | trim }} {# Remove whitespace #}
{{ text | striptags }} {# Remove HTML tags #}
{{ slug | replace('-', '_') }} {# Replace characters #}
{# Numeric formatting #}
{{ price | round(2) }} {# 19.99 #}
{{ value | abs }} {# Absolute value #}
{{ num | int }} {# Convert to integer #}
{{ num | float }} {# Convert to float #}
{# List operations #}
{{ items | length }} {# Count items #}
{{ items | first }} {# First element #}
{{ items | last }} {# Last element #}
{{ items | join(', ') }} {# Join with delimiter #}
{{ items | sort }} {# Sort ascending #}
{{ items | reverse }} {# Reverse order #}
{{ items | unique }} {# Remove duplicates #}
{{ items | random }} {# Random element #}
{# Default values #}
{{ value | default('N/A') }}
{{ value | d('N/A') }} {# Short form #}
{{ value | default('N/A', true) }} {# Also for falsy values #}
{# JSON handling #}
{{ data | tojson }}
{{ data | tojson(indent=2) }}
{# Escaping #}
{{ html | e }} {# HTML escape #}
{{ url | urlencode }} {# URL encode #}
Filter Chaining
{# Multiple filters applied left to right #}
{{ names | sort | join(', ') | title }}
{# Complex transformation #}
{{ users | selectattr('active', 'true') | map(attribute='name') | list | join(', ') }}
{# Formatted output #}
{{ description | truncate(100) | title | trim }}
Selection Filters
{# Filter lists by attribute #}
{% for user in users | selectattr('is_active') %}
{{ user.name }}
{% endfor %}
{# Filter by attribute value #}
{% for item in items | selectattr('status', 'equalto', 'published') %}
{{ item.title }}
{% endfor %}
{# Reject items #}
{% for user in users | rejectattr('is_banned') %}
{{ user.name }}
{% endfor %}
{# Map to extract attributes #}
{{ users | map(attribute='email') | list }}
{# Select/reject with tests #}
{{ numbers | select('even') | list }}
{{ values | reject('none') | list }}
Built-in Tests
{# Type tests #}
{% if value is string %}
{% if value is number %}
{% if value is integer %}
{% if value is float %}
{% if value is mapping %} {# dict-like #}
{% if value is iterable %}
{% if value is sequence %}
{% if value is callable %}
{# Value tests #}
{% if value is defined %}
{% if value is none %}
{% if value is true %}
{% if value is false %}
{% if value is sameas(other) %}
{# Numeric tests #}
{% if num is even %}
{% if num is odd %}
{% if num is divisibleby(3) %}
{# String tests #}
{% if name is lower %}
{% if name is upper %}
{# Container tests #}
{% if item is in collection %}
{% if value is eq(other) %} {# equality #}
{% if value is ne(other) %} {# not equal #}
{% if value is lt(10) %} {# less than #}
{% if value is gt(0) %} {# greater than #}
{% if value is le(100) %} {# less or equal #}
{% if value is ge(1) %} {# greater or equal #}
Template Inheritance
Create reusable layouts with parent templates and overridable blocks.
flowchart TD
A[base.html] --> B[page.html]
A --> C[admin.html]
B --> D[home.html]
B --> E[about.html]
C --> F[dashboard.html]
subgraph "base.html"
G["block head"]
H["block content"]
I["block footer"]
end
Base Template
{# base.html - Parent template #}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="/css/main.css">
{% endblock %}
</head>
<body>
<header>
{% block header %}
<nav>Site Navigation</nav>
{% endblock %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
<p>© 2024 My Site</p>
{% endblock %}
</footer>
{% block scripts %}
<script src="/js/main.js"></script>
{% endblock %}
</body>
</html>
Child Template
{# page.html - Child template #}
{% extends "base.html" %}
{% block title %}My Page - {{ super() }}{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/css/page.css">
{% endblock %}
{% block content %}
<h1>Welcome to My Page</h1>
<p>This content replaces the parent block.</p>
{% endblock %}
{# footer block not overridden - uses parent's content #}
{% block scripts %}
{{ super() }}
<script src="/js/page.js"></script>
{% endblock %}
Advanced Inheritance
{# Multiple levels of inheritance #}
{% extends "layouts/base.html" %}
{# Dynamic parent template #}
{% extends parent_template %}
{# Conditional inheritance #}
{% extends "admin.html" if user.is_admin else "base.html" %}
{# Named block scoping #}
{% block sidebar %}
<h2>Navigation</h2>
{% block sidebar_content %}{% endblock %}
{% endblock %}
{# Using block content in expressions #}
{% set page_title %}{% block title %}{% endblock %}{% endset %}
Include and Import
{# Include another template #}
{% include 'header.html' %}
{# Include with context #}
{% include 'sidebar.html' with context %}
{# Include without context #}
{% include 'widget.html' without context %}
{# Ignore if template missing #}
{% include 'optional.html' ignore missing %}
{# Include from list (first found) #}
{% include ['custom/header.html', 'default/header.html'] %}
{# Import macros from another file #}
{% import 'forms.html' as forms %}
{{ forms.input('username') }}
{# Import specific macros #}
{% from 'forms.html' import input, textarea %}
{{ input('email') }}
Working with Lists and Dictionaries
Manipulate complex data structures effectively.
List Operations
{# Access elements #}
{{ items[0] }} {# First element #}
{{ items[-1] }} {# Last element #}
{{ items[1:3] }} {# Slice #}
{# Check membership #}
{% if 'admin' in roles %}
{# List comprehension-style filtering #}
{% set active_users = users | selectattr('active') | list %}
{# Concatenate lists #}
{% set all_items = list1 + list2 %}
{# Batch into groups #}
{% for row in items | batch(3) %}
<div class="row">
{% for item in row %}
<div class="col">{{ item }}</div>
{% endfor %}
</div>
{% endfor %}
{# Slice into columns #}
{% for column in items | slice(3) %}
<div class="column">
{% for item in column %}
{{ item }}
{% endfor %}
</div>
{% endfor %}
{# Group by attribute #}
{% for category, items in products | groupby('category') %}
<h2>{{ category }}</h2>
{% for item in items %}
<p>{{ item.name }}</p>
{% endfor %}
{% endfor %}
{# Sort by attribute #}
{% for user in users | sort(attribute='name') %}
{% for user in users | sort(attribute='created', reverse=true) %}
Dictionary Operations
{# Access values #}
{{ config.database.host }}
{{ config['database']['port'] }}
{# Get with default #}
{{ config.get('timeout', 30) }}
{# Iterate keys #}
{% for key in config %}
{{ key }}
{% endfor %}
{# Iterate key-value pairs #}
{% for key, value in config.items() %}
{{ key }}: {{ value }}
{% endfor %}
{# Check key existence #}
{% if 'database' in config %}
{# Merge dictionaries (combine is an Ansible filter, not core Jinja2; #}
{# in plain Jinja2 use dict(dict1, **dict2) or the | items approach) #}
{% set merged = dict1 | combine(dict2) %}
{# Convert to items #}
{{ config | dictsort }} {# Sort by key #}
{{ config | dictsort(by='value') }} {# Sort by value #}
{# Extract values #}
{{ servers | map(attribute='hostname') | list }}
Nested Data Structures
{# Navigate nested structures safely #}
{{ data.users[0].address.city | default('Unknown') }}
{# Iterate nested lists #}
{% for group in data.groups %}
<h2>{{ group.name }}</h2>
{% for member in group.members %}
<p>{{ member.name }} - {{ member.email }}</p>
{% endfor %}
{% endfor %}
{# Build complex structures #}
{% set server_config = {
'hostname': hostname,
'port': port | default(8080),
'ssl': ssl_enabled | default(false)
} %}
Common Patterns for Configuration Management
Practical patterns for Ansible, SaltStack, and general config generation.
Ansible Configuration Templates
{# nginx.conf.j2 #}
user {{ nginx_user | default('www-data') }};
worker_processes {{ ansible_processor_vcpus | default(1) }};
http {
{% for upstream in upstreams %}
upstream {{ upstream.name }} {
{% for server in upstream.servers %}
server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }};
{% endfor %}
}
{% endfor %}
{% for vhost in virtual_hosts %}
server {
listen {{ vhost.port | default(80) }};
server_name {{ vhost.domains | join(' ') }};
root {{ vhost.root }};
{% if vhost.ssl is defined and vhost.ssl %}
ssl_certificate {{ vhost.ssl_cert }};
ssl_certificate_key {{ vhost.ssl_key }};
{% endif %}
{% for location in vhost.locations | default([]) %}
location {{ location.path }} {
{% for directive in location.directives %}
{{ directive }};
{% endfor %}
}
{% endfor %}
}
{% endfor %}
}
Environment-Specific Configuration
{# config.yaml.j2 #}
environment: {{ env }}
database:
host: {{ db_host }}
port: {{ db_port | default(5432) }}
name: {{ db_name }}
{% if env == 'production' %}
pool_size: 20
ssl_mode: require
{% else %}
pool_size: 5
ssl_mode: disable
{% endif %}
logging:
level: {{ 'INFO' if env == 'production' else 'DEBUG' }}
{% if env == 'production' %}
handlers:
- file
- syslog
{% else %}
handlers:
- console
{% endif %}
features:
{% for feature, enabled in feature_flags.items() %}
{{ feature }}: {{ enabled | lower }}
{% endfor %}
Kubernetes Manifests
{# deployment.yaml.j2 #}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ app_name }}
labels:
app: {{ app_name }}
version: {{ app_version }}
spec:
replicas: {{ replicas | default(3) }}
selector:
matchLabels:
app: {{ app_name }}
template:
metadata:
labels:
app: {{ app_name }}
spec:
containers:
- name: {{ app_name }}
image: {{ image_registry }}/{{ image_name }}:{{ image_tag }}
ports:
- containerPort: {{ container_port }}
env:
{% for key, value in env_vars.items() %}
- name: {{ key }}
value: "{{ value }}"
{% endfor %}
{% if secrets is defined %}
envFrom:
- secretRef:
name: {{ app_name }}-secrets
{% endif %}
resources:
requests:
memory: {{ memory_request | default('128Mi') }}
cpu: {{ cpu_request | default('100m') }}
limits:
memory: {{ memory_limit | default('256Mi') }}
cpu: {{ cpu_limit | default('500m') }}
Dynamic Host Inventory
{# hosts.ini.j2 #}
{% for group_name, hosts in groups.items() %}
[{{ group_name }}]
{% for host in hosts %}
{{ host.hostname }} ansible_host={{ host.ip }}{% if host.port is defined %} ansible_port={{ host.port }}{% endif %}
{% endfor %}
{% if hostvars[group_name] is defined %}
[{{ group_name }}:vars]
{% for key, value in hostvars[group_name].items() %}
{{ key }}={{ value }}
{% endfor %}
{% endif %}
{% endfor %}
Conditional Blocks Pattern
{# Include sections based on features #}
{% if monitoring_enabled | default(false) %}
monitoring:
endpoint: {{ monitoring_endpoint }}
interval: {{ monitoring_interval | default(60) }}
metrics:
{% for metric in monitored_metrics %}
- {{ metric }}
{% endfor %}
{% endif %}
{% if caching_enabled | default(false) %}
cache:
type: {{ cache_type | default('redis') }}
host: {{ cache_host }}
ttl: {{ cache_ttl | default(3600) }}
{% endif %}
Macros and Reusable Components
Create reusable template functions for DRY code.
Basic Macros
{# Define a macro #}
{% macro input(name, value='', type='text', placeholder='') %}
<input type="{{ type }}"
name="{{ name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
class="form-control">
{% endmacro %}
{# Use the macro #}
{{ input('username', placeholder='Enter username') }}
{{ input('password', type='password') }}
{{ input('email', type='email', placeholder='your@email.com') }}
Macros with Complex Content
{# Form field with label and validation #}
{% macro form_field(name, label, type='text', required=false, value='', errors=[]) %}
<div class="form-group {% if errors %}has-error{% endif %}">
<label for="{{ name }}">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
<input type="{{ type }}"
id="{{ name }}"
name="{{ name }}"
value="{{ value }}"
{% if required %}required{% endif %}
class="form-control">
{% if errors %}
<ul class="error-list">
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{# Alert component #}
{% macro alert(message, type='info', dismissible=true) %}
<div class="alert alert-{{ type }} {% if dismissible %}alert-dismissible{% endif %}">
{% if dismissible %}
<button type="button" class="close" data-dismiss="alert">×</button>
{% endif %}
{{ message }}
</div>
{% endmacro %}
Macros with Caller
{# Card component with caller for content #}
{% macro card(title, footer=none) %}
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
{{ caller() }}
</div>
{% if footer %}
<div class="card-footer">
{{ footer }}
</div>
{% endif %}
</div>
{% endmacro %}
{# Use with call block #}
{% call card('User Profile') %}
<p>Name: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
<p>Role: {{ user.role }}</p>
{% endcall %}
{# Modal component #}
{% macro modal(id, title) %}
<div class="modal" id="{{ id }}">
<div class="modal-header">
<h2>{{ title }}</h2>
<button class="close">×</button>
</div>
<div class="modal-body">
{{ caller() }}
</div>
</div>
{% endmacro %}
{% call modal('confirm-delete', 'Confirm Deletion') %}
<p>Are you sure you want to delete this item?</p>
<button class="btn btn-danger">Delete</button>
<button class="btn btn-secondary">Cancel</button>
{% endcall %}
Organising Macros in Files
{# macros/forms.html #}
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro textarea(name, value='', rows=5) %}
<textarea name="{{ name }}" rows="{{ rows }}">{{ value }}</textarea>
{% endmacro %}
{% macro select(name, options, selected='') %}
<select name="{{ name }}">
{% for value, label in options %}
<option value="{{ value }}" {% if value == selected %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
{% endmacro %}
{# Using the macro library #}
{% import 'macros/forms.html' as forms %}
<form method="post">
{{ forms.input('username') }}
{{ forms.input('password', type='password') }}
{{ forms.textarea('bio', rows=3) }}
{{ forms.select('country', [('uk', 'United Kingdom'), ('us', 'United States')]) }}
</form>
Recursive Macros
{# Render nested menu structure #}
{% macro render_menu(items) %}
<ul>
{% for item in items %}
<li>
<a href="{{ item.url }}">{{ item.title }}</a>
{% if item.children %}
{{ render_menu(item.children) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}
{{ render_menu(navigation) }}
{# Render nested comments #}
{% macro render_comments(comments, depth=0) %}
{% for comment in comments %}
<div class="comment" style="margin-left: {{ depth * 20 }}px">
<strong>{{ comment.author }}</strong>
<p>{{ comment.text }}</p>
{% if comment.replies %}
{{ render_comments(comment.replies, depth + 1) }}
{% endif %}
</div>
{% endfor %}
{% endmacro %}
Debugging Templates
Techniques for troubleshooting and inspecting template rendering.
Debug Output
{# Print variable type and value #}
<pre>{{ variable | pprint }}</pre>
{# Dump the whole render context (requires the debug extension: #}
{# Environment(extensions=['jinja2.ext.debug']); enabled by default #}
{# in Flask). Prints context + filters + tests. #}
{% debug %}
{# Type checking #}
Variable type: {{ variable.__class__.__name__ }}
Python-side Debugging
from jinja2 import Environment, FileSystemLoader, meta
# Enable debug mode
env = Environment(
loader=FileSystemLoader('templates'),
undefined=DebugUndefined # Show undefined variable names
)
# Use StrictUndefined to raise errors on undefined variables
from jinja2 import StrictUndefined
env = Environment(
loader=FileSystemLoader('templates'),
undefined=StrictUndefined
)
# Find undefined variables in template
template_source = env.loader.get_source(env, 'template.html')[0]
ast = env.parse(template_source)
undefined = meta.find_undeclared_variables(ast)
print(f"Required variables: {undefined}")
# List all variables passed to template
@app.context_processor
def debug_context():
def show_context():
return dict(request.environ)
return {'show_context': show_context}
Common Debug Patterns
{# Check if variable exists #}
{% if variable is defined %}
Variable exists: {{ variable }}
{% else %}
Variable is undefined
{% endif %}
{# Safe attribute access #}
{{ object.attribute | default('Not set') }}
{# Log to console (browser) #}
<script>
console.log('Debug:', {{ data | tojson | safe }});
</script>
{# Temporary debug block #}
{% if debug_mode %}
<div class="debug-panel">
<h4>Debug Info</h4>
<pre>{{ config | tojson(indent=2) }}</pre>
</div>
{% endif %}
{# Check loop state #}
{% for item in items %}
Loop index: {{ loop.index }}/{{ loop.length }}
First: {{ loop.first }}, Last: {{ loop.last }}
{% endfor %}
Ansible Template Debugging
# Check template syntax
ansible-playbook playbook.yml --syntax-check
# Render template locally
ansible localhost -m template -a "src=template.j2 dest=/tmp/output.txt"
# Debug with increased verbosity
ansible-playbook playbook.yml -vvv
# Check variable values
- debug:
var: my_variable
- debug:
msg: "Variable value: {{ my_variable }}"
Quick Reference
Syntax Overview
| Element | Syntax | Example |
|---|---|---|
| Variable | {{ }} |
{{ user.name }} |
| Statement | {% %} |
{% if active %} |
| Comment | {# #} |
{# note #} |
| Escape raw | {% raw %}{% endraw %} |
Show literal {{ }} |
| Whitespace strip | - |
{%- if -%} |
Essential Filters
| Filter | Description | Example |
|---|---|---|
default(val) |
Default if undefined | {{ x | default(0) }} |
length |
Count items | {{ list | length }} |
join(sep) |
Join list | {{ items | join(', ') }} |
sort |
Sort items | {{ items | sort }} |
first / last |
First/last item | {{ items | first }} |
upper / lower |
Change case | {{ name | upper }} |
trim |
Strip whitespace | {{ text | trim }} |
replace(a, b) |
Replace string | {{ s | replace('-', '_') }} |
tojson |
Convert to JSON | {{ data | tojson }} |
selectattr |
Filter by attr | {{ items | selectattr('active') }} |
Control Structures
{# Conditionals #}
{% if condition %}...{% elif other %}...{% else %}...{% endif %}
{# Loops #}
{% for item in items %}...{% else %}...{% endfor %}
{# Assignment #}
{% set variable = value %}
{# Include #}
{% include 'file.html' %}
{# Import macros #}
{% import 'macros.html' as m %}
{# Inheritance #}
{% extends 'base.html' %}
{% block name %}...{% endblock %}
Common Tests
| Test | Description | Example |
|---|---|---|
defined |
Variable exists | {% if x is defined %} |
none |
Value is None | {% if x is none %} |
true / false |
Boolean value | {% if x is true %} |
even / odd |
Number parity | {% if x is even %} |
iterable |
Can iterate | {% if x is iterable %} |
mapping |
Dict-like | {% if x is mapping %} |
string / number |
Type check | {% if x is string %} |
Common Issues and Solutions
Undefined Variable Errors
Issue: UndefinedError: 'variable' is undefined
{# Solution 1: Use default filter #}
{{ variable | default('fallback') }}
{# Solution 2: Check if defined #}
{% if variable is defined %}
{{ variable }}
{% endif %}
{# Solution 3: Use default for boolean context #}
{% if variable | default(false) %}
Whitespace Problems in Output
Issue: Unwanted blank lines in YAML/INI output
{# Problem #}
{% for item in items %}
{{ item }}
{% endfor %}
{# Solution: Use whitespace control #}
{%- for item in items %}
{{ item }}
{%- endfor %}
Variable Scope in Loops
Issue: Cannot modify variable inside loop
{# Problem: This won't work #}
{% set total = 0 %}
{% for item in items %}
{% set total = total + item.price %} {# Creates new scoped variable #}
{% endfor %}
{{ total }} {# Still 0 #}
{# Solution: Use namespace #}
{% set ns = namespace(total=0) %}
{% for item in items %}
{% set ns.total = ns.total + item.price %}
{% endfor %}
{{ ns.total }} {# Correct sum #}
Escaping Template Syntax
Issue: Need to output literal {{ }} characters
{# Solution: Use raw block #}
{% raw %}
This shows literal {{ variable }} syntax
{% endraw %}
{# Or escape with variable #}
{{ '{{' }} variable {{ '}}' }}
Dictionary Key Errors
Issue: KeyError when accessing missing dictionary keys
{# Problem #}
{{ config['missing_key'] }}
{# Solution 1: Use get() method #}
{{ config.get('missing_key', 'default') }}
{# Solution 2: Default filter #}
{{ config.missing_key | default('default') }}
{# Solution 3: Check existence #}
{% if 'missing_key' in config %}
{{ config['missing_key'] }}
{% endif %}
Boolean Conversion Issues
Issue: String 'false' is truthy in conditions
{# Problem #}
{% if 'false' %} {# This is truthy! #}
{# Solution: compare explicitly (the bool filter is Ansible-only, #}
{# not core Jinja2) #}
{% if value == true %}
{% if value | lower == 'true' %}
Macro Not Found Errors
Issue: TemplateAssertionError when importing macros
{# Problem: Wrong import syntax #}
{% import 'macros.html' %} {# Missing 'as' clause #}
{# Solution 1: Import with alias #}
{% import 'macros.html' as macros %}
{# Solution 2: Import specific macros #}
{% from 'macros.html' import input, textarea %}
Performance Issues with Large Loops
Issue: Template rendering is slow
{# Problem: Complex operations in loop #}
{% for item in items | sort | reverse | unique %}
{# Solution: Pre-process in Python #}
{# Python #}
context['sorted_items'] = sorted(set(items), reverse=True)
{# Template #}
{% for item in sorted_items %}
{# Also consider: batch processing #}
{% for batch in large_list | batch(100) %}
{% for item in batch %}...{% endfor %}
{% endfor %}
Inheritance Block Not Rendering
Issue: Child template content not appearing
{# Problem: Missing extends or block doesn't exist in parent #}
{# Solution: Ensure extends is first tag #}
{% extends "base.html" %} {# Must be first! #}
{% block content %}
{# This must match a block name in the parent #}
{% endblock %}
Related Topics: Ansible, Python, Flask, Configuration Management, YAML, Kubernetes