Template Engine
Tornado includes a lightweight built-in template engine whose syntax is similar to Jinja2 but not identical. Templates are compiled into Python code on first load and then cached, resulting in good performance. This article covers template configuration, variable rendering, control statements, inheritance, and the built-in functions available in templates.
Basic Configuration
Configure the template directory in Application’s settings:
settings = {
"template_path": "templates", # Template root directory (relative to the startup script)
"autoescape": "xhtml_escape", # Auto-escaping (enabled by default; prevents XSS)
}Example directory structure:
project/
├── app.py
└── templates/
├── base.html
├── index.html
└── user/
└── profile.htmlRendering a template from a Handler:
class IndexHandler(tornado.web.RequestHandler):
def get(self):
self.render(
"index.html",
title="Home",
user={"name": "Alice", "age": 28},
items=["Python", "Go", "Rust"],
)Template Syntax
Variables and Expressions
Use double curly braces {{ }} to output the value of a variable or expression (output is automatically HTML-escaped):
<h1>{{ title }}</h1>
<p>User: {{ user["name"] }}, Age: {{ user["age"] }}</p>
<p>Number of items: {{ len(items) }}</p>To output raw HTML without escaping, use {% raw %}:
{% raw html_content %}Control Statements
Control statements are wrapped in {% %}. All block structures end with {% end %}:
<!-- if / elif / else -->
{% if user["age"] >= 18 %}
<p>Adult user</p>
{% elif user["age"] >= 12 %}
<p>Teen user</p>
{% else %}
<p>Child user</p>
{% end %}
<!-- for loop -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% end %}
</ul>
<!-- while loop (rarely used) -->
{% set i = 0 %}
{% while i < 3 %}
<p>Row {{ i }}</p>
{% set i = i + 1 %}
{% end %}Comments
{# This is a comment; it will not appear in the rendered output #}Template Inheritance
Tornado templates support an inheritance mechanism similar to Jinja2, using {% extends %} and {% block %} for layout reuse.
Base Template base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Default Title{% end %} - My Site</title>
<link rel="stylesheet" href="{{ static_url('css/main.css') }}">
</head>
<body>
<nav>{% block nav %}{% end %}</nav>
<main>
{% block content %}{% end %}
</main>
<footer>Copyright 2026</footer>
</body>
</html>Child Template index.html
{% extends "base.html" %}
{% block title %}Home{% end %}
{% block content %}
<h1>Welcome, {{ user_name }}!</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% end %}
</ul>
{% end %}Including Partials
{% include %} embeds another template file at the current position, sharing the parent template’s context variables:
{% include "components/header.html" %}
<main>{% block content %}{% end %}</main>
{% include "components/footer.html" %}Built-in Functions
The following built-in functions are available directly in templates:
| Function | Description |
|---|---|
escape(s) | HTML-escape a string (escapes <>, &, etc.) |
url_escape(s) | URL-encode a string |
json_encode(obj) | Serialize an object to a JSON string |
static_url(path) | Generate a versioned static file URL |
reverse_url(name, *args) | Reverse-resolve a URL from a route name |
xsrf_form_html() | Generate a hidden XSRF form field |
<!-- Anti-XSS: manually escape untrusted content -->
<p>{{ escape(user_input) }}</p>
<!-- Static file URL (automatically appended version hash, e.g. /static/css/main.css?v=3a2b1c) -->
<link rel="stylesheet" href="{{ static_url('css/main.css') }}">
<!-- XSRF protection (requires xsrf_cookies=True in settings) -->
<form method="post">
{% raw xsrf_form_html() %}
<input type="text" name="username">
<button type="submit">Submit</button>
</form>Passing Custom Functions to Templates
Keyword arguments to render() can be functions, which can be called directly in the template:
import datetime
class IndexHandler(tornado.web.RequestHandler):
def get(self):
def format_date(ts):
return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d")
self.render(
"index.html",
articles=articles,
format_date=format_date,
)Using it in the template:
{% for article in articles %}
<article>
<h2>{{ article.title }}</h2>
<time>{{ format_date(article.created_at) }}</time>
</article>
{% end %}XSRF Protection
Tornado has built-in XSRF (Cross-Site Request Forgery) protection. Enable it in settings:
settings = {"xsrf_cookies": True}Once enabled, all POST, PUT, and DELETE requests must carry a valid XSRF token, otherwise a 403 is returned.
- HTML forms: Call
{% raw xsrf_form_html() %}inside the form to automatically insert the hidden field. - AJAX requests: Read the token from the
_xsrfcookie and pass it in the request header asX-XSRFToken.
// jQuery example
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
$.ajax({
url: "/api/submit",
method: "POST",
headers: {"X-XSRFToken": getCookie("_xsrf")},
data: JSON.stringify(payload),
});pip install jinja2, then wire it in via a custom render() method).