Python Template Engines — Jinja2 & String Templates
Templating generates dynamic text from templates with variable substitution and logic. Essential for HTML generation, email templates, configuration files, and report generation. This tutorial covers string.Template, Jinja2 fundamentals, template inheritance, filters, macros, and real-world use cases.
Learning Objectives
- Use Python's string.Template for simple substitution
- Master Jinja2 for complex templating
- Apply template inheritance for reusable layouts
- Create custom filters and macros
- Generate HTML, emails, and reports
- Build a dynamic report generator
string.Template (Standard Library)
from string import Template
# Simple substitution
template = Template("Hello, $name! Your balance is $${amount}.")
result = template.substitute(name="Alice", amount=1000)
print(result) # Hello, Alice! Your balance is $1000.
# Safe substitution (no error on missing keys)
result = template.safe_substitute(name="Bob")
print(result) # Hello, Bob! Your balance is $${amount}.
# From a file
template = Template(open("email.txt").read())
result = template.substitute(name="Charlie", date="2024-01-15")
string.Template vs Jinja2
| Feature | string.Template | Jinja2 |
|---|---|---|
| Syntax | $name, ${name} | {{ name }} |
| Dependencies | None (stdlib) | External package |
| Control flow | No | Yes (if/for) |
| Filters | No | Yes |
| Inheritance | No | Yes |
| Auto-escaping | No | Yes (HTML) |
| Best for | Simple substitution | Complex templates |
Jinja2 Basics
Variable Substitution
from jinja2 import Template
# Simple variable
template = Template("Hello, {{ name }}!")
print(template.render(name="Alice"))
# Dot notation for objects
template = Template("{{ user.name }} ({{ user.email }})")
print(template.render(user={"name": "Alice", "email": "alice@example.com"}))
# Square bracket notation
template = Template("{{ user['name'] }}")
print(template.render(user={"name": "Alice"}))
# Undefined handling
from jinja2 import Undefined
template = Template("{{ name | default('Anonymous') }}")
print(template.render()) # Anonymous
Filters
Filters transform variables using the pipe | syntax.
from jinja2 import Template
# String filters
template = Template("{{ name | upper }}") # ALICE
template = Template("{{ name | lower }}") # alice
template = Template("{{ name | title }}") # Alice
template = Template("{{ text | truncate(10) }}") # First te...
template = Template("{{ text | replace('old', 'new') }}")
template = Template("{{ text | center(20) }}")
template = Template("{{ text | striptags }}") # Remove HTML
# Number filters
template = Template("{{ price | round(2) }}")
template = Template("{{ price | int }}")
template = Template("{{ price | float }}")
# List filters
template = Template("{{ items | length }}")
template = Template("{{ items | first }}")
template = Template("{{ items | last }}")
template = Template("{{ items | join(', ') }}")
template = Template("{{ items | sort | reverse | list }}")
# Default filter
template = Template("{{ name | default('Anonymous') }}")
print(template.render()) # Anonymous
# Formatting filter
template = Template("{{ '{:.2f}'.format(price) }}")
template = Template("{{ price | round(2) }}")
# Multiple filters
template = Template("{{ name | upper | center(20) }}")
Control Flow
from jinja2 import Template
# For loops
template = Template("""
{% for item in items %}
{{ loop.index }}. {{ item }}
{% endfor %}
""")
print(template.render(items=["Apple", "Banana", "Cherry"]))
# 1. Apple
# 2. Banana
# 3. Cherry
# Loop variables
template = Template("""
{% for user in users %}
{{ loop.index }}/{{ loop.length }}: {{ user.name }}{% if loop.first %} (first){% endif %}{% if loop.last %} (last){% endif %}
{% endfor %}
""")
# If/elif/else
template = Template("""
{% if score >= 90 %}
Grade: A
{% elif score >= 80 %}
Grade: B
{% elif score >= 70 %}
Grade: C
{% else %}
Grade: F
{% endif %}
""")
# If in
template = Template("""
{% if 'admin' in user.roles %}
Admin panel available
{% endif %}
""")
# Macros (reusable components)
template = Template("""
{% macro input_field(name, type='text', placeholder='') %}
<input type="{{ type }}" name="{{ name }}" placeholder="{{ placeholder }}">
{% endmacro %}
{{ input_field('username', placeholder='Enter username') }}
{{ input_field('password', type='password') }}
""")
Template Inheritance
Template inheritance lets you create a base layout and extend it across multiple pages.
Base Template (layout.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}My App{% endblock %}</title>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
<footer>
{% block footer %}© 2024 My App{% endblock %}
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>
Child Template (page.html)
{% extends "layout.html" %}
{% block title %}{{ page_title }} - My App{% endblock %}
{% block extra_css %}
{% endblock %}
{% block content %}
<div class="hero">
<h1>{{ heading }}</h1>
<p>{{ subheading }}</p>
</div>
<div class="articles">
{% for article in articles %}
<article>
<h2>{{ article.title }}</h2>
<p>{{ article.summary | truncate(200) }}</p>
<a href="/articles/{{ article.id }}">Read more</a>
</article>
{% endfor %}
</div>
{% endblock %}
{% block extra_js %}
<script>
console.log("Page loaded");
</script>
{% endblock %}
Using Inheritance
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('page.html')
html = template.render(
page_title="Blog",
heading="Latest Articles",
subheading="Read our latest posts",
articles=[
{"id": 1, "title": "First Post", "summary": "This is the first post..."},
{"id": 2, "title": "Second Post", "summary": "This is the second post..."},
]
)
Generating Emails with Templates
from jinja2 import Template
# Welcome email
welcome_template = Template("""
Subject: Welcome to {{ company }}, {{ name }}!
Hi {{ name }},
Welcome to {{ company }}! We're excited to have you on board.
Your account details:
- Username: {{ username }}
- Email: {{ email }}
{% if referral_code %}
Your referral code: {{ referral_code }}
Share it with friends for {{ referral_bonus }} credits!
{% endif %}
To get started, visit: {{ getting_started_url }}
Best regards,
The {{ company }} Team
""")
email = welcome_template.render(
company="Acme Inc",
name="Alice",
username="alice123",
email="alice@example.com",
referral_code="ALICE2024",
referral_bonus=50,
getting_started_url="https://acme.com/getting-started"
)
# Invoice email
invoice_template = Template("""
Subject: Invoice #{{ invoice_number }}
Dear {{ customer_name }},
Invoice #{{ invoice_number }}
Date: {{ invoice_date }}
Items:
{% for item in items %}
- {{ item.description }}: ${{ "%.2f" | format(item.price) }} x {{ item.quantity }} = ${{ "%.2f" | format(item.price * item.quantity) }}
{% endfor %}
{% if discount %}
Discount: -${{ "%.2f" | format(discount) }}
{% endif %}
Total: ${{ "%.2f" | format(total) }}
Payment due by: {{ due_date }}
Thank you for your business!
""")
Generating HTML Reports
from jinja2 import Template
report_template = Template("""
<!DOCTYPE html>
<html>
<head>
<title>Sales Report — {{ period }}</title>
</head>
<body>
<h1>Sales Report — {{ period }}</h1>
<p>Generated: {{ generated_at }}</p>
<h2>Summary</h2>
<table>
<tr><td>Total Revenue</td><td>${{ "%.2f" | format(total_revenue) }}</td></tr>
<tr><td>Total Orders</td><td>{{ total_orders }}</td></tr>
<tr><td>Average Order Value</td><td>${{ "%.2f" | format(avg_order_value) }}</td></tr>
</table>
<h2>Top Products</h2>
<table>
<tr><th>Product</th><th>Units Sold</th><th>Revenue</th></tr>
{% for product in products %}
<tr>
<td>{{ product.name }}</td>
<td>{{ product.units }}</td>
<td>${{ "%.2f" | format(product.revenue) }}</td>
</tr>
{% endfor %}
</table>
{% if top_regions %}
<h2>Top Regions</h2>
<ul>
{% for region in top_regions %}
<li>{{ region.name }}: ${{ "%.2f" | format(region.revenue) }}</li>
{% endfor %}
</ul>
{% endif %}
<div class="total">
Total Revenue: ${{ "%.2f" | format(total_revenue) }}
</div>
</body>
</html>
""")
html = report_template.render(
period="January 2024",
generated_at="2024-02-01 10:00:00",
products=[
{"name": "Widget A", "units": 150, "revenue": 4500.00},
{"name": "Widget B", "units": 80, "revenue": 3200.00},
{"name": "Widget C", "units": 45, "revenue": 1350.00},
],
total_revenue=9050.00,
total_orders=275,
avg_order_value=32.91,
top_regions=[
{"name": "North America", "revenue": 5000.00},
{"name": "Europe", "revenue": 3000.00},
{"name": "Asia Pacific", "revenue": 1050.00},
]
)
with open("report.html", "w") as f:
f.write(html)
Auto-Escaping for Security
from jinja2 import Environment, FileSystemLoader
# Auto-escape HTML (prevents XSS)
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=True # Enable auto-escaping
)
# Safe rendering for trusted HTML
template = env.from_string("{{ user_input | safe }}")
template = env.from_string("{{ trusted_html }}") # Auto-escaped
template = env.from_string("{% autoescape true %}{{ content }}{% endautoescape %}")
# Disable escaping for specific variables
template = env.from_string("{{ html_content | safe }}") # Not escaped
Custom Filters
from jinja2 import Environment
def format_currency(value, symbol="$"):
return f"{symbol}{value:,.2f}"
def slugify(value):
return value.lower().replace(" ", "-").replace("_", "-")
def time_ago(value):
from datetime import datetime
diff = datetime.now() - value
if diff.days > 365:
return f"{diff.days // 365} years ago"
elif diff.days > 30:
return f"{diff.days // 30} months ago"
elif diff.days > 0:
return f"{diff.days} days ago"
elif diff.seconds > 3600:
return f"{diff.seconds // 3600} hours ago"
elif diff.seconds > 60:
return f"{diff.seconds // 60} minutes ago"
else:
return "just now"
env = Environment()
env.filters["currency"] = format_currency
env.filters["slugify"] = slugify
env.filters["time_ago"] = time_ago
template = env.from_string("{{ price | currency }}")
print(template.render(price=1234.5)) # $1,234.50
template = env.from_string("{{ title | slugify }}")
print(template.render(title="Hello World")) # hello-world
Common Mistakes
| Mistake | Problem | Solution |
|---|---|---|
| No auto-escaping | XSS vulnerabilities | Enable autoescape=True |
Forgetting {% endblock %} | Template parse error | Always close blocks |
Using {{ | safe }} on untrusted input | Security risk | Only use on trusted HTML |
Not using default filter | Undefined variable error | Always provide defaults |
| Hardcoding paths | Not portable | Use FileSystemLoader |
| No whitespace control | Messy HTML output | Use {%- -%} syntax |
Key Takeaways
- Use
string.Templatefor simple substitution (no dependencies) - Use Jinja2 for complex HTML/document generation
- Template inheritance reduces duplication
- Auto-escaping prevents XSS in HTML
- Use
|safeto disable escaping for trusted HTML only - Filters transform variables in templates
- Macros are reusable template components
- Use
defaultfilter to handle missing variables - Custom filters extend Jinja2's capabilities
- Always enable auto-escaping for user-generated content