Jinja Templates

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Flask 3.1.3 (released 2026-02-18), Werkzeug 3.1.9, Jinja 3.1.6 and MarkupSafe 3.0.4, checked on 2026-10-07, and every Python block was executed on Python 3.13. Flask 3.2 is not released yet; the one templating change it lists is described as upcoming in What Changed and Where Older Material Is Outdated. Flask does not have a template engine of its own. It configures a Jinja environment, finds templates in the application and blueprint folders, adds a small standard context and a handful of helpers, and hands the result back to the view as a string. Everything else, the template syntax, the filters, the sandbox and the compiler, belongs to Jinja, and its documentation is the reference for the syntax. This page covers the part Flask adds: how templates are found and rendered, what is escaped, what is available in every template, and how to extend all of it for the Bookshelf project of this section.

What Happens in render_template

render_template("catalog/list.html", books=books) is a thin function. It takes the application from current_app, asks app.jinja_env for the template (a list of names is allowed and the first one that exists wins), merges the standard context and the context processors into the keyword arguments, sends the before_render_template signal, renders, sends template_rendered and returns a str. Flask then turns the string into a Response with the text/html mimetype like any other returned string (see Responses & JSON).

flowchart LR V["View calls render_template with a name and keyword arguments"] --> E["app.jinja_env get_or_select_template"] E --> L["DispatchingJinjaLoader: app templates folder first, then blueprint folders"] L --> U["update_template_context: g, request, then context processors"] U --> B["before_render_template signal"] B --> R["Jinja renders with autoescaping chosen by file extension"] R --> T["template_rendered signal"] T --> S["View returns the string, Flask builds the Response"]
The path of one render_template call

The Jinja environment is created lazily the first time app.jinja_env is read. Its options come from app.jinja_options (a class-level dict, empty by default) and from the application: the autoescape option is app.select_jinja_autoescape, auto_reload follows TEMPLATES_AUTO_RELOAD (or debug mode), and the loader is a Flask-specific one that searches several folders. Changing jinja_options after the environment exists has no effect, and so does registering filters through jinja_env after the first request (see Custom Filters, Tests and Globals).

render_template and render_template_string

By default an application looks for templates in a folder named templates next to the module or package that creates the Flask object (the template_folder argument of Flask(...)). render_template takes the file name relative to that folder, using forward slashes for sub-folders, and keyword arguments that become template variables.

The first block builds a tiny application whose templates live in a temporary folder, so that the page’s examples run anywhere. The helper make_app is test scaffolding only, a real project keeps the files in templates/ and never needs it. The following blocks reuse the helper.

import logging
import tempfile
from pathlib import Path

from flask import Flask, render_template, render_template_string

TMP = Path(tempfile.mkdtemp(prefix="bookshelf-templates-"))


def make_app(files, name="bookshelf", **options):
    """Write {relative name: source} under a temp folder and build an app on it."""
    root = TMP / name
    for rel, source in files.items():
        target = root / rel
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(source, encoding="utf-8")
    return Flask(name, template_folder=str(root), **options)


app = make_app({
    "hello.html": "<h1>{{ title }}</h1><p>{{ books|length }} books</p>",
    "fallback.html": "<p>fallback for {{ title }}</p>",
})

with app.test_request_context():
    html = render_template("hello.html", title="Bookshelf", books=["A", "B"])
    assert html == "<h1>Bookshelf</h1><p>2 books</p>"

    # a list picks the first template that exists
    html = render_template(["missing.html", "fallback.html"], title="Bookshelf")
    assert html == "<p>fallback for Bookshelf</p>"

    # a template from a string
    html = render_template_string("<b>{{ n }} copies</b>", n=3)
    assert html == "<b>3 copies</b>"


@app.get("/")
def home():
    return render_template("hello.html", title="Bookshelf", books=[])


reply = app.test_client().get("/")
assert reply.mimetype == "text/html"
assert reply.get_data(as_text=True) == "<h1>Bookshelf</h1><p>0 books</p>"

render_template_string(source, **context) compiles a template from a string. It exists for tiny examples and tests, and it comes with a security trap that is covered in Server-Side Template Injection: the source is code, never data. A missing template raises jinja2.TemplateNotFound, which Flask turns into a 500 response (the debugger page in debug mode); the loader log described in Loaders and Blueprint Template Folders is the fastest way to find out why.

Jinja Syntax in One Table

The syntax is documented in the Jinja Template Designer Documentation; this page assumes it and only lists the delimiters so the examples are readable.

Delimiter Use

{{ expression }}

Print the value of an expression (autoescaped, see Autoescaping).

{% statement %}

A statement: if, for, block, extends, include, import, macro, set, with, call, filter, autoescape.

{# comment #}

A comment, not sent to the client.

{{ value|filter(arg) }}

Apply a filter; filters chain left to right.

{% if x is test %}

Apply a test such as defined, none, even or a custom one.

{%- ... -%}

A minus strips whitespace on that side. trim_blocks and lstrip_blocks do it globally.

Autoescaping

Autoescaping is what makes Jinja safe enough for HTML. When it is on, every {{ }} expression result that is a plain str has &, <, >, ' and " replaced by entities before it is printed. Flask turns it on for templates whose file name ends in .html, .htm, .xml, .xhtml or .svg (.svg since Flask 2.2.3), and for templates rendered from a string with render_template_string, because no file name is known and the rule returns True then. Any other extension, notably .txt, .md, .js and .css, is not escaped.

Situation Is it escaped?

page.html, .htm, .xml, .xhtml, .svg

Yes.

render_template_string(...)

Yes.

mail.txt, data.csv, app.js, style.css

No. Escape with |e yourself, or rename and rely on the rule.

A value wrapped in Markup or an object with an __html__() method

No, the object promises to be safe.

value|safe

No, you promise that the value is safe.

value|e or value|escape

Escaped once; escaping an already escaped Markup does nothing, so there is no double escaping.

Inside {% autoescape false %}...{% endautoescape %}

No, until endautoescape.

The rule lives in app.select_jinja_autoescape(filename); override it in a Flask subclass or pass your own autoescape in jinja_options to change it. Keep the default unless you have a reason.

from markupsafe import Markup, escape

app = make_app({
    "page.html": "<p>{{ comment }}</p><p>{{ comment|safe }}</p><p>{{ trusted }}</p>",
    "note.txt": "{{ comment }} | {{ comment|e }}",
})

hostile = "<script>alert('x')</script>"

with app.test_request_context():
    html = render_template("page.html", comment=hostile, trusted=Markup("<em>fine</em>"))
    assert html.startswith("<p>&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;</p>")  # escaped
    assert "<p><script>alert('x')</script></p>" in html                                  # |safe: dangerous
    assert html.endswith("<p><em>fine</em></p>")                                         # Markup: not escaped

    text = render_template("note.txt", comment=hostile)    # .txt is not escaped, only |e is
    assert text.startswith("<script>") and text.endswith("&lt;/script&gt;")

    # Markup.format escapes its arguments, so building HTML from user data stays safe
    snippet = Markup("<b>{}</b>").format(hostile)
    assert snippet == "<b>&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;</b>"
    assert escape(snippet) == snippet                       # already safe, not escaped twice

Source: MarkupSafe

Where Autoescaping Stops

Autoescaping is HTML-text escaping. It does nothing for the other contexts a value can land in, and the Flask security page names the same cases:

  • Unquoted or single-quoted attributes. Jinja escapes the quote characters, but <input value={{ v }}> lets a space start a new attribute (x onfocus=alert(1)). Always quote attributes with double quotes: <input value="{{ v }}">.

  • javascript: and other dangerous URL schemes. In <a href="{{ url }}"> a user-supplied javascript:alert(1) survives escaping because it contains nothing to escape. Validate the scheme on the server (allow http, https, mailto) or build links with url_for.

  • Inside <script> and <style>. Escaping for HTML is wrong there. Emit data with the tojson filter, which produces a JSON literal safe for a script element and marks it safe: <script>const book = {{ book|tojson }};</script>. Never write var name = '{{ name }}';.

  • Event-handler attributes such as onclick="..." hold JavaScript. Pass data through data-* attributes and read it from a script, or use tojson inside a quoted attribute (tojson escapes the quote).

  • Uploaded files and stored HTML. A user-supplied HTML or SVG file served back from your origin runs script. Serve uploads from another origin or with Content-Disposition: attachment and a restrictive content type.

app = make_app({
    "book.html": (
        '<a href="/search?q={{ q|urlencode }}">{{ q }}</a>\n'
        '<input value="{{ q }}">\n'
        "<script>const book = {{ book|tojson }};</script>"
    ),
})

with app.test_request_context():
    html = render_template("book.html", q='"><b>x</b>', book={"title": "</script><b>"})
    assert '<input value="&#34;&gt;&lt;b&gt;x&lt;/b&gt;">' in html          # quote and tag characters escaped
    assert "q=%22%3E%3Cb%3Ex%3C/b%3E" in html                               # urlencode for query strings
    assert '{"title": "\\u003c/script\\u003e\\u003cb\\u003e"}' in html      # tojson cannot close the script element

Safe, Markup and Their Risks

value|safe and Markup(value) are the same promise: this string is HTML that needs no escaping. The risk is always the same, the promise is made about data the programmer does not control. Use them for HTML you produced yourself (a rendered Markdown fragment that was sanitised, an icon sprite, the output of a macro) and never for anything that came from a request, a database row a user can edit or a third-party API.

The import changed over the years. In Flask 2.3 flask.Markup and flask.escape were deprecated and in Flask 3.0 they were removed; jinja2.Markup and jinja2.escape went away in Jinja 3.1. The home of both is MarkupSafe:

from markupsafe import Markup, escape

Markup is a str subclass. Its operators (`, `%`, `format`, `join`) escape the plain strings that are mixed into it, which is why building HTML with `+Markup("<b>{}</b>").format(user_value) is safe and building it with an f-string and wrapping the result afterwards, Markup(f"<b>{user_value}</b>"), is not.

The Standard Context

Every template, whether rendered by a view or imported from another template, can read these names without being passed anything:

Name What it is

config

The application’s app.config (see Configuration). Do not print it: it holds SECRET_KEY and other secrets.

request

The current request object: request.args, request.path, request.endpoint, request.blueprint and so on (see Request Data).

session

The current session (a dict-like object; reading it marks the response Vary: Cookie).

g

The g object of the current application context, a per-request scratch space.

url_for

The function that builds URLs from endpoints (see Routing & URL Building).

get_flashed_messages

Returns the messages stored with flash(), optionally with categories and a filter.

Two details matter in practice. First, Flask puts the concrete request and g objects (not the proxies) into the context of each render for speed, and keeps the proxies as environment globals, so that templates imported with {% import %}, which by default do not see the context of the importing template, can still use request, session, g, config, url_for and get_flashed_messages. Variables that you passed to render_template are not visible in an imported template unless you write {% import "x.html" as x with context %}; macros that need such a variable should take it as an argument instead. Second, request and session exist only while a request context is active; rendering from a CLI command or a background job does not have them, and a template that uses them then fails with a RuntimeError or a quietly empty value. Pass what the job needs explicitly.

import secrets

from flask import flash, session

app = make_app({
    "ctx.html": (
        "{% import 'helpers.html' as helpers %}"
        "path={{ request.path }}|endpoint={{ request.endpoint }}|"
        "debug={{ config['DEBUG'] }}|user={{ session.get('user', 'anonymous') }}|"
        "g={{ g.theme }}|url={{ url_for('home') }}|"
        "{% for category, text in get_flashed_messages(with_categories=true) %}"
        "{{ category }}:{{ text }};{% endfor %}"
        "|imported={{ helpers.where() }}|local={{ helpers.local_value() }}"
    ),
    "helpers.html": (
        "{% macro where() %}{{ request.path }}{% endmacro %}"
        "{% macro local_value() %}{{ title|default('no title') }}{% endmacro %}"
    ),
})
app.secret_key = secrets.token_hex()  # the real key comes from config, see the configuration page


@app.get("/")
def home():
    from flask import g
    g.theme = "dark"
    session["user"] = "ada"
    flash("Saved", "success")
    return render_template("ctx.html", title="Shown only to the including template")


reply = app.test_client().get("/")
assert reply.get_data(as_text=True) == (
    "path=/|endpoint=home|debug=False|user=ada|g=dark|url=/|success:Saved;|imported=/|local=no title"
)
assert "Cookie" in reply.headers["Vary"]

Template Inheritance

Inheritance is the feature that keeps a site consistent. A base template defines the page skeleton, with named block placeholders; a child template starts with {% extends "base.html" %} and fills only the blocks it wants to change. In the books of this section’s literature the base layout is typically a Bootstrap page; the same idea works with any CSS framework. The Bookshelf layout below links its own stylesheet through url_for('static', ...) (the static-files page covers how to serve it).

flowchart BT L["catalog/list.html: fills the content and scripts blocks"] -->|extends| C["catalog/base.html: adds a sidebar and fills the title block"] C -->|extends| B["base.html: html skeleton, header, flash messages, footer, empty blocks"]
Template inheritance in Bookshelf: each child extends its parent and overrides blocks

Rules worth remembering:

  • extends must be the first tag in the child; anything outside blocks in a child is ignored (except set/import).

  • A block name can be defined once per template. {{ super() }} inside a block renders the parent’s content of that block, which is how a child appends to a list of scripts instead of replacing it.

  • {{ self.title() }} renders another block of the current template, useful for repeating the title in a heading.

  • A block inside a loop does not see the loop variable unless it is declared {% block item scoped %}.

  • The template name in extends and include is evaluated against the same loader, so catalog/base.html is found in the application folder or in a blueprint folder (see Loaders and Blueprint Template Folders).

  • A block can be marked required in the parent (Jinja 3.1), forcing every child to fill it.

app = make_app({
    "base.html": (
        "<!doctype html>\n<html lang=\"en\">\n<head>\n"
        "<title>{% block title %}Bookshelf{% endblock %}</title>\n"
        "<link rel=\"stylesheet\" href=\"{{ url_for('static', filename='site.css') }}\">\n"
        "</head>\n<body>\n"
        "<header><a href=\"{{ url_for('home') }}\">Bookshelf</a></header>\n"
        "{% block body %}{% endblock %}\n"
        "{% block scripts %}<script src=\"{{ url_for('static', filename='site.js') }}\"></script>{% endblock %}\n"
        "</body>\n</html>\n"
    ),
    "catalog/base.html": (
        "{% extends 'base.html' %}\n"
        "{% block title %}Catalog - {{ super() }}{% endblock %}\n"
        "{% block body %}<nav>shelves</nav><main>{% block content %}{% endblock %}</main>{% endblock %}"
    ),
    "catalog/list.html": (
        "{% extends 'catalog/base.html' %}\n"
        "{% block content %}<h1>{{ self.title() }}</h1>"
        "<ul>{% for b in books %}<li>{{ b }}</li>{% endfor %}</ul>{% endblock %}\n"
        "{% block scripts %}{{ super() }}<script src=\"{{ url_for('static', filename='list.js') }}\"></script>{% endblock %}"
    ),
})


@app.get("/")
def home():
    return render_template("catalog/list.html", books=["Dune", "Emma & Co"])


html = app.test_client().get("/").get_data(as_text=True)
assert "<title>Catalog - Bookshelf</title>" in html
assert "<nav>shelves</nav><main><h1>Catalog - Bookshelf</h1>" in html
assert "<li>Emma &amp; Co</li>" in html
assert html.index("site.js") < html.index("list.js")      # super() kept the parent's script first
assert 'href="/static/site.css"' in html

Includes and Macros

{% include "partial.html" %} renders another template in place, with the current context. It is the right tool for a fragment that appears on several pages (a pagination bar, a cookie notice). ignore missing makes an optional include silent, and a list of names uses the first one that exists. An include is rendered at runtime on every occurrence, so including inside a loop with hundreds of iterations costs more than a macro call.

A macro is a function written in Jinja. Macros live in their own file, are loaded with import, and are called with arguments, so they are the way to build reusable form controls. A macro that takes a field and prints the label, the input and the error messages replaces dozens of repeated lines. Output of a macro is Markup, so it is not escaped a second time when printed; the values inside the macro are escaped as usual.

app = make_app({
    "_forms.html": (
        "{% macro field(name, label, value='', errors=(), type='text') -%}\n"
        "<div class=\"field{% if errors %} has-error{% endif %}\">"
        "<label for=\"{{ name }}\">{{ label }}</label>"
        "<input id=\"{{ name }}\" name=\"{{ name }}\" type=\"{{ type }}\" value=\"{{ value }}\">"
        "{% for message in errors %}<span class=\"error\">{{ message }}</span>{% endfor %}"
        "</div>\n"
        "{%- endmacro %}\n"
        "{% macro card(title) -%}\n"
        "<section><h2>{{ title }}</h2>{{ caller() }}</section>\n"
        "{%- endmacro %}"
    ),
    "new_book.html": (
        "{% from '_forms.html' import field, card %}\n"
        "{% call card('New book') %}\n"
        "{{ field('title', 'Title', value, errors) }}\n"
        "{{ field('isbn', 'ISBN') }}\n"
        "{% endcall %}\n"
        "{% include 'footer.html' ignore missing %}"
    ),
})

with app.test_request_context():
    html = render_template("new_book.html", value='A "quoted" <title>', errors=["Required"])
    assert 'value="A &#34;quoted&#34; &lt;title&gt;"' in html          # values inside a macro are escaped
    assert '<span class="error">Required</span>' in html
    assert "<section><h2>New book</h2>" in html and html.count('class="field') == 2

Source: Jinja: Macros

Source: Jinja: Include

Form libraries ship macro-style helpers of their own. Flask-WTF fields are rendered by calling the field ({{ form.title() }}) or by a macro like field above that receives form.title; the forms page of this section shows both.

Custom Filters, Tests and Globals

Jinja ships many filters (length, join, default, truncate, sort, tojson, …​) and tests (defined, none, even, string, …​). Flask registers your own with decorators on the application or on a blueprint:

Decorator or method Effect

@app.template_filter("name") / app.add_template_filter(f, name)

Adds a filter value|name. Without a name the function name is used.

@app.template_test("name") / app.add_template_test(f, name)

Adds a test value is name.

@app.template_global("name") / app.add_template_global(f, name)

Adds a global function or constant {{ name() }}.

@bp.app_template_filter(), @bp.app_template_test(), @bp.app_template_global()

The same, registered when the blueprint is registered, and available to every template of the application. See Blueprints.

app.jinja_env.filters["name"] = f

The raw dictionary, equivalent to the methods when done before the first request.

In Flask 3.1 the decorators need the parentheses, @app.template_filter(); the form without parentheses is listed for Flask 3.2 and is covered in What Changed and Where Older Material Is Outdated. Like every setup method, they fail with an AssertionError once the application has handled its first request, so register filters in the application factory.

Filters receive the value as the first argument. A filter that returns plain text can return a str, and autoescaping handles it (do not escape it yourself, or it is escaped twice). A filter that returns HTML must return Markup, and must escape the data it embeds, which Markup.format does. The currency filter of the books is the textbook case of a plain str filter, and a date filter is the textbook case of one that must not build a <script> out of strings:

from datetime import date, datetime, timezone
from decimal import Decimal

from flask import Blueprint
from jinja2 import pass_context
from markupsafe import Markup

app = make_app({
    "book.html": (
        "{{ price|currency }} | {{ price|currency('GBP') }} | {{ published|time_tag }}\n"
        "{% if due is overdue %}OVERDUE{% else %}on time{% endif %}\n"
        "{{ shelf_label('Fiction') }} | {{ site_name }} | {{ now().year >= 2026 }} | {{ greet() }}"
    ),
})

SYMBOLS = {"EUR": "\u20ac", "GBP": "\u00a3", "USD": "$"}


@app.template_filter()
def currency(amount, code="EUR"):
    return f"{SYMBOLS[code]}{Decimal(amount):,.2f}"


@app.template_filter("time_tag")
def time_tag(day):
    """A semantic <time> element. The attribute and the text are escaped; no script is generated."""
    iso = day.isoformat()
    return Markup('<time datetime="{}">{}</time>').format(iso, day.strftime("%d %b %Y"))


@app.template_test("overdue")
def is_overdue(day):
    return day < date(2026, 10, 7)


@app.template_global()
def shelf_label(name):
    return f"Shelf: {name}"


@app.template_global("now")
def now():
    return datetime.now(timezone.utc)


@app.template_global()
@pass_context
def greet(context):
    # a context function sees the template variables; here the request-independent title
    return f"Hello {context.get('title', 'reader')}"


app.jinja_env.globals["site_name"] = "Bookshelf"     # a constant global

with app.test_request_context():
    html = render_template("book.html", price=1234.5, published=date(2026, 1, 9),
                           due=date(2026, 1, 1), title="Ada")
    assert html.splitlines()[0] == '\u20ac1,234.50 | \u00a31,234.50 | <time datetime="2026-01-09">09 Jan 2026</time>'
    assert html.splitlines()[1] == "OVERDUE"
    assert html.splitlines()[2] == "Shelf: Fiction | Bookshelf | True | Hello Ada"
    # a filter returns plain text and lets autoescaping do its job; Markup.format escapes its arguments
    assert Markup("<b>{}</b>").format("<i>") == "<b>&lt;i&gt;</b>"

# the same registration from a blueprint, available app-wide
bp = Blueprint("ui", __name__)


@bp.app_template_filter()
def shout(value):
    return str(value).upper()        # plain str: autoescaping still applies to the result


app2 = make_app({"s.html": "{{ x|shout }}"}, name="shout")
app2.register_blueprint(bp)
with app2.test_request_context():
    assert render_template("s.html", x="<b>") == "&lt;B&gt;"

Three remarks on that block. The currency filter is deliberately simple; the money amount is a Decimal, never a float, and a real application formats currencies and dates through Babel so they follow the user’s locale (see the internationalisation page). pass_context (and pass_environment, pass_eval_context) are Jinja decorators that hand the render context to the function; use them sparingly because they couple the function to templates. Finally, globals can be anything callable or constant, but heavy objects (a database session) should not be exposed this way: pass data from the view, or use a context processor that returns a function (see the next section).

Context Processors

A context processor is a function that returns a dict. Flask calls it before every render and merges the dict into the template context, so the names become available everywhere without being passed from each view. It is the right place for data that nearly every page shows: the current user, the navigation, a site name, the number of items in the cart.

  • @app.context_processor applies to every template rendered in the application.

  • @bp.context_processor applies only to templates rendered from views of that blueprint.

  • @bp.app_context_processor is registered on the application when the blueprint is registered, so it applies to every template, like the application decorator (see Blueprints).

Order and precedence: the application’s processors run first, then the processors of the blueprints that handled the request, from the outermost to the innermost, so inner blueprints win over the application on a name clash. Values that the view passes to render_template override anything a processor returned (the original context is re-applied last). A processor that returns a function instead of a value is evaluated only when the template calls it, which is how you avoid running an expensive query for pages that never use it.

from flask import Blueprint, g, request

app = make_app({
    "page.html": "{{ site }}|{{ shelf_count() }}|{{ title }}|{{ area }}",
})
admin = Blueprint("admin", __name__, url_prefix="/admin")
calls = []


@app.context_processor
def inject_site():
    return {"site": "Bookshelf", "title": "from the processor", "area": "public"}


@admin.context_processor
def inject_admin():
    return {"area": "admin"}                      # only for templates rendered by admin views


@admin.app_context_processor
def inject_counter():
    def shelf_count():                           # evaluated lazily, only when called
        calls.append(request.path)
        return 7
    return {"shelf_count": shelf_count}


@app.get("/")
def home():
    return render_template("page.html", title="from the view")


@admin.get("/")
def dashboard():
    return render_template("page.html")


app.register_blueprint(admin)
client = app.test_client()
assert client.get("/").get_data(as_text=True) == "Bookshelf|7|from the view|public"
assert client.get("/admin/").get_data(as_text=True) == "Bookshelf|7|from the processor|admin"
assert calls == ["/", "/admin/"]

Two cautions. A processor runs for every render, including partial renders for HTMX fragments and e-mail bodies, so it must be cheap and must not require a request context unless it guards for it (request is only truthy while one is active). And a processor must never return secrets: everything it returns is readable by every template author.

Streaming a Template

stream_template(name, **context) and stream_template_string(source, **context) (added in Flask 2.2) return an iterator of strings instead of one string, because Jinja can generate output piece by piece. Returning that iterator from a view streams the page to the browser while it is still being computed, which helps a large table or a page that waits on a slow source. If a request context is active, Flask keeps it alive while the generator runs (the same mechanism as stream_with_context), so request, session and url_for keep working. The template_rendered signal is sent when the generator is exhausted, not when it is created.

from flask import Response, stream_template

app = make_app({
    "report.html": (
        "<h1>{{ title }}</h1>\n"
        "{% for row in rows() %}<p>{{ loop.index }}. {{ row }} ({{ request.args.get('mode', 'full') }})</p>\n{% endfor %}"
    ),
})


def slow_rows():
    for name in ("Dune", "Emma", "Ulysses"):
        yield name


@app.get("/report")
def report():
    return Response(stream_template("report.html", title="Report", rows=slow_rows))


client = app.test_client()
reply = client.get("/report?mode=lazy")
body = reply.get_data(as_text=True)
assert body.startswith("<h1>Report</h1>")
assert "3. Ulysses (lazy)" in body
assert "Content-Length" not in reply.headers

The caveats of streaming apply (headers and the status are sent before the body, a failure halfway cannot change the status, each streamed request occupies a worker). The streaming page of this section covers them in depth, and Responses & JSON shows streaming a generator. Template inheritance and macros work normally in a streamed template.

Settings That Affect Templates

Setting Default Effect

TEMPLATES_AUTO_RELOAD

None

None means "follow debug mode": templates are checked for changes on every render when debug is on, and compiled once otherwise. True always checks the file’s modification time, False never does.

EXPLAIN_TEMPLATE_LOADING

False

When True, Flask logs (at INFO level on app.logger) every loader it tried for a template and whether it matched, and warns when several matched (see Loaders and Blueprint Template Folders).

DEBUG

False

Besides the debugger, it turns template auto reload on when TEMPLATES_AUTO_RELOAD is None.

TEMPLATES_AUTO_RELOAD is read when the environment is created and app.debug = value updates jinja_env.auto_reload when the setting is None; setting it late is therefore unreliable, put it in the config object of the factory. In production leave it off: with reloading on, every render stats the file, and a template edited on disk changes a running server’s pages.

Everything else is an option of the Jinja Environment, passed through app.jinja_options (a class attribute, set on a Flask subclass or on the application before the first template is used) or on the environment object itself. Useful options are:

Option Why

undefined=jinja2.StrictUndefined

Raise UndefinedError for a missing variable instead of printing an empty string. A typo is found in tests rather than as a blank cell in production.

trim_blocks=True, lstrip_blocks=True

Remove the newline after a block tag and the indentation before it, giving cleaner output from indented templates.

autoescape=...

Replace the default rule (not recommended).

extensions=[...]

Enable Jinja extensions such as jinja2.ext.do.

loader=...

A custom loader; replaces the Flask loader, so blueprint template folders stop working (see Loaders and Blueprint Template Folders).

keep_trailing_newline=True

Keep the final newline of a template, important for plain-text output.

from jinja2 import StrictUndefined, UndefinedError


class StrictFlask(Flask):
    jinja_options = {**Flask.jinja_options, "undefined": StrictUndefined,
                     "trim_blocks": True, "lstrip_blocks": True}


root = TMP / "strict"
root.mkdir(exist_ok=True)
(root / "t.html").write_text("{% if show %}\n  <p>{{ name }}</p>\n{% endif %}\n", encoding="utf-8")
strict = StrictFlask("strict", template_folder=str(root))

with strict.test_request_context():
    assert render_template("t.html", show=True, name="Ada") == "  <p>Ada</p>\n".replace("  ", "  ")
    try:
        render_template("t.html", show=True)             # forgot name
    except UndefinedError as exc:
        assert "'name' is undefined" in str(exc)
    else:
        raise AssertionError("StrictUndefined should have raised")

# auto reload follows debug unless TEMPLATES_AUTO_RELOAD is set
plain = Flask("plain")
assert plain.jinja_env.auto_reload is False
forced = Flask("forced")
forced.config["TEMPLATES_AUTO_RELOAD"] = True
assert forced.jinja_env.auto_reload is True
debugging = Flask("debugging")
debugging.debug = True
assert debugging.jinja_env.auto_reload is True

Loaders and Blueprint Template Folders

Jinja finds a template through a loader. Flask installs a DispatchingJinjaLoader that asks, in this order:

  1. the application’s own loader (app.jinja_loader), a FileSystemLoader on the application’s template_folder;

  2. then the loader of each blueprint that has a template_folder, in the order the blueprints were registered.

The first loader that has the template wins, and there is no prefix: the blueprint’s name is not part of the template name. So render_template("index.html") in two blueprints that both ship index.html renders the same file in both (the first match), and an application template silently overrides a blueprint template of the same name. That override is useful (a project can theme a reusable blueprint by shadowing its templates) and dangerous (an accidental clash). The convention that avoids clashes is to put each blueprint’s files in a sub-folder named after the blueprint: catalog/templates/catalog/list.html, rendered as render_template("catalog/list.html").

A blueprint declares its folder with Blueprint("catalog", __name__, template_folder="templates"); the path is relative to the blueprint’s root_path, the folder of the module that created it. The template_folder="templates" of a Flask application is the same thing for the application.

When a template is not found or the wrong one is loaded, set EXPLAIN_TEMPLATE_LOADING = True. The log lists every loader tried, with the folder searched and no match or found for each, and warns when more than one loader matched.

import io

from flask import Blueprint

bp_root = TMP / "bp_catalog" / "templates"
(bp_root / "catalog").mkdir(parents=True, exist_ok=True)
(bp_root / "catalog" / "list.html").write_text("catalog list", encoding="utf-8")
(bp_root / "shared.html").write_text("from the blueprint", encoding="utf-8")

catalog = Blueprint("catalog", "bp_catalog", root_path=str(bp_root.parent), template_folder="templates")

app = make_app({"shared.html": "from the app"}, name="loading")
app.register_blueprint(catalog)
app.config["EXPLAIN_TEMPLATE_LOADING"] = True

stream = io.StringIO()
handler = logging.StreamHandler(stream)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

with app.test_request_context():
    assert render_template("catalog/list.html") == "catalog list"       # found in the blueprint folder
    assert render_template("shared.html") == "from the app"             # the application wins a name clash

log = stream.getvalue()
assert "Locating template 'catalog/list.html'" in log
assert "trying loader of blueprint 'catalog'" in log
assert "Warning: multiple loaders returned a match for the template." in log   # shared.html matched twice
app.logger.removeHandler(handler)

# the combined search path, listed by name
names = sorted(app.jinja_env.list_templates())
assert names == ["catalog/list.html", "shared.html"]

Source: Jinja: Loaders

To add folders that no blueprint owns (a shared theme in another package), override jinja_loader, or pass a Jinja ChoiceLoader that contains Flask’s own loader first:

from jinja2 import ChoiceLoader, DictLoader


class ThemedFlask(Flask):
    def create_global_jinja_loader(self):
        return ChoiceLoader([super().create_global_jinja_loader(), DictLoader({"theme/footer.html": "theme footer"})])


themed = ThemedFlask("themed", template_folder=str(TMP / "none"))
with themed.test_request_context():
    assert render_template("theme/footer.html") == "theme footer"

Python packages can also be searched with Jinja’s PackageLoader. It is mostly useful for libraries that ship their own templates (for example the template of a mail renderer) rather than for a Flask application, whose folders are already known. Passing a bare loader through jinja_options replaces the dispatching loader and with it every blueprint folder, which is why the example above wraps the Flask loader instead.

Server-Side Template Injection

render_template_string(source) evaluates source as a template. If any part of source comes from a request, the visitor writes template code: {{ 7*7 }} becomes 49, and expressions that walk Python objects (for example {{ config }}, which prints the secret key, or attribute chains reaching __globals__ of a function) can read secrets or run code. This is server-side template injection, and it is the most dangerous mistake of this page.

The fix is always the same: user data goes into the context, never into the template source.

import secrets

app = Flask("ssti")
app.config["SECRET_KEY"] = secret = secrets.token_hex(8)    # a stand-in; real keys come from the environment

with app.test_request_context():
    name = "{{ config['SECRET_KEY'] }}"                  # what an attacker sends as ?name=...

    unsafe = render_template_string("Hello " + name)    # WRONG: the data became template code
    assert unsafe == "Hello " + secret

    safe = render_template_string("Hello {{ name }}", name=name)   # RIGHT: data stays data
    assert safe == "Hello {{ config[&#39;SECRET_KEY&#39;] }}"

Source: Jinja: Sandbox

Rules that follow from it: render templates from files you wrote, never from strings that contain request, database or file-upload content; if users must write templates (an e-mail designer, a report builder), render them in jinja2.sandbox.SandboxedEnvironment, with a small whitelist of context values, and still treat the sandbox as a defence in depth, not a guarantee. Python’s str.format and f-strings are also unsuitable for building HTML from user values; use templates or Markup.format.

Template Signals and Testing

Flask sends two Blinker signals around every render: before_render_template(app, template, context) just before it and template_rendered(app, template, context) just after. They make templates testable without parsing HTML: a test can record which template was used and with which variables. The signals page of this section covers Blinker in general.

from flask import template_rendered


app = make_app({"detail.html": "<h1>{{ book['title'] }}</h1>"})


@app.get("/books/<int:book_id>")
def detail(book_id):
    return render_template("detail.html", book={"id": book_id, "title": "Dune"})


recorded = []


def record(sender, template, context, **extra):
    recorded.append((template.name, dict(context)))


with template_rendered.connected_to(record, app):
    reply = app.test_client().get("/books/7")

assert reply.status_code == 200
name, context = recorded[0]
assert name == "detail.html" and context["book"]["id"] == 7
assert "request" in context and "g" in context           # the standard context was merged in

Pitfalls and Checklist

  • A missing variable prints nothing. The default Undefined renders as an empty string, so typos go unnoticed. Use StrictUndefined in tests and development at least.

  • Data in <script> without tojson. Escaping for HTML is not escaping for JavaScript.

  • |safe on request data, Markup(user) and render_template_string("..." + user). The three ways an XSS or SSTI is introduced in a Flask application. Review every occurrence.

  • Business logic in templates. Templates should format and iterate. Queries, authorisation decisions and calculations belong to views or services; a template that calls db.session is a smell.

  • Name clashes between blueprints. Use blueprint_name/page.html, and turn on EXPLAIN_TEMPLATE_LOADING when a page shows the wrong file.

  • Reading request outside a request. Pass explicit values to templates rendered by commands and jobs.

  • Filters registered late. Register filters, tests, globals and context processors in the factory, before the first request.

  • Whitespace. Indented templates produce indented HTML; it is harmless in HTML, wrong in plain text e-mails. Use - markers or trim_blocks/lstrip_blocks.

  • Debug mode in production. Besides the debugger risk, it turns template reloading on.

  • Printing config. {{ config }} shows every secret. Print single keys only.

  • Caching rendered pages with per-user data. Reading session makes the response Vary: Cookie; do not cache a page that shows the user’s name for other users. The caching page of this section shows fragment caching.

What Changed and Where Older Material Is Outdated

Version Change Impact

Flask 2.2

stream_template and stream_template_string were added.

Streamed pages no longer need a hand-written generator around a template.

Flask 2.2.3

Autoescaping is on for .svg files.

Inline SVG templates are escaped; a template that relied on raw output must use |safe deliberately.

Flask 2.3 / 3.0

flask.Markup and flask.escape were deprecated in 2.3 and removed in 3.0.

Import Markup and escape from markupsafe.

Jinja 3.1

jinja2.Markup and jinja2.escape were removed.

Older tutorials and the second edition of the Flask cookbook use from jinja2 import Markup, which fails with an ImportError.

Jinja 3.0 / 3.1

pass_context, pass_environment and pass_eval_context replaced contextfunction, environmentfunction and evalcontextfunction in 3.0; the old names were removed in 3.1.

Old filters and globals that use the old decorators fail on import.

Flask 3.2 (not released)

template_filter, template_test and template_global can be used without parentheses.

@app.template_filter will work as well as @app.template_filter(). Keep the parentheses to stay compatible with 3.1.

Source: Flask: Changes

Source: Jinja: Changes

The Flask Framework Cookbook (3rd edition 2023, and the 2019 second edition) has a chapter on templating that covers a base layout, block inheritance, context processors, a currency filter and form macros. All of those ideas are current; the points to correct are these:

  • The second edition imports Markup from jinja2, which no longer works. Use markupsafe.Markup.

  • The book’s date-formatting helper assembles a <script> element with document.write(...) through string formatting inside Markup. Interpolating values into Markup with % or an f-string defeats escaping and puts data in a JavaScript context without tojson. Prefer the <time datetime="..."> element produced with Markup.format (shown above) and let the browser or Babel format it for the user’s locale, or use tojson for data that must reach a script.

  • Examples that call render_template_string with request data are SSTI (see Server-Side Template Injection).

  • Bootstrap versions in older books are old: take the current major version’s documentation, load the stylesheet from your own static folder or from a pinned CDN URL with a Subresource Integrity hash, and do not copy versions from a book.

Summary

  • Templates are looked up in the application’s templates folder and then in blueprint folders; the first match wins, so give blueprint templates a sub-folder named after the blueprint.

  • Autoescaping is on for .html, .htm, .xml, .xhtml, .svg and for string templates, off elsewhere. |safe and Markup are promises; import Markup from markupsafe.

  • Use tojson for data in scripts, quote attributes, validate URL schemes.

  • Every template sees config, request, session, g, url_for and get_flashed_messages; imports do not see your variables unless with context.

  • Inheritance (extends, block, super()), include and macros build the layout; filters, tests, globals and context processors extend the language, registered in the factory.

  • stream_template streams a page; EXPLAIN_TEMPLATE_LOADING explains loader decisions; StrictUndefined finds typos.

  • Never build template source from user data.

The next pages of this section continue with static files and assets, sessions and flashing, forms with Flask-WTF and security considerations, which all depend on what this page covers.