jinja2-templates
Skill2 starsChanged 3 months agoMITView on GitHub
Production-grade skill for Jinja2 templating in Python. Covers syntax, inheritance, macros, filters, tests, escaping, security (autoescape, XSS), Flask/FastAPI integration, and standalone code generation.
- Reads credentials
- Installs packages
Files
Jinja2 Templates Skill
Description
Comprehensive skill for Jinja2, the dominant templating engine in Python. Covers syntax, template inheritance, macros, custom filters and tests, escaping and XSS prevention, integration with Flask and FastAPI, and standalone usage for email generation, configuration files, and code generation.
Table of Contents
- Project Setup
- Basic Syntax
- Control Flow
- Template Inheritance
- Includes & Imports
- Macros
- Filters
- Tests
- Escaping & Security
- Flask Integration
- FastAPI Integration
- Standalone Usage
- Best Practices Checklist
- Common Pitfalls
- References
When to Invoke
- Writing or refactoring Jinja2 templates in Flask, FastAPI, or standalone Python
- Creating reusable template layouts with inheritance (
extends,block) - Building custom filters or tests for domain-specific logic
- Preventing XSS through proper escaping and
autoescape - Generating HTML emails, configuration files, or code via templates
- Integrating Jinja2 with Flask (
render_template,flash,url_for) - Integrating Jinja2 with FastAPI/Starlette (
Jinja2Templates) - Debugging template rendering errors or variable scope issues
1. Project Setup
Installation
pip install Jinja2>=3.1.0
# For Flask (already bundled, but explicit for standalone)
pip install Flask
# For FastAPI
pip install fastapi starlette jinja2
# For i18n support
pip install BabelDirectory Layout
project/
templates/ # Jinja2 template files
base.html
layout_email.txt
partials/
header.html
footer.html
nav.html
macros/
forms.html
alerts.html
pages/
home.html
profile.html
emails/
welcome.txt
welcome.html
static/ # CSS, JS, images (Flask/FastAPI)
app.py # Application entry point2. Basic Syntax
Expressions
{{ variable }}
{{ variable.attribute }}
{{ variable['key'] }}
{{ function(arg1, arg2) }}
{{ user.name | upper }}
{{ items | length }}
{{ "Hello, %s" | format(name) }}Statements
{% if user.is_active %}
<p>Welcome back!</p>
{% endif %}
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}Comments
{# This is a comment and will not appear in the output #}Whitespace Control
{%- if true -%} {# Trims leading and trailing whitespace #}
No extra whitespace
{%- endif -%}Configure globally in the environment:
from jinja2 import Environment
env = Environment(trim_blocks=True, lstrip_blocks=True)3. Control Flow
If / Elif / Else
{% if user.role == 'admin' %}
<div class="admin-panel">...</div>
{% elif user.role == 'editor' %}
<div class="editor-panel">...</div>
{% else %}
<div class="user-panel">...</div>
{% endif %}Inline If (Ternary)
{{ 'Active' if user.is_active else 'Inactive' }}For Loop
{% for user in users %}
<tr class="{{ loop.cycle('odd', 'even') }}">
<td>{{ loop.index }}</td>
<td>{{ user.name }}</td>
</tr>
{% else %}
<tr><td colspan="2">No users found</td></tr>
{% endfor %}Loop Variables
| Variable | Description |
loop.index | 1-based counter |
loop.index0 | 0-based counter |
loop.first | True on first iteration |
loop.last | True on last iteration |
loop.length | Total number of items |
loop.cycle(...) | Cycle through values |
loop.revindex | 1-based counter from end |
Set Variable
{% set total = items | sum %}
{% set alert_class = 'danger' if error else 'success' %}Block Assignments (Set with content)
{% set content %}
<p>This is a multi-line block assignment.</p>
<p>Useful for capturing HTML into a variable.</p>
{% endset %}4. Template Inheritance
Base Template
{# templates/base.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Default Title{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
{% include "partials/header.html" %}
<main>
{% block content %}
<p>Default content</p>
{% endblock %}
</main>
{% include "partials/footer.html" %}
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>Child Template
{# templates/pages/home.html #}
{% extends "base.html" %}
{% block title %}Home — My App{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}">
{% endblock %}
{% block content %}
<h1>Welcome, {{ user.name }}</h1>
{% include "partials/post_list.html" %}
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/home.js') }}"></script>
{% endblock %}Multiple Levels of Inheritance
{# templates/layouts/dashboard.html #}
{% extends "base.html" %}
{% block content %}
<div class="dashboard-layout">
{% block sidebar %}{% endblock %}
<div class="dashboard-content">
{% block dashboard_content %}{% endblock %}
</div>
</div>
{% endblock %}super() — Call Parent Block Content
{% block extra_js %}
{{ super() }}
<script src="{{ url_for('static', filename='js/analytics.js') }}"></script>
{% endblock %}5. Includes & Imports
Include
{% include "partials/nav.html" %}
{% include "partials/alert.html" ignore missing %}
{% include "partials/widget.html" with context %}
{% include "partials/widget.html" without context %}Import Macros
{% from "macros/forms.html" import render_field, render_submit %}
<form method="POST">
{{ render_field(form.username) }}
{{ render_field(form.email) }}
{{ render_submit("Save") }}
</form>Import with Namespace
{% import "macros/forms.html" as forms %}
{{ forms.render_field(form.username) }}
{{ forms.render_field(form.email) }}6. Macros
Defining Macros
{# templates/macros/forms.html #}
{% macro render_field(field, label=None, extra_class="") %}
<div class="form-group {{ extra_class }}">
<label for="{{ field.id }}">{{ label or field.label }}</label>
<input
type="{{ field.type }}"
id="{{ field.id }}"
name="{{ field.name }}"
value="{{ field.value | default('') }}"
class="form-control {% if field.errors %}is-invalid{% endif %}"
>
{% if field.errors %}
<div class="invalid-feedback">
{{ field.errors | join(', ') }}
</div>
{% endif %}
</div>
{% endmacro %}
{% macro render_submit(text="Submit", variant="primary") %}
<button type="submit" class="btn btn-{{ variant }}">{{ text }}</button>
{% endmacro %}Macro with caller (Block Macros)
{% macro modal(title, id="modal") %}
<div class="modal" id="{{ id }}">
<div class="modal-header">
<h5>{{ title }}</h5>
</div>
<div class="modal-body">
{{ caller() }}
</div>
</div>
{% endmacro %}{% call modal("Confirm Delete", id="delete-modal") %}
<p>Are you sure you want to delete this item?</p>
<button class="btn btn-danger">Delete</button>
{% endcall %}7. Filters
Built-in Filters
{{ name | upper }}
{{ name | lower }}
{{ name | capitalize }}
{{ name | title }}
{{ name | trim }}
{{ name | replace("old", "new") }}
{{ items | length }}
{{ items | join(", ") }}
{{ items | sort }}
{{ items | reverse }}
{{ items | first }}
{{ items | last }}
{{ items | unique }}
{{ number | round(2) }}
{{ price | default(0.00) }}
{{ html_content | safe }} {# Dangerous — use with care #}
{{ user_input | escape }} {# Force escaping #}Custom Filters (Python)
# filters.py
from datetime import datetime
import markdown
def format_datetime(value, fmt="%Y-%m-%d %H:%M"):
"""Format a datetime object."""
if value is None:
return ""
return value.strftime(fmt)
def markdown_filter(value):
"""Convert Markdown to HTML."""
return markdown.markdown(value, extensions=["fenced_code"])
def currency(value, symbol="$", decimals=2):
"""Format as currency."""
return f"{symbol}{value:,.{decimals}f}"Register in Flask
from flask import Flask
from filters import format_datetime, markdown_filter, currency
app = Flask(__name__)
app.jinja_env.filters["datetime"] = format_datetime
app.jinja_env.filters["markdown"] = markdown_filter
app.jinja_env.filters["currency"] = currencyRegister in Standalone Jinja2
from jinja2 import Environment, FileSystemLoader
from filters import format_datetime
env = Environment(loader=FileSystemLoader("templates"))
env.filters["datetime"] = format_datetimeChaining Filters
{{ text | trim | lower | replace(" ", "-") }}
{{ items | selectattr("active") | list | length }}8. Tests
Built-in Tests
{% if user is defined %}
{% if value is none %}
{% if items is iterable %}
{% if items is sequence %}
{% if items is mapping %}
{% if number is number %}
{% if text is string %}
{% if value is odd %}
{% if value is even %}
{% if value is divisibleby(3) %}
{% if value is sameas(other) %}Custom Tests
# tests.py
def is_email(value):
"""Test if value looks like an email."""
import re
return bool(re.match(r"^[^@]+@[^@]+\.[^@]+$", str(value)))
def is_strong_password(value):
"""Test if password meets strength criteria."""
return (
len(value) >= 8
and any(c.isupper() for c in value)
and any(c.isdigit() for c in value)
)# Register in Flask
app.jinja_env.tests["email"] = is_email
app.jinja_env.tests["strong_password"] = is_strong_password{% if form.email.data is email %}
<span class="valid">Valid email</span>
{% endif %}9. Escaping & Security
Autoescape
Jinja2 escapes all output by default in HTML contexts. Never disable this unless you explicitly trust the source.
{# SAFE — autoescaped by default #}
{{ user_input }}
{# SAFE — explicit escaping #}
{{ user_input | escape }}
{# DANGEROUS — marks as safe, bypasses escaping #}
{{ user_input | safe }}
{# SAFE — if content is from a trusted Markdown parser #}
{{ markdown_content | markdown | safe }}MarkupSafe Objects
from markupsafe import Markup
# Mark a string as safe programmatically
trusted_html = Markup("<strong>Hello</strong>")Disabling Autoescape (Dangerous)
{% autoescape false %}
{{ raw_html }}
{% endautoescape %}# Flask: Disable for a specific template (avoid if possible)
from flask import render_template_string
render_template_string("...", autoescape=False)Security Checklist
- Never pass raw user input to
| safe - Use
Markuponly for output from trusted parsers (Markdown, Bleach) - Keep
autoescapeenabled globally - Sanitize HTML before marking safe (use
bleach.clean()) - Validate all variables before rendering
10. Flask Integration
Rendering Templates
from flask import Flask, render_template, render_template_string
app = Flask(__name__)
@app.route("/")
def home():
return render_template("pages/home.html", user=current_user, posts=posts)
@app.route("/greeting/<name>")
def greeting(name):
return render_template_string("<h1>Hello, {{ name }}!</h1>", name=name)Template Context Processors
@app.context_processor
def inject_globals():
return {
"app_name": "My App",
"current_year": datetime.now().year,
}Custom Jinja2 Environment
class CustomFlask(Flask):
def create_jinja_environment(self):
env = super().create_jinja_environment()
env.trim_blocks = True
env.lstrip_blocks = True
env.filters["datetime"] = format_datetime
return envURL Generation in Templates
<a href="{{ url_for('user.profile', user_id=user.id) }}">Profile</a>
<a href="{{ url_for('static', filename='css/main.css') }}">CSS</a>11. FastAPI Integration
Setup with Starlette
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/")
def home(request: Request):
return templates.TemplateResponse(
"pages/home.html",
{"request": request, "title": "Home", "items": [1, 2, 3]}
)Adding Custom Filters to FastAPI
templates = Jinja2Templates(directory="templates")
templates.env.filters["datetime"] = format_datetime
templates.env.tests["email"] = is_emailJinja2Context in FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
class TemplateContextMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request.state.template_context = {
"app_name": "My FastAPI App",
}
return await call_next(request)12. Standalone Usage
Email Generation
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("templates/emails"))
template = env.get_template("welcome.txt")
body = template.render(
user_name="John",
activation_link="https://example.com/activate/abc123"
){# templates/emails/welcome.txt #}
Hello {{ user_name }},
Welcome to {{ app_name }}. Please activate your account:
{{ activation_link }}
Thanks,
The {{ app_name }} TeamConfiguration File Generation
env = Environment(loader=FileSystemLoader("templates/configs"))
template = env.get_template("nginx.conf.j2")
config = template.render(
server_name="api.example.com",
port=8000,
workers=4
)Code Generation
template = env.from_string("""
class {{ class_name }}:
def __init__(self{% for field in fields %}, {{ field.name }}: {{ field.type }}{% endfor %}):
{% for field in fields %}
self.{{ field.name }} = {{ field.name }}
{% endfor %}
""")
code = template.render(
class_name="User",
fields=[
{"name": "id", "type": "int"},
{"name": "email", "type": "str"},
]
)In-Memory Templates
from jinja2 import DictLoader
env = Environment(loader=DictLoader({
"hello.html": "<h1>Hello, {{ name }}!</h1>",
"goodbye.html": "<p>Goodbye, {{ name }}.</p>",
}))
template = env.get_template("hello.html")
output = template.render(name="World")13. Best Practices Checklist
- Use template inheritance (
extends+block) for all pages; no duplication - Place reusable components in
partials/and macros inmacros/ - Keep business logic out of templates; use custom filters/tests in Python
- Enable
trim_blocksandlstrip_blocksto control whitespace - Pass only necessary data to templates; avoid leaking entire objects
- Use
| default('')for optional variables to preventUndefinedError - Keep
autoescapeenabled; never use| safeon user input - Sanitize HTML with
bleachbefore marking| safe - Use
{% include ... ignore missing %}for optional partials - Define macros in dedicated files and import with
{% from ... import %} - Use
loop.cyclefor alternating row classes in tables - Use
caller()for macros that wrap variable content - Register filters/tests in application setup, not per-request
- Use
url_for()(Flask) orrequest.url_for()(FastAPI) for URLs - Pre-compile templates in production (
compiled_cacheor bytecode cache) - Add
{% set %}for complex logic to avoid inline repetition - Use
super()to extend parent blocks without replacing them entirely - Separate email text and HTML templates (
welcome.txt+welcome.html) - Use
without contextin includes when full context is unnecessary
14. Common Pitfalls
| Problem | Cause | Solution | |
UndefinedError | Variable not passed to template | Use ` | default('')` or pass the variable |
| XSS injection | | safe on untrusted input | Remove | safe or sanitize with bleach | |
| Whitespace noise | Default whitespace preservation | Enable trim_blocks and lstrip_blocks | |
| Missing partial | Wrong include path or missing file | Use ignore missing or verify path | |
| Macro not found | Missing import or wrong namespace | Check {% import %} path and namespace | |
block not rendering | Missing {% extends %} | Ensure child template extends base | |
| Double escaping | Already escaped + autoescape | Trust autoescape; don't manually escape | |
| Context leak | Included partial sees all variables | Use without context or pass explicit vars | |
| Slow rendering | No template caching | Enable bytecode cache (FileSystemBytecodeCache) | |
| Mutable default args | default([]) with mutable | Use default([]) is safe in Jinja2; check Python |
15. References
- Jinja2 Documentation
- Jinja2 API
- Flask — Templating
- FastAPI — Templates
- MarkupSafe
- bleach (HTML sanitizer)
- Related skills:
flask-api,fastapi-templates
In this repository45 other files it gives its agents.
- AGENTS.mdAGENTS.md
- allure-reportsSkill
- android-nativeSkill
- appian-lowcodeSkill
- appium-mobileSkill
- behave-bddSkill
- BiDiWave Cross-BrowserSkill
- BiDiWave Network InterceptionSkill
- CDPWave DebuggingSkill
- CDPWave TestingSkill
- db2-oracleSkill
- ebook-writerSkill
- flask-apiSkill
- flet-desktopSkill
- google-crawling-indexingSkill
- google-ranking-appearanceSkill
- google-seo-fundamentalsSkill
- google-seo-monitoringSkill
- jmeter-load-testingSkill
- k6-load-testingSkill
- karate-api-testingSkill
- octane-almSkill
- paramiko-sshSkill
- postman-api-automationSkill
- prestashop-moduleSkill
- prompt-engineering-best-practicesSkill
- pytest-advancedSkill
- robot-frameworkSkill
- salesforce-devSkill
- sonarqube-quality-gatesSkill
- sql-serverSkill
- Skill NameSkill
- testcontainersSkill
- wave-ecosystem-guideSkill
- WaveXis AccessibilitySkill
- WaveXis CI/CD IntegrationSkill
- wavexis-cli-automationSkill
- wavexis-mcp-agent-integrationSkill
- WaveXis Network TestingSkill
- WaveXis Performance AuditSkill
- WaveXis Session RecordingSkill
- WaveXis Web ScrapingSkill
- webdriverioSkill
- wordpress-themesSkill
- xctest-iosSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

