Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

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.

Template Elements{{ variables }}{% statements %}{# comments #}Template FileJinja2 EngineContext DataRendered OutputTemplate Elements{{ variables }}{% statements %}{# comments #}Template FileJinja2 EngineContext DataRendered Output

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.

Control StructuresConditionalsLoopsAssignmentsif/elif/elsefor loopsloop variablesset statementsControl StructuresConditionalsLoopsAssignmentsif/elif/elsefor loopsloop variablesset 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.

base.htmlblock headblock contentblock footerbase.htmlpage.htmladmin.htmlhome.htmlabout.htmldashboard.htmlbase.htmlblock headblock contentblock footerbase.htmlpage.htmladmin.htmlhome.htmlabout.htmldashboard.html

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>&copy; 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">&times;</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">&times;</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