Skip to content

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.html

Rendering 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:

FunctionDescription
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 _xsrf cookie and pass it in the request header as X-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),
});
Tornado’s template engine is fully featured but relatively lightweight. If your frontend is complex, consider a frontend/backend separation architecture (where Tornado only serves APIs) or integrate Jinja2 (pip install jinja2, then wire it in via a custom render() method).
Last updated on