Python Jinja2 Templating: Rendering HTML and Config Files

Jinja2 is Python's most popular template engine — used by Flask, Ansible, dbt, and FastAPI. It renders HTML, email templates, Kubernetes manifests, Terraform configs, Nginx configs, and any text file that needs parameterisation. Jinja2's template inheritance, macros, and filter system make it powerful enough to build maintainable multi-page web apps or complex infrastructure templating pipelines.

Basics: Variables, Filters, Control Flow

pip install jinja2
from jinja2 import Template, Environment

# Simple string rendering
template = Template("Hello, {{ name }}! You have {{ count }} messages.")
output = template.render(name="Alice", count=5)
print(output)  # Hello, Alice! You have 5 messages.

# Variables: {{ variable }}
# Filters: {{ variable | filter }}
# Statements: {% tag %}
# Comments: {# comment #}

tpl = Template("""
{{ user.name | upper }}
{{ user.email | lower }}
{{ price | round(2) }}
{{ description | truncate(50) }}
{{ items | join(", ") }}
{{ "hello world" | title }}
{{ timestamp | default("now") }}
""")
print(tpl.render(
    user={"name": "alice", "email": "ALICE@EXAMPLE.COM"},
    price=19.9999,
    description="A very long description that will be truncated",
    items=["Python", "FastAPI", "Jinja2"],
    timestamp=None,
))
{# Control flow in templates #}

{# If/elif/else #}
{% if user.role == "admin" %}
  Admin
{% elif user.role == "editor" %}
  Editor
{% else %}
  Viewer
{% endif %}

{# For loops #}
    {% for product in products %}
  • {{ loop.index }}. {{ product.name }} — ${{ product.price | round(2) }} {% if loop.last %} (last item) {% endif %}
  • {% else %}
  • No products found
  • {% endfor %}
{# Loop variables: loop.index, loop.index0, loop.first, loop.last, loop.length #}

Environment and Template Loading

Use Environment with FileSystemLoader to load templates from disk files. This is the production pattern — templates are in a directory, not inline strings, and can be edited without touching Python code.

from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape

# Load templates from a directory
env = Environment(
    loader=FileSystemLoader("templates/"),
    autoescape=select_autoescape(["html", "xml"]),   # auto-escape HTML
    trim_blocks=True,      # remove first newline after a block tag
    lstrip_blocks=True,    # strip leading spaces before block tags
    undefined=jinja2.StrictUndefined,  # raise error on undefined variables
)

# Render a template file
template = env.get_template("email/welcome.html")
html = template.render(
    user_name="Alice",
    activation_link="https://techoral.com/activate/abc123",
    support_email="support@techoral.com",
)

# From a Python package (for libraries)
env_pkg = Environment(
    loader=PackageLoader("mypackage", "templates"),
    autoescape=select_autoescape(["html"]),
)

# From multiple locations
from jinja2 import ChoiceLoader
env_multi = Environment(
    loader=ChoiceLoader([
        FileSystemLoader("custom_templates/"),  # checked first
        FileSystemLoader("default_templates/"), # fallback
    ])
)

Template Inheritance

Template inheritance lets you define a base layout and override specific blocks in child templates. This is the standard pattern for web applications — one base with navigation, head, footer, and children that fill in the content area.

{# templates/base.html #}



  
  {% block title %}Techoral{% endblock %}
  {% block head %}
    
  {% endblock %}


  

  
{% block content %}{% endblock %}
{% block footer %}© 2026 Techoral{% endblock %}
{% block scripts %} {% endblock %}
{# templates/products/list.html #}
{% extends "base.html" %}

{% block title %}Products — Techoral{% endblock %}

{% block head %}
  {{ super() }}  {# include parent block content #}
  
{% endblock %}

{% block content %}

Products

{% for product in products %}

{{ product.name }}

{{ product.description | truncate(80) }}

${{ product.price | round(2) }} View
{% endfor %}
{% endblock %}

Macros and Includes

Macros are reusable template functions — equivalent to Python functions but in Jinja2 syntax. Use {% include %} to include partial templates and {% from ... import %} to import macros from other files.

{# templates/macros/forms.html #}
{% macro input(name, label, type="text", required=False, value="") %}
{% endmacro %} {% macro button(label, type="submit", variant="primary") %} {% endmacro %} {% macro pagination(page, total_pages, base_url) %} {% endmacro %}
{# templates/products/create.html #}
{% extends "base.html" %}
{% from "macros/forms.html" import input, button %}

{% block content %}
{{ input("name", "Product Name", required=True) }} {{ input("price", "Price", type="number", required=True) }} {{ input("sku", "SKU", required=True) }} {{ button("Create Product") }} {{ button("Cancel", type="button", variant="secondary") }}
{% endblock %}

Custom Filters and Tests

from jinja2 import Environment, FileSystemLoader
from datetime import datetime
import markdown


env = Environment(loader=FileSystemLoader("templates/"))

# Custom filter — callable with (value, *args, **kwargs)
def currency(value: float, symbol: str = "$", decimals: int = 2) -> str:
    return f"{symbol}{value:,.{decimals}f}"

def humanize_date(value: datetime) -> str:
    now = datetime.now()
    delta = now - value
    if delta.days == 0:
        if delta.seconds < 3600:
            return f"{delta.seconds // 60} minutes ago"
        return f"{delta.seconds // 3600} hours ago"
    if delta.days == 1:
        return "yesterday"
    if delta.days < 7:
        return f"{delta.days} days ago"
    return value.strftime("%B %d, %Y")

def render_markdown(text: str) -> str:
    return markdown.markdown(text, extensions=["tables", "fenced_code"])

def truncate_words(text: str, count: int) -> str:
    words = text.split()
    return " ".join(words[:count]) + ("..." if len(words) > count else "")

# Register filters
env.filters["currency"] = currency
env.filters["humanize_date"] = humanize_date
env.filters["markdown"] = render_markdown
env.filters["truncate_words"] = truncate_words

# Usage in templates:
# {{ product.price | currency }}
# {{ product.price | currency("€") }}
# {{ post.created_at | humanize_date }}
# {{ post.content | markdown | safe }}
# {{ description | truncate_words(20) }}

# Custom test
env.tests["affordable"] = lambda value: value < 50

# {% if product.price is affordable %}
#   Budget-Friendly
# {% endif %}

Rendering Config and YAML Files

Jinja2 is used beyond HTML — Ansible, dbt, and Kubernetes tooling use it for YAML/config templating. Use a custom Environment with different delimiters to avoid conflicts with YAML syntax.

from jinja2 import Environment, FileSystemLoader
import yaml


# Kubernetes manifest template
k8s_env = Environment(
    loader=FileSystemLoader("k8s/templates/"),
    trim_blocks=True,
    lstrip_blocks=True,
)

def render_k8s_manifest(template_name: str, values: dict) -> str:
    tpl = k8s_env.get_template(template_name)
    return tpl.render(**values)


# k8s/templates/deployment.yaml.j2:
DEPLOYMENT_TEMPLATE = """
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ app_name }}
  namespace: {{ namespace }}
  labels:
    app: {{ app_name }}
    version: {{ image_tag }}
spec:
  replicas: {{ replicas | default(2) }}
  selector:
    matchLabels:
      app: {{ app_name }}
  template:
    spec:
      containers:
        - name: {{ app_name }}
          image: {{ image_repo }}/{{ app_name }}:{{ image_tag }}
          ports:
            - containerPort: {{ port | default(8000) }}
          resources:
            requests:
              memory: {{ memory_request | default("128Mi") }}
              cpu: {{ cpu_request | default("100m") }}
            limits:
              memory: {{ memory_limit | default("512Mi") }}
              cpu: {{ cpu_limit | default("500m") }}
          env:
            {% for key, value in env_vars.items() %}
            - name: {{ key }}
              value: "{{ value }}"
            {% endfor %}
"""

from jinja2 import Template
manifest = Template(DEPLOYMENT_TEMPLATE).render(
    app_name="order-service",
    namespace="production",
    image_repo="registry.techoral.com",
    image_tag="v2.1.0",
    replicas=3,
    env_vars={"DATABASE_URL": "postgresql://...", "REDIS_URL": "redis://..."},
)
print(manifest)

FastAPI with Jinja2Templates

pip install jinja2 python-multipart
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from jinja2 import Environment

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

# Add custom filters to Jinja2Templates
templates.env.filters["currency"] = currency
templates.env.filters["humanize_date"] = humanize_date


@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={
            "title": "Techoral",
            "featured_products": await get_featured_products(),
        },
    )


@app.get("/products", response_class=HTMLResponse)
async def products_list(request: Request, page: int = 1, category: str = ""):
    products, total = await get_products(page=page, category=category)
    return templates.TemplateResponse(
        request=request,
        name="products/list.html",
        context={
            "products": products,
            "total": total,
            "page": page,
            "total_pages": (total + 19) // 20,
            "category": category,
        },
    )


async def get_featured_products(): return []
async def get_products(page=1, category=""): return [], 0

Frequently Asked Questions

How do I prevent XSS with Jinja2?
Enable autoescape=select_autoescape(["html", "xml"]) in the Environment. This automatically escapes <, >, &, " in all variable outputs. Mark trusted HTML safe with {{ content | safe }} or Markup(content) — only use safe when the content comes from your own code, never from user input.
Jinja2 vs Mako vs Django templates?
Jinja2 is the standard for non-Django Python web apps. Django templates deliberately limit Python expressions for security; Jinja2 allows full expressions. Mako is faster for complex templates but has a steeper syntax. Jinja2 is the best general-purpose choice — it powers Flask, Ansible, dbt, and Airflow.
How do I cache compiled templates?
Jinja2 compiles templates to Python bytecode on first use. Set bytecode_cache=FileSystemBytecodeCache("/tmp/jinja2_cache") on the Environment to persist compiled bytecode across process restarts. In production, templates are compiled once at startup — cache is mainly useful for CLI tools and scripts that start fresh each run.