Templates & UI Modules
|
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 Tornado 6.5.10 (released 2026-09-15) on Python 3.13, checked on 2026-10-07. Tornado 6.6
is only available as the alpha 6.6a1 at that date, so anything about 6.6 is upcoming and may change; nothing on this
page depends on it. Tornado ships its own small template engine, tornado.template, and a component mechanism built
on top of it, the UI modules. This page covers both, plus the escape hatches (ui_methods, a custom
render_string, using Jinja instead). The running scenario is Bookshelf Live: a shelf page that lists books,
shows who is signed in, embeds a form carrying an XSRF token and renders each book as a reusable card. The Flask counterpart of this page is
Jinja Templates, which uses Jinja rather than a built-in engine; the
use of Jinja inside Tornado is described in Using Jinja Instead.
How Tornado Renders HTML
A Tornado template is not interpreted. tornado.template.Template parses the file and compiles it into Python source,
then `compile()`s that source once; rendering executes the generated function. The consequences shape everything else
on this page:
-
Statements are Python.
{% if %},{% for %},{% while %}and{% try %}are translated one to one into Python statements, so any Python expression is allowed in them and in{{ }}. There is no sandbox. Templates must be trusted code written by developers, never strings built from user input. -
Rendering is synchronous. A template (and a UI module) cannot
await. Do all I/O in the handler, pass the results in, and keep template logic cheap, because a slow render blocks the event loop for every other connection (see Asynchronous I/O & the Event Loop). -
Compiled templates are cached by a loader, which is what makes repeated renders fast and what
compiled_template_cachecontrols.
The handler API sits on top: RequestHandler.render(template_name, **kwargs) renders and finishes the response,
RequestHandler.render_string(template_name, **kwargs) returns the bytes without writing them. render() calls
render_string(), then inserts the CSS and JavaScript that UI modules asked for, then calls finish().
(template_path or template_loader)"] L -- "cache hit or compile" --> T["Template
compiled Python function"] NS["get_template_namespace()
+ ui_methods + ui_modules
+ keyword arguments"] --> T T --> HTML["HTML bytes"] HTML --> INS["insert module resources
CSS before head end,
JS before body end"] INS --> F["finish()"]
Source: Tornado: RequestHandler.render
A Helper to Run the Examples
All examples below are executed against real loopback servers. This helper starts the application on a free port,
makes one request with AsyncHTTPClient and shuts the server down. Test classes based on AsyncHTTPTestCase do the
same thing; one is shown at the end of the page.
import asyncio
import pathlib
import secrets
import tempfile
from tornado import escape, template, web
from tornado.httpclient import AsyncHTTPClient
from tornado.httpserver import HTTPServer
from tornado.testing import bind_unused_port
async def _get(app, path, headers=None):
sock, port = bind_unused_port()
server = HTTPServer(app)
server.add_sockets([sock])
client = AsyncHTTPClient()
try:
return await client.fetch(
f"http://127.0.0.1:{port}{path}", headers=headers, raise_error=False
)
finally:
client.close()
server.stop()
def get(app, path, headers=None):
return asyncio.run(_get(app, path, headers))
Template Syntax
Expressions go between double curly braces and are escaped by default; statements use {% %} and, unlike Python,
are closed with {% end %} instead of indentation. Comments are {# ... #}. The escaped forms {{!, {%!
and {#! print the literal opening delimiter.
| Construct | Meaning |
|---|---|
|
Evaluates any Python expression and inserts the result, escaped with the current autoescape function. |
|
The Python |
|
Loops; |
|
The Python |
|
Sets a local variable. |
|
The Python import statements. |
|
Passes everything rendered between |
|
Inserts the expression without escaping. |
|
Sets the escape function (or |
|
Sets the whitespace mode for the rest of the file. |
|
Template inheritance, see below. |
|
Textually includes another template, sharing the current variables. |
|
Renders a UI module; its output is not escaped. See UI Modules. |
|
A comment that runs to the closing delimiter and has no |
The template body below is rendered directly with Template.generate(), with no handler and no loader. That is
enough for plain statements and expressions, but extends, include and module need a loader (and module needs a
RequestHandler), which is why real applications go through render().
t = template.Template("""<ul>
{# not in the output #}
{% set total = len(books) %}
{% for b in books %}<li>{{ b['title'] }} ({{ b['year'] }}){% if b['year'] < 1950 %} classic{% end %}</li>
{% end %}</ul>
<p>{{ total }} books. Literal: {{! total }} and {%! if %}</p>
{% apply linkify %}see https://www.tornadoweb.org/ now{% end %}
<p>{% raw "<b>trusted</b>" %} vs {{ "<b>untrusted</b>" }}</p>""")
books = [{"title": "Dune <1965>", "year": 1965}, {"title": "R&R", "year": 1920}]
print(t.generate(books=books).decode())
Two details are easy to miss. First, generate() returns bytes. Second, names beginning with _tt_ are reserved by
the engine and must not be used for your own variables or namespace entries.
The Template Namespace
Every template sees a namespace assembled from three layers: the defaults of tornado.template, the result of
RequestHandler.get_template_namespace(), and the keyword arguments passed to render(). Later layers win.
| Name | What it is |
|---|---|
|
|
|
|
|
|
|
Collapse whitespace; turn plain URLs into links (escaping the text first). |
|
The standard library module. |
|
The current |
|
The value of |
|
The request’s |
|
|
|
|
|
|
|
A lazy namespace of the registered UI modules (the |
every key of |
Application-wide helper functions, see ui_methods. |
To add application-wide values, override get_template_namespace() in a base handler and call super(). Values
that depend on one page are simply keyword arguments to render().
Template Files, template_path and Inheritance
Real applications keep templates in a directory named by the template_path application setting. Paths passed to
render(), {% extends %}, {% include %} and {% module Template() %} are resolved relative to it; names used by include and extends are first tried relative to the directory of the
including file and, if that does not stay inside the root, from template_path. Individual handlers can return another directory
from get_template_path(), for example to give an admin area its own templates; returning None makes Tornado load
templates relative to the file that calls render().
Settings that influence templates are all Application settings (keyword arguments of the constructor):
| Setting | Default | Effect |
|---|---|---|
|
none |
Directory searched by the default |
|
none |
An instance of a |
|
|
Name of the escape function applied to every |
|
|
How whitespace is treated in templates, see Whitespace. |
|
|
Keep compiled templates in the loader. |
|
empty |
Registers UI modules and helper functions, see below. |
Source: Tornado: Application settings
extends, block, include and raw
{% extends %} makes a template a child of another one. Everything in the child outside a {% block %} is
ignored; the blocks replace the same-named blocks of the parent. {% include %} pastes another template in place
and shares the current local variables (apart from the autoescape directive). {% module Template(...) %} renders
a template with an isolated namespace instead; it is described with the UI modules.
The next example is the whole Bookshelf Live shelf page. The TEMPLATES dictionary stands in for the files of a
templates/ directory (the helper writes them to a temporary directory), and STATIC for static/. The page uses
current_user, reverse_url, static_url, locale, _, an included footer and the XSRF form field.
TEMPLATES = {
"base.html": """<!doctype html>
<html lang="{{ locale.code.replace('_', '-') }}">
<head>
<meta charset="utf-8">
<title>{% block title %}Bookshelf Live{% end %}</title>
<link rel="stylesheet" href="{{ static_url('css/app.css') }}">
</head>
<body>
<header>{% if current_user %}Signed in as {{ current_user }}{% else %}<a href="{{ reverse_url('login') }}">Sign in</a>{% end %}</header>
<main>
{% block content %}{% end %}
</main>
{% include "footer.html" %}
</body>
</html>
""",
"footer.html": """<footer>{{ site_name }}: {{ len(books) }} {{ _("books on the shelf") }}</footer>
""",
"shelf.html": """{% extends "base.html" %}
{% block title %}{{ shelf_title }} - Bookshelf Live{% end %}
{% block content %}
<h1>{{ shelf_title }}</h1>
<ul>
{% for book in books %}
<li><a href="{{ reverse_url('book', book['id']) }}">{{ book['title'] }}</a> by {{ book['author'] }}</li>
{% end %}
</ul>
<form method="post" action="/books">
{% module xsrf_form_html() %}
<input name="title"><button>Add</button>
</form>
{% end %}
""",
}
STATIC = {
"css/app.css": "body { font-family: sans-serif; }\n",
"css/book-card.css": ".book-card { border: 1px solid #888; }\n",
"js/cards.js": "console.log('cards');\n",
"js/rating.js": "console.log('rating');\n",
}
def write_tree(root, files):
for name, content in files.items():
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
SITE = pathlib.Path(tempfile.mkdtemp(prefix="bookshelf-live-"))
write_tree(SITE / "templates", TEMPLATES)
write_tree(SITE / "static", STATIC)
BOOKS = [
{"id": 1, "title": "Dune <Part 1>", "author": "Frank Herbert", "rating": 5},
{"id": 2, "title": "R&R", "author": "A. Writer", "rating": 3},
]
class BaseHandler(web.RequestHandler):
def get_current_user(self):
value = self.get_signed_cookie("user")
return value.decode() if value else None
def get_template_namespace(self):
namespace = super().get_template_namespace()
namespace.update(site_name="Bookshelf Live")
return namespace
class ShelfHandler(BaseHandler):
def get(self):
self.render("shelf.html", shelf_title="Reading now", books=BOOKS)
class BookHandler(BaseHandler):
def get(self, book_id):
self.write(f"book {book_id}")
class LoginHandler(BaseHandler):
def get(self):
self.write("login form")
COOKIE_SECRET = secrets.token_hex(32) # in production: read it from the environment
def make_app(**overrides):
settings = dict(
template_path=str(SITE / "templates"),
static_path=str(SITE / "static"),
cookie_secret=COOKIE_SECRET,
xsrf_cookies=True,
)
settings.update(overrides)
return web.Application(
[
web.url(r"/", ShelfHandler, name="shelf"),
web.url(r"/books/([0-9]+)", BookHandler, name="book"),
web.url(r"/login", LoginHandler, name="login"),
],
**settings,
)
app = make_app()
signed = web.create_signed_value(COOKIE_SECRET, "user", "ana").decode()
response = get(app, "/", headers={"Cookie": f"user={signed}"})
print(response.code)
print(response.body.decode())
The output shows the inheritance resolved, the title block replaced, Dune <Part 1> and R&R escaped, the login
link replaced by the signed-in name and the static_url link carrying a content hash in a v query parameter. When
the cookie is absent, current_user is None and the "Sign in" link is rendered instead.
Autoescaping and Its Limits
Every {{ }} expression is passed through xhtml_escape, which replaces &, <, >, " and ' with entities
(since Tornado 6.4 it is a thin wrapper around html.escape, so a single quote becomes ' rather than
'). This neutralises injected markup in HTML text and in quoted attribute values. It is not a universal
defence: the escape function only knows HTML. The context decides whether the result is safe.
| Where the value lands | Problem with plain {{ }} |
What to do |
|---|---|---|
Element text |
Safe. |
Nothing extra. |
Quoted attribute value |
Safe. |
Always quote attributes. |
URL attribute ( |
|
Validate the scheme in Python (allow |
Inside a script element or event handler |
HTML entities are not decoded inside a script element, and JavaScript string quoting is different. |
Pass data with |
Inside a style element or |
CSS has its own escapes; HTML escaping is neither necessary nor sufficient. |
Never interpolate untrusted values; choose from a fixed set of classes. |
Output of a UI module or |
Not escaped at all. |
Escape inside the module’s own template, or pass trusted markup only. |
The following example puts a hostile value in each context. The value </script><script>alert(1)</script> is
rendered once as text, once in a JavaScript context the wrong way, and once the right way with json_encode, which
never emits the sequence </ and so cannot close the surrounding script element.
t = template.Template("""<p>{{ v }}</p>
<script>var wrong = "{{ v }}";</script>
<script>var right = {% raw json_encode(v) %};</script>
<a href="/search?q={{ url_escape(v) }}">search</a>""")
hostile = "</script><script>alert(1)</script>"
print(t.generate(v=hostile).decode())
The "wrong" line is also a bug without any attacker: a title such as Tom's book becomes Tom's book inside the
JavaScript string. The right line is valid JavaScript.
To disable escaping, use {% raw expr %} for one expression, {% autoescape None %} for one file, or the
autoescape setting with None for the whole application. Disabling it globally is almost never a good idea; a
better pattern is to leave it on and mark individual trusted fragments with raw. An alternative escape function can
be registered in the namespace and selected by name, for example {% autoescape my_escape %} or
autoescape="my_escape", which is how a JSON-only or plain-text template can use a different policy. Templates not
ending in .html are still autoescaped (unlike Flask, which keys the default on file extension).
Loaders, Caching and Whitespace
The default loader is tornado.template.Loader(root_directory). It resolves names to files under the root, compiles
each file once and caches the Template. DictLoader(dict) does the same for templates held in a dictionary, which
is handy in tests. Both extend BaseLoader, whose load(name, parent_path=None) and reset() methods are the public
interface, and a custom loader overrides resolve_path and _create_template. A loader constructor also takes
autoescape, namespace (extra names for every template) and whitespace.
Pass an instance as the template_loader setting to replace the default. The following loader serves templates from a
dictionary and adds a global name to every template:
loader = template.DictLoader(
{"hello.html": "<p>{{ greeting }}, {{ name }}! {{ BRAND }}</p>"},
namespace={"BRAND": "Bookshelf Live"},
)
class HelloHandler(web.RequestHandler):
def get(self):
self.render("hello.html", greeting="Hello", name="<reader>")
dict_app = web.Application(
[(r"/hello", HelloHandler)],
template_loader=loader,
)
print(get(dict_app, "/hello").body.decode())
compiled_template_cache and Debug Mode
Because the loader caches compiled templates, editing a file has no effect on a running server unless the cache is
off. debug=True turns compiled_template_cache (and static_hash_cache) off, together with autoreload, which is
why edits show up immediately during development and why development timings are not representative. In production
leave the cache on. The next example proves the behaviour by editing a template between two requests:
class EditedHandler(web.RequestHandler):
def get(self):
self.render("edited.html")
edit_dir = SITE / "edit"
edit_dir.mkdir()
(edit_dir / "edited.html").write_text("version 1")
results = {}
for cache in (True, False):
app_edit = web.Application(
[(r"/", EditedHandler)],
template_path=str(edit_dir),
compiled_template_cache=cache,
)
async def two_requests(app=app_edit):
sock, port = bind_unused_port()
server = HTTPServer(app)
server.add_sockets([sock])
client = AsyncHTTPClient()
try:
url = f"http://127.0.0.1:{port}/"
first = (await client.fetch(url)).body.decode()
(edit_dir / "edited.html").write_text("version 2")
second = (await client.fetch(url)).body.decode()
(edit_dir / "edited.html").write_text("version 1")
return first, second
finally:
client.close()
server.stop()
# reset the process-wide loader cache between the two runs
web.RequestHandler._template_loaders.clear()
results[cache] = asyncio.run(two_requests())
print(results)
With the cache on the second response still says version 1; with it off, version 2. (The example clears the
private _template_loaders class attribute only to restart cleanly inside one process; application code never needs
to.) The loaders are shared per template directory across all RequestHandler instances in the process, so the cache
is also shared between applications that use the same template_path.
Whitespace Handling
The template_whitespace setting (and the whitespace argument of Loader, DictLoader and Template, and the
{% whitespace mode %} directive) selects how runs of whitespace in the template source are emitted:
| Mode | Behaviour |
|---|---|
|
Everything is kept unchanged. |
|
Runs of spaces and tabs collapse to one space, and a newline with its surrounding whitespace collapses to a single
newline. This is the default for files ending in |
|
All whitespace runs, including newlines, collapse to one space. Useful for HTML meant to be small, but it will
change the meaning of |
The directive was added in Tornado 4.3; the old boolean compress_whitespace argument is deprecated since the same
version. A quick comparison:
source = "<ul>\n <li>a</li>\n <li>b</li>\n</ul>\n"
for mode in ("all", "single", "oneline"):
print(mode, repr(template.Template(source, whitespace=mode).generate().decode()))
UI Modules
A UI module is a reusable fragment of page: a class with a render() method that returns markup, optionally
together with the CSS and JavaScript it needs. It is the Tornado answer to "I use this card in six places and want
its styles and scripts to come along exactly once", and the nearest equivalent in Flask is a Jinja macro or include
combined with manual asset wiring.
Subclass tornado.web.UIModule, register the class in the ui_modules setting and invoke it from a template with
{% module Name(arguments) %}. The arguments are passed to render(). A module instance has handler, request,
current_user, locale and ui attributes and a render_string() method that renders a template with the module’s
handler. The module’s return value is inserted without escaping, so the usual pattern is for render() to delegate to
a template that escapes, as in this example.
| Method | Purpose |
|---|---|
|
Required. Returns the HTML for the module. |
|
Iterable of stylesheet paths. Relative paths go through |
|
Same for scripts. |
|
A CSS string inserted in a |
|
A JavaScript string inserted in a |
|
Raw HTML inserted before the end of the head and body. |
TEMPLATES.update({
"modules/book_card.html": """<article class="book-card{% if highlight %} featured{% end %}">
<h2>{{ book['title'] }}</h2>
<p>{{ book['author'] }}</p>
</article>
""",
"cards.html": """{% extends "base.html" %}
{% block content %}
{% for book in books %}
{% module BookCard(book, highlight=book['rating'] >= 5) %}
{% end %}
{% end %}
""",
})
write_tree(SITE / "templates", TEMPLATES)
class BookCard(web.UIModule):
def render(self, book, highlight=False):
return self.render_string("modules/book_card.html", book=book, highlight=highlight)
def css_files(self):
return ["css/book-card.css", "https://cdn.example.test/ui.css"]
def embedded_css(self):
return ".book-card.featured { border-width: 3px; }"
def javascript_files(self):
return "js/cards.js"
def embedded_javascript(self):
return "document.title += ' (cards)';"
class CardsHandler(BaseHandler):
def get(self):
self.render("cards.html", books=BOOKS)
cards_app = make_app(ui_modules={"BookCard": BookCard})
cards_app.add_handlers(r".*", [web.url(r"/cards", CardsHandler)])
print(get(cards_app, "/cards").body.decode())
Source: Tornado User’s Guide: UI modules
In the output the two cards are rendered in place, and the resources appear once even though the module was used
twice: the stylesheet links and the embedded style before the end of the head, and the script link and the embedded
script before the end of the body. The order of css_files and javascript_files is preserved and duplicates
are removed. Two facts about this machinery matter in practice:
-
A module is instantiated once per handler (per request) and kept by name, so
render()is called on the same object for every{% module %}of that name on the page, and the resource methods are called once, after the template has rendered. Do not rely on the resource methods seeing the arguments of one particular call. -
The insertion points are found by searching the finished HTML for the last
</body>and the first</head>. A page that uses modules with resources but lacks those tags makesrender()raise an error (for example from a partial or an HTMX fragment). Render fragments withrender_string()and write them yourself, or use a module without resources.
The embedded script is inline JavaScript. A strict Content Security Policy that forbids inline scripts (for
instance script-src 'self') blocks it; prefer javascript_files() when you adopt such a policy.
The Built-in Template Module and set_resources
Registering a class for every fragment is heavy when all you want is "include this template with its own variables".
Tornado provides {% module Template("path.html", **kwargs) %} for this. It differs from {% include %} in that
the fragment gets its own namespace, containing only the keyword arguments, so it cannot accidentally read or
overwrite the page’s variables. Inside such a template, the function set_resources(...) registers the same kinds of
resources as the UIModule methods, with keyword arguments css_files, embedded_css, javascript_files,
embedded_javascript, html_head and html_body. Resources are output once per template file, not once per use, so
they must not depend on the arguments.
TEMPLATES.update({
"modules/rating.html": """{{ set_resources(embedded_css=".stars { color: #c80; }", javascript_files="js/rating.js") }}<span class="stars">{{ "★" * rating }}</span>""",
"rated.html": """{% extends "base.html" %}
{% block content %}
{% for book in books %}
<p>{{ book['title'] }} {% module Template("modules/rating.html", rating=book['rating']) %}</p>
{% end %}
{% end %}
""",
})
write_tree(SITE / "templates", TEMPLATES)
class RatedHandler(BaseHandler):
def get(self):
self.render("rated.html", books=BOOKS)
rated_app = make_app()
rated_app.add_handlers(r".*", [web.url(r"/rated", RatedHandler)])
body = get(rated_app, "/rated").body.decode()
print(body.count("rating.js"), body.count(".stars { color"), "★★★★★" in body)
Source: Tornado: tornado.web.UIModule
The script link and the style appear once although two modules were rendered. The Template module, linkify and
xsrf_form_html are the three modules Tornado registers itself, which is why {% module xsrf_form_html() %} works
in the shelf template: the module version of the form field does not escape its output, whereas
{{ xsrf_form_html() }} would print the input element as visible text. Both XSRF and current_user are
discussed in Authentication & Signed Cookies and XSRF & Cross-Origin Protection.
ui_methods
UI methods are plain functions registered in the ui_methods setting. Unlike a module they have no lifecycle and no
resources; they appear in the namespace under their own names and are called like any function. Tornado calls them
with the current handler as the first argument, which you do not pass from the template. The setting accepts a
dictionary, a module or a list of them; only callables whose names do not start with an underscore or an upper-case
letter are registered.
def money(handler, cents):
symbol = {"EUR": "€", "USD": "$"}.get(handler.get_query_argument("currency", "USD"), "$")
return f"{symbol}{cents / 100:.2f}"
class PriceHandler(web.RequestHandler):
def get(self):
self.render("price.html", cents=1299)
price_app = web.Application(
[(r"/price", PriceHandler)],
template_loader=template.DictLoader({"price.html": "<p>{{ money(cents) }}</p>"}),
ui_methods={"money": money},
)
print(get(price_app, "/price?currency=EUR").body.decode())
print(get(price_app, "/price").body.decode())
Source: Tornado: Application settings
The module versus method decision is simple. Use a UI method for a small pure formatting function, a UI module for
anything with markup of its own or with assets, and a template include for static layout pieces. A function that is
the same in every request and needs no handler can instead be added to the template namespace with a loader’s
namespace argument.
Using Jinja Instead
Tornado does not integrate with Jinja, and does not need to: a template engine only has to turn a name and some
values into a string. The documented approach is to render with the other engine and write the string yourself. For a
cleaner integration, override render_string() in a base handler, so every self.render() in the application goes
through Jinja, render() still looks for UI-module resources, which will not be
found because Jinja templates do not use {% module %}, so nothing extra is inserted.
import jinja2
jinja_env = jinja2.Environment(
loader=jinja2.DictLoader({"jshelf.html": "<h1>{{ title }}</h1>{% for b in books %}<p>{{ b.title }}</p>{% endfor %}"}),
autoescape=jinja2.select_autoescape(default=True, default_for_string=True),
)
class JinjaHandler(web.RequestHandler):
def render_string(self, template_name, **kwargs):
namespace = self.get_template_namespace()
namespace.update(kwargs)
return jinja_env.get_template(template_name).render(**namespace).encode()
def get(self):
self.render("jshelf.html", title="Jinja shelf", books=BOOKS)
jinja_app = web.Application([(r"/", JinjaHandler)])
print(get(jinja_app, "/").body.decode())
Jinja’s own escaping is configured separately (select_autoescape above); Tornado’s autoescape setting does not
apply to Jinja templates. The namespace carries static_url, reverse_url, current_user and the other names
unchanged, so {{ static_url("css/app.css") }} works in Jinja too. Switching is worthwhile mainly for sandboxing
(SandboxedEnvironment) when templates are not trusted, for filters, macros and a larger ecosystem of extensions, or
when a team already knows Jinja from Flask. Keep Tornado’s own engine when you want zero dependencies, UI modules
with automatic asset insertion, and templates that are plain Python. Jinja’s render is synchronous too, so the
event loop warning above applies; Jinja’s async mode is a different rendering API and is not needed for this pattern.
Testing Templates
Test through the handler, with AsyncHTTPTestCase: assert on the status, on fragments of the body and on the
escaping of hostile data. Template syntax errors raise ParseError when the template is first loaded, so a request
for each page in the test suite is the cheapest syntax check.
import unittest
from tornado.testing import AsyncHTTPTestCase
class ShelfTests(AsyncHTTPTestCase):
def get_app(self):
return make_app()
def test_escapes_titles(self):
body = self.fetch("/").body.decode()
self.assertIn("Dune <Part 1>", body)
self.assertNotIn("<Part 1>", body)
def test_anonymous_sees_login_link(self):
self.assertIn('href="/login"', self.fetch("/").body.decode())
def test_form_has_xsrf_input(self):
self.assertIn('<input type="hidden" name="_xsrf"', self.fetch("/").body.decode())
suite = unittest.defaultTestLoader.loadTestsFromTestCase(ShelfTests)
outcome = unittest.TextTestRunner(verbosity=0).run(suite)
assert outcome.wasSuccessful()
Pitfalls
-
Untrusted template source. A template can run arbitrary Python. Never build template text from user input (server-side template injection is remote code execution) and never let users upload templates to Tornado’s engine; use a sandboxed Jinja environment for that.
-
Escaping applied once, in the wrong place. Escape on output, in the template. Storing pre-escaped text in the database produces double escaping later and does not protect other contexts.
-
{{ }}versus{% module %}for markup. A function that returns HTML, such asxsrf_form_html(), is escaped and shown as text when printed with the double-brace form; the{% module %}and{% raw %}forms do not escape at all, so the module is responsible for escaping anything user-controlled it emits. -
HTML-only escaping in JavaScript, CSS and URL contexts, see Autoescaping and Its Limits.
-
Blocking work in a template or module. A database call inside
render()blocks the loop. Fetch data in the handler. -
Missing
</head>or</body>in a page that uses modules with assets, see UI Modules. -
Cache confusion. The compiled cache is on by default; a deployment that edits templates in place will serve old versions until restart. Restart (or roll) the processes on deploy rather than turning the cache off.
-
Reserved names. Do not use
escape,url_escape,json_encode,squeeze,linkify,datetime,handler,request,modules,localeor names starting with_tt_as variables.
Recent Changes and Version Notes
| Version | Change |
|---|---|
4.3 |
|
5.1 |
|
6.4 |
|
6.5.10 |
Current stable release used on this page. |
6.6 (upcoming) |
Only an alpha exists on 2026-10-07. Check the release notes for template-related changes before relying on any behaviour described here when you upgrade. |
Related Reading
-
Getting Started for
debugmode and the first handler. -
Introduction & Architecture for where templates sit in the layer stack.
-
Flask: Jinja Templates for the Jinja equivalent, and FastAPI for a framework that leaves HTML to Jinja2 and static files to Starlette.
-
Django for another built-in template language.
Static Files, Localization (the _() function and pgettext), XSRF & Cross-Origin Protection and Authentication & Signed Cookies describe static files,
localization, and the security features behind xsrf_form_html and current_user.