Template Rendering

Difficulty: Beginner

Templates are the foundation of dynamic HTML generation in Flask. Instead of returning raw HTML strings from your view functions, you write HTML files with special placeholders that Jinja2 fills in with data at render time. This separates your presentation logic from your application logic, making code easier to maintain and allowing designers and developers to work independently.

Flask looks for templates in a directory called 'templates' in your application root by default. You can customize this by passing template_folder to the Flask constructor. The render_template() function takes a template filename and keyword arguments that become available as variables in the template.

Jinja2 uses three types of delimiters in templates. Double curly braces {{ expression }} output the result of an expression. Curly-brace-percent {% statement %} execute control flow statements like if/else and for loops. Curly-brace-hash {# comment #} creates comments that do not appear in the output.

Template variables are passed from view functions as keyword arguments to render_template(). Inside the template, you access them by name. Jinja2 supports attribute access (user.name), item access (user['name']), and method calls (items.count()). If a variable does not exist, Jinja2 returns an Undefined object that renders as an empty string by default.

Jinja2 automatically escapes HTML in variables to prevent Cross-Site Scripting (XSS) attacks. If you pass the string '<script>alert(1)</script>' to a template, Jinja2 renders it as '&lt;script&gt;alert(1)&lt;/script&gt;', which displays as text rather than executing as JavaScript. If you need to render raw HTML (for trusted content like CMS output), use the |safe filter or the Markup class.

Flask makes several variables available in templates automatically without passing them explicitly. These include request (the current request object), session (the session dict), g (the application global namespace), url_for (the URL building function), and get_flashed_messages (for flash messages). You can also register custom global template variables using app.context_processor.

The render_template_string() function renders a Jinja2 template from a string instead of a file. This is useful for short templates or testing, but should never be used with user input due to Server-Side Template Injection (SSTI) vulnerabilities.

Template rendering is synchronous by default. For very complex templates with heavy computation, consider precomputing values in the view function and passing simple data to the template. Jinja2 compiles templates to Python bytecode and caches them, so template rendering is generally fast.

Code examples

Basic Template Rendering

# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html',
        title='Home',
        username='Alice',
        items=['Flask', 'Jinja2', 'Werkzeug']
    )

# templates/index.html
# <!DOCTYPE html>
# <html>
# <head><title>{{ title }}</title></head>
# <body>
#     <h1>Hello, {{ username }}!</h1>
#     <ul>
#     {% for item in items %}
#         <li>{{ item }}</li>
#     {% endfor %}
#     </ul>
# </body>
# </html>

render_template loads the template file from the templates/ directory and passes keyword arguments as template variables. {{ variable }} outputs values, {% %} handles control flow like for loops.

Jinja2 Control Flow

<!-- templates/dashboard.html -->
<h1>Dashboard</h1>

{% if user.is_admin %}
    <p>Welcome, Admin {{ user.name }}!</p>
    <a href="{{ url_for('admin_panel') }}">Admin Panel</a>
{% elif user.is_authenticated %}
    <p>Welcome back, {{ user.name }}!</p>
{% else %}
    <p>Please <a href="{{ url_for('login') }}">log in</a>.</p>
{% endif %}

{% for notification in notifications %}
    <div class="alert alert-{{ notification.type }}">
        {{ notification.message }}
        {% if notification.link %}
            <a href="{{ notification.link }}">View</a>
        {% endif %}
    </div>
{% else %}
    <p>No notifications.</p>
{% endfor %}

Jinja2 supports if/elif/else for conditionals and for/else for loops. The else clause on a for loop executes when the iterable is empty. You can access object attributes with dot notation.

Auto-escaping and the safe Filter

from flask import Flask, render_template, Markup

app = Flask(__name__)

@app.route('/')
def index():
    # This will be escaped (safe from XSS)
    user_input = '<script>alert("xss")</script>'

    # This is trusted HTML (from your CMS, etc.)
    trusted_html = Markup('<strong>Bold text</strong>')

    return render_template('index.html',
        user_input=user_input,
        trusted_html=trusted_html
    )

# templates/index.html
# <!-- Escaped: &lt;script&gt;alert("xss")&lt;/script&gt; -->
# <p>{{ user_input }}</p>
#
# <!-- Not escaped: <strong>Bold text</strong> -->
# <p>{{ trusted_html }}</p>
#
# <!-- Also not escaped (use sparingly): -->
# <p>{{ user_input|safe }}</p>  {# DANGEROUS with user input! #}

Jinja2 auto-escapes HTML to prevent XSS. Use the |safe filter or Markup class only for trusted content. Never use |safe on user-supplied data.

Context Processors

from flask import Flask, render_template
import datetime

app = Flask(__name__)

@app.context_processor
def inject_globals():
    """Make these variables available in all templates."""
    return {
        'app_name': 'My Flask App',
        'current_year': datetime.datetime.now().year,
        'nav_links': [
            {'url': '/', 'label': 'Home'},
            {'url': '/about', 'label': 'About'},
            {'url': '/contact', 'label': 'Contact'},
        ]
    }

@app.route('/')
def index():
    # No need to pass app_name or current_year
    return render_template('index.html')

# templates/index.html can use {{ app_name }}, {{ current_year }},
# and {{ nav_links }} without them being passed from the view.

Context processors inject variables into every template automatically. This is useful for app-wide data like navigation, the app name, or the current year for copyright notices.

Key points

Concepts covered

render_template, Jinja2, Template Variables, Templates Directory, Context