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).
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>"
Source: Flask API: flask.render_template
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 |
|---|---|
|
Print the value of an expression (autoescaped, see Autoescaping). |
|
A statement: |
|
A comment, not sent to the client. |
|
Apply a filter; filters chain left to right. |
|
Apply a test such as |
|
A minus strips whitespace on that side. |
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? |
|---|---|
|
Yes. |
|
Yes. |
|
No. Escape with |
A value wrapped in |
No, the object promises to be safe. |
|
No, you promise that the value is safe. |
|
Escaped once; escaping an already escaped |
Inside |
No, until |
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><script>alert('x')</script></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("</script>")
# Markup.format escapes its arguments, so building HTML from user data stays safe
snippet = Markup("<b>{}</b>").format(hostile)
assert snippet == "<b><script>alert('x')</script></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-suppliedjavascript:alert(1)survives escaping because it contains nothing to escape. Validate the scheme on the server (allowhttp,https,mailto) or build links withurl_for. -
Inside
<script>and<style>. Escaping for HTML is wrong there. Emit data with thetojsonfilter, which produces a JSON literal safe for a script element and marks it safe:<script>const book = {{ book|tojson }};</script>. Never writevar name = '{{ name }}';. -
Event-handler attributes such as
onclick="..."hold JavaScript. Pass data throughdata-*attributes and read it from a script, or usetojsoninside a quoted attribute (tojsonescapes 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: attachmentand 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=""><b>x</b>">' 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
Source: Jinja: the tojson filter
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
Source: MarkupSafe: Escaping and Markup
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 |
|---|---|
|
The application’s |
|
The current |
|
The current session (a dict-like object; reading it marks the response |
|
The |
|
The function that builds URLs from endpoints (see Routing & URL Building). |
|
Returns the messages stored with |
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"]
Source: Jinja: Import Context Behavior
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).
Rules worth remembering:
-
extendsmust be the first tag in the child; anything outside blocks in a child is ignored (exceptset/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
extendsandincludeis evaluated against the same loader, socatalog/base.htmlis found in the application folder or in a blueprint folder (see Loaders and Blueprint Template Folders). -
A block can be marked
requiredin 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 & 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
Source: Jinja: Template Inheritance
Source: Flask: Template Inheritance
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 "quoted" <title>"' 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: Call Blocks
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 |
|---|---|
|
Adds a filter |
|
Adds a test |
|
Adds a global function or constant |
|
The same, registered when the blueprint is registered, and available to every template of the application. See Blueprints. |
|
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><i></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>") == "<B>"
Source: Jinja: Custom Filters
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_processorapplies to every template rendered in the application. -
@bp.context_processorapplies only to templates rendered from views of that blueprint. -
@bp.app_context_processoris 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
Source: Flask API: flask.stream_template
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 |
|---|---|---|
|
|
|
|
|
When |
|
|
Besides the debugger, it turns template auto reload on when |
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 |
|---|---|
|
Raise |
|
Remove the newline after a block tag and the indentation before it, giving cleaner output from indented templates. |
|
Replace the default rule (not recommended). |
|
Enable Jinja extensions such as |
|
A custom loader; replaces the Flask loader, so blueprint template folders stop working (see Loaders and Blueprint Template Folders). |
|
Keep the final newline of a template, important for plain-text output. |
Source: Flask API: Flask.jinja_options
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
Source: Flask API: Flask.jinja_options
Source: Jinja: StrictUndefined
Loaders and Blueprint Template Folders
Jinja finds a template through a loader. Flask installs a DispatchingJinjaLoader that asks, in this order:
-
the application’s own loader (
app.jinja_loader), aFileSystemLoaderon the application’stemplate_folder; -
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"
Source: Jinja: ChoiceLoader
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['SECRET_KEY'] }}"
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
Undefinedrenders as an empty string, so typos go unnoticed. UseStrictUndefinedin tests and development at least. -
Data in
<script>withouttojson. Escaping for HTML is not escaping for JavaScript. -
|safeon request data,Markup(user)andrender_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.sessionis a smell. -
Name clashes between blueprints. Use
blueprint_name/page.html, and turn onEXPLAIN_TEMPLATE_LOADINGwhen a page shows the wrong file. -
Reading
requestoutside 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 ortrim_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
sessionmakes the responseVary: 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 |
|
Streamed pages no longer need a hand-written generator around a template. |
Flask 2.2.3 |
Autoescaping is on for |
Inline SVG templates are escaped; a template that relied on raw output must use |
Flask 2.3 / 3.0 |
|
Import |
Jinja 3.1 |
|
Older tutorials and the second edition of the Flask cookbook use |
Jinja 3.0 / 3.1 |
|
Old filters and globals that use the old decorators fail on import. |
Flask 3.2 (not released) |
|
|
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
Markupfromjinja2, which no longer works. Usemarkupsafe.Markup. -
The book’s date-formatting helper assembles a
<script>element withdocument.write(...)through string formatting insideMarkup. Interpolating values intoMarkupwith%or an f-string defeats escaping and puts data in a JavaScript context withouttojson. Prefer the<time datetime="...">element produced withMarkup.format(shown above) and let the browser or Babel format it for the user’s locale, or usetojsonfor data that must reach a script. -
Examples that call
render_template_stringwith 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
templatesfolder 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,.svgand for string templates, off elsewhere.|safeandMarkupare promises; importMarkupfrommarkupsafe. -
Use
tojsonfor data in scripts, quote attributes, validate URL schemes. -
Every template sees
config,request,session,g,url_forandget_flashed_messages; imports do not see your variables unlesswith context. -
Inheritance (
extends,block,super()),includeand macros build the layout; filters, tests, globals and context processors extend the language, registered in the factory. -
stream_templatestreams a page;EXPLAIN_TEMPLATE_LOADINGexplains loader decisions;StrictUndefinedfinds 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.