Jinja2 is a Python template engine. pip install jinja2. String: from jinja2 import Template; t = Template("Hello {{ name }}!"); t.render(name="Alice"). Env: from jinja2 import Environment, FileSystemLoader. env = Environment(loader=FileSystemLoader("templates/")). tmpl = env.get_template("email.html"). output = tmpl.render(user=user). Loop: {% for item in items %}{{ item }}{% endfor %}. If: {% if user.admin %}Admin{% else %}User{% endif %}. Filter: {{ name|upper }}, {{ price|round(2) }}, {{ items|join(", ") }}, {{ value|default("N/A") }}. Custom filter: env.filters["humanize"] = my_fn. Test: {% if x is defined %}. Macro: {% macro input(name, type="text") %}<input name="{{ name }}">{% endmacro %}. Inheritance: {% extends "base.html" %}{% block content %}...{% endblock %}. Include: {% include "partials/nav.html" %}. Globals: env.globals["site_name"] = "MyApp". Autoescape: Environment(autoescape=True) — escapes <>&"'. Strict: Environment(undefined=StrictUndefined) — raises on missing vars. PackageLoader: PackageLoader("mypackage", "templates"). DictLoader: DictLoader({"t.html": "Hello {{ name }}"}). Sandbox: from jinja2.sandbox import SandboxedEnvironment. Comment: {# comment #}. Raw: {% raw %}{{ not interpolated }}{% endraw %}. Strip whitespace: {%- for x in xs -%}. set: {% set total = items|sum %}. Claude Code generates Jinja2 environments, template hierarchies, and email rendering pipelines.
CLAUDE.md for Jinja2
## Jinja2 Stack
- Version: jinja2 >= 3.1 | pip install jinja2
- Env: Environment(loader=FileSystemLoader("templates/"), autoescape=True)
- Render: env.get_template("name.html").render(**context)
- Loop: {% for x in xs %} | Filter: {{ val|default("N/A")|upper }}
- Inherit: {% extends "base.html" %} {% block name %}...{% endblock %}
- Macro: {% macro name(args) %}...{% endmacro %} — reusable partials
- Strict: undefined=StrictUndefined — raises UndefinedError on missing vars
Jinja2 Template Rendering Pipeline
# app/templates_engine.py — Jinja2 environment, filters, macros, and email rendering
from __future__ import annotations
import html
from pathlib import Path
from typing import Any
from jinja2 import (
BaseLoader,
DictLoader,
Environment,
FileSystemLoader,
PackageLoader,
StrictUndefined,
Template,
TemplateNotFound,
select_autoescape,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Environment factory
# ─────────────────────────────────────────────────────────────────────────────
def make_env(template_dir: str | Path | None = None) -> Environment:
"""
FileSystemLoader looks for templates in template_dir.
autoescape=True escapes HTML in {{ variables }} — essential for web output.
StrictUndefined raises UndefinedError on {{ missing_var }} — catches typos early.
"""
loader: BaseLoader
if template_dir:
loader = FileSystemLoader(str(template_dir))
else:
loader = DictLoader({}) # replaced below by register_templates()
env = Environment(
loader=loader,
autoescape=select_autoescape(["html", "htm", "xml"]),
undefined=StrictUndefined,
trim_blocks=True, # strip newline after block tags
lstrip_blocks=True, # strip leading whitespace before block tags
)
_register_filters(env)
_register_globals(env)
return env
def _register_filters(env: Environment) -> None:
"""Register custom Jinja2 filters."""
import humanize as _h
env.filters["naturalsize"] = _h.naturalsize
env.filters["naturaltime"] = _h.naturaltime
env.filters["intcomma"] = _h.intcomma
env.filters["ordinal"] = _h.ordinal
env.filters["currency"] = lambda v, symbol="$": f"{symbol}{v:,.2f}"
env.filters["pct"] = lambda v: f"{v:.1%}"
env.filters["slug"] = lambda s: s.lower().replace(" ", "-")
env.filters["truncate_words"] = lambda s, n=20: " ".join(s.split()[:n]) + ("…" if len(s.split()) > n else "")
def _register_globals(env: Environment) -> None:
"""Add global variables available in all templates."""
from datetime import datetime, timezone
env.globals["now"] = lambda: datetime.now(timezone.utc)
env.globals["site_name"] = "My App"
env.globals["year"] = datetime.now().year
# ─────────────────────────────────────────────────────────────────────────────
# 2. In-memory templates (no files needed)
# ─────────────────────────────────────────────────────────────────────────────
TEMPLATES: dict[str, str] = {
"base.html": """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}{{ site_name }}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
<footer>© {{ year }} {{ site_name }}</footer>
</body>
</html>""",
"email/welcome.html": """\
Hello {{ user.first_name }},
Welcome to {{ site_name }}!
{% if user.plan == "pro" %}
You're on the Pro plan. Here's what you get:
{% for feature in features %}
- {{ feature }}
{% endfor %}
{% else %}
Start with our free tier and upgrade anytime.
{% endif %}
Your account was created {{ user.created_at|naturaltime }}.
Thanks,
The {{ site_name }} Team""",
"report.html": """\
{% extends "base.html" %}
{% block title %}{{ title }} — {{ site_name }}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<p>Generated {{ now()|naturaltime }}</p>
<table>
<thead>
<tr>
{% for col in columns %}<th>{{ col }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for col in columns %}
<td>{{ row[col]|default("—") }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<p>Total: {{ rows|length }} row(s)</p>
{% endblock %}""",
"invoice.txt": """\
INVOICE #{{ invoice.number }}
Date: {{ invoice.date }}
Due: {{ invoice.due_date }}
Bill to: {{ customer.name }}
{{ customer.address }}
{% for item in items %}
{{ item.description|truncate_words(8) }}: {{ item.amount|currency }}
{% endfor %}
──────────────────────────────────
Subtotal: {{ subtotal|currency }}
Tax ({{ tax_rate|pct }}): {{ tax_amount|currency }}
TOTAL: {{ total|currency }}
Thank you for your business!""",
}
def make_dict_env() -> Environment:
"""Environment backed by the in-memory TEMPLATES dict."""
env = Environment(
loader=DictLoader(TEMPLATES),
autoescape=select_autoescape(["html"]),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
_register_filters(env)
_register_globals(env)
return env
# ─────────────────────────────────────────────────────────────────────────────
# 3. Email renderer
# ─────────────────────────────────────────────────────────────────────────────
def render_welcome_email(user: dict) -> str:
"""Render a welcome email from the in-memory template."""
env = make_dict_env()
tmpl = env.get_template("email/welcome.html")
return tmpl.render(
user=user,
features=["Unlimited projects", "Priority support", "Advanced analytics"],
)
def render_invoice(invoice: dict, customer: dict, items: list[dict]) -> str:
"""Render a plain-text invoice."""
env = make_dict_env()
env.autoescape = False # plain text — no escaping needed
subtotal = sum(i["amount"] for i in items)
tax_rate = 0.10
tax_amount = subtotal * tax_rate
total = subtotal + tax_amount
return env.get_template("invoice.txt").render(
invoice=invoice,
customer=customer,
items=items,
subtotal=subtotal,
tax_rate=tax_rate,
tax_amount=tax_amount,
total=total,
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Report builder
# ─────────────────────────────────────────────────────────────────────────────
def render_report(title: str, columns: list[str], rows: list[dict]) -> str:
"""Render an HTML report using base.html inheritance."""
env = make_dict_env()
return env.get_template("report.html").render(
title=title,
columns=columns,
rows=rows,
)
# ─────────────────────────────────────────────────────────────────────────────
# 5. FastAPI integration
# ─────────────────────────────────────────────────────────────────────────────
try:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
# Jinja2Templates wraps FileSystemLoader — used by the Starlette integration
# templates = Jinja2Templates(directory="templates/")
# Custom: use our env with filters
class CustomTemplates:
def __init__(self, directory: str) -> None:
self.env = make_env(directory)
def TemplateResponse(self, name: str, context: dict) -> HTMLResponse:
tmpl = self.env.get_template(name)
return HTMLResponse(tmpl.render(**context))
mini_app = FastAPI()
@mini_app.get("/report", response_class=HTMLResponse)
async def report_page(request: Request) -> HTMLResponse:
rows = [{"Product": f"Widget {i}", "Sales": i * 100} for i in range(5)]
html_content = render_report("Sales Report", ["Product", "Sales"], rows)
return HTMLResponse(html_content)
except ImportError:
pass
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
from datetime import datetime, timedelta, timezone
print("=== Welcome email ===")
user = {
"first_name": "Alice",
"plan": "pro",
"created_at": datetime.now(timezone.utc) - timedelta(minutes=2),
}
email = render_welcome_email(user)
print(email)
print("\n=== Invoice ===")
invoice = render_invoice(
invoice={"number": "INV-0042", "date": "2024-01-15", "due_date": "2024-02-15"},
customer={"name": "Acme Corp", "address": "123 Main St, Springfield"},
items=[
{"description": "Monthly subscription", "amount": 99.00},
{"description": "Additional seats (5 × $10)", "amount": 50.00},
{"description": "Professional support add-on", "amount": 29.99},
],
)
print(invoice)
print("\n=== HTML report (truncated) ===")
report = render_report(
title="Product Performance",
columns=["Product", "Revenue", "Units"],
rows=[
{"Product": "Widget A", "Revenue": "$1,234", "Units": 42},
{"Product": "Gadget B", "Revenue": "$5,678", "Units": 100},
],
)
print(report[:400] + "…")
For the str.format() / f-string alternative — f-strings are excellent for short inline formatting but don’t support loops, conditionals, template inheritance, or reusable macros in external files, making them impractical for multi-block HTML pages or parameterized email templates, while Jinja2’s {% for %} / {% if %} / {% extends %} / {% macro %} tags compose into full page templates where {% block content %} is overridden per page, {% include "nav.html" %} reuses partials, and autoescape=True automatically escapes &, <, > in {{ user_input }} preventing XSS — none of which is possible with f-strings. For the Django templates alternative — Django’s built-in template language uses the same {{ variable }} and {% tag %} syntax but does not allow calling Python functions in templates, while Jinja2’s Environment(undefined=StrictUndefined) raises immediately on missing variables in development, custom env.filters["fn"] = fn registers arbitrary Python callables as template filters in one line, and Jinja2 compiles templates to Python bytecode so repeated tmpl.render() calls are faster than Django’s interpreted template engine. The Claude Skills 360 bundle includes Jinja2 skill sets covering Environment with FileSystemLoader and DictLoader, autoescape with select_autoescape, StrictUndefined for strict mode, custom filter registration, global variable injection, template inheritance with extends/block, include for partials, macro for reusable fragments, trim_blocks and lstrip_blocks for clean whitespace, email and invoice rendering patterns, and FastAPI Jinja2Templates integration. Start with the free tier to try template engine code generation.