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 Pyramid 2.1 (released 2026-03-11), pyramid_jinja2 2.10.1, pyramid_chameleon 0.3 and pyramid_mako 1.1.0, checked on 2026-10-07. Pyramid 2.1 supports Python 3.10 to 3.14; the examples were executed on Python 3.13 with Jinja2 3.1.6, Chameleon 4.6.0, Mako 1.4.3 and WebTest 3.0.7. The running scenario is Bookshelf Shelves: public shelves owned by users, each holding books. This page turns the dictionaries returned by views into HTML pages.
Pyramid core ships no template engine. It defines what a renderer is, a callable that turns the value returned by a
view into the body of a response, and it ships three renderers for data (string, json and jsonp, described in
the renderers page of this section). Everything that produces HTML is an add-on binding
to a third-party engine: pyramid_jinja2 for Jinja2, pyramid_chameleon for Chameleon and pyramid_mako for Mako. A
binding registers a renderer for a file extension, so that a view configured with renderer="shelf.jinja2" is
rendered by Jinja2 and one configured with renderer="shelf.pt" by Chameleon. The same mechanism exists in the
sibling frameworks of this site with a different shape: Flask bundles Jinja2 directly
(Jinja Templates), Tornado has its own engine
(Templates & UI Modules), FastAPI offers Jinja2 through
Starlette (Static Files, Templates & Frontend) and
Django has its own template language (Templates). Pyramid is the only one of them in
which the engine is a choice you make per project and, if you want, per file extension.
The Three Bindings
| Language | Binding package | File extensions | Latest release | Notes |
|---|---|---|---|---|
Jinja2 |
|
|
2024-02-08 |
Autoescaping on by default, template inheritance, the default choice of the cookiecutter starter; requires Python 3.7 or newer, so it works on every Python that Pyramid 2.1 supports. |
Chameleon |
|
|
2014-07-01 |
XML-based Zope Page Templates (TAL and METAL) plus |
Mako |
|
|
2019-08-18 |
Python-flavoured templates with |
Which one to pick for a new project is rarely a technical question. Jinja2 is the engine that most Python web developers already know, it is the default of the official cookiecutter (see Cookiecutter Projects & pserve), and its binding is the only one with recent releases. Chameleon is the natural choice for teams coming from Zope or Plone and for pages that must remain well-formed XML. Mako is a good fit for developers who want Python expressions in templates. This page spends most of its length on Jinja2 for that reason, and then shows what differs for the other two.
Rendering Through the Renderer Machinery
There are two ways to render a template in Pyramid. The recommended one is declarative: name the template as the
renderer of the view configuration and return a plain dictionary. The other one calls the rendering API from inside
the view and returns a Response.
The dictionary returned by a renderer-configured view becomes the set of top-level names of the template, and Pyramid adds a number of system values to it. The official Templates chapter lists them:
| Name | Meaning |
|---|---|
|
The current request (the |
|
The context resource of the request; |
|
A function returning the current CSRF token (see CSRF Tokens in Templates). |
|
The renderer name used, for example |
|
An object with the attributes |
|
The view callable, or the instance of the class for a class-based view; |
The first block prepares a small package named shelvesui in a temporary directory, with a layout template, a shelf
template and a style sheet, so that the later examples are self-contained and can be run as they are. In a real
project the same files live in the templates/ and static/ directories of your package (the cookiecutter generates
this layout) and you simply create them in your editor.
import importlib
import pathlib
import sys
import tempfile
project = pathlib.Path(tempfile.mkdtemp())
sys.path.insert(0, str(project))
def write_files(files):
for name, text in files.items():
path = project / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
importlib.invalidate_caches()
write_files({
"shelvesui/__init__.py": "",
"shelvesui/static/site.css": "body { font-family: sans-serif; }\n",
"shelvesui/templates/layout.jinja2": """\
<!doctype html>
<html lang="{{ request.locale_name }}">
<head>
<meta charset="utf-8">
<title>{% block title %}Bookshelf Shelves{% endblock %}</title>
<link rel="stylesheet" href="{{ 'shelvesui:static/site.css'|static_url }}">
</head>
<body>
<nav><a href="{{ 'home'|route_url }}">All shelves</a></nav>
{% block content %}{% endblock %}
</body>
</html>
""",
"shelvesui/templates/shelf.jinja2": """\
{% extends "layout.jinja2" %}
{% block title %}{{ shelf.title }} | {{ site_name }}{% endblock %}
{% block content %}
<h1>{{ shelf.title }}</h1>
<ul>
{% for book in books %} <li>{{ book }}</li>
{% endfor %}</ul>
{% endblock %}
""",
})
print(sorted(str(p.relative_to(project)) for p in project.rglob("*") if p.is_file()))
Source: Templates
The listing prints the three files created below shelvesui/. The next block is the first complete application. It
includes pyramid_jinja2, points the Jinja2 search path at the package, installs the two URL filters, registers a
static view and a route, and uses the asset specification shelvesui:templates/shelf.jinja2 as the renderer.
from pyramid.config import Configurator
from webtest import TestApp
def show_shelf(request):
return {"shelf": {"title": "Ada <Lovelace> & friends"}, "books": ["Dune", "A & B"]}
settings = {
"jinja2.directories": "shelvesui:templates",
"jinja2.filters": (
"route_url = pyramid_jinja2.filters:route_url_filter\n"
"static_url = pyramid_jinja2.filters:static_url_filter"
),
}
with Configurator(settings=settings) as config:
config.include("pyramid_jinja2")
config.add_route("home", "/")
config.add_route("shelf", "/shelves/{name}")
config.add_static_view("static", "shelvesui:static")
config.add_view(
show_shelf,
route_name="shelf",
renderer="shelvesui:templates/shelf.jinja2",
)
app = config.make_wsgi_app()
client = TestApp(app)
response = client.get("/shelves/ada")
print(response.status, response.content_type)
print(response.html.title.string)
The status is 200 OK and the title of the page is +Ada <Lovelace> & friends | +: the view’s text was escaped
on output and parsed back by the HTML parser, and the template variable site_name that nobody provided rendered as
an empty string, without any error. That silence is a pitfall explained in the settings section below, and the
variable is provided properly with a renderer global in Renderer Globals with BeforeRender.
Source: Templates
To render a template from inside a view instead, use pyramid.renderers.render (returns a string) or
pyramid.renderers.render_to_response (returns a Response). Always pass request as a keyword argument: without
it, request and context are not available to the template and renderer globals are not applied, which is the most
common cause of "my template works as a renderer but not through `render`".
from pyramid.renderers import render, render_to_response
from pyramid.response import Response
write_files({
"shelvesui/templates/card.jinja2": "<p class=\"card\">{{ title }} by {{ author }}</p>\n",
})
def card_as_string(request):
body = render(
"shelvesui:templates/card.jinja2",
{"title": "Dune", "author": "Frank Herbert"},
request=request,
)
return Response(body, content_type="text/plain")
def card_as_response(request):
response = render_to_response(
"shelvesui:templates/card.jinja2",
{"title": "Emma", "author": "Jane Austen"},
request=request,
)
response.status_int = 201
return response
with Configurator(settings=settings) as config:
config.include("pyramid_jinja2")
config.add_route("card1", "/card1")
config.add_route("card2", "/card2")
config.add_view(card_as_string, route_name="card1")
config.add_view(card_as_response, route_name="card2")
direct = TestApp(config.make_wsgi_app())
first = direct.get("/card1")
second = direct.get("/card2")
print(first.status, first.content_type, first.text.strip())
print(second.status, second.content_type, second.text.strip())
The first response is 200 OK, text/plain, because a response was built by hand; the second is 201 Created with
the default text/html, because render_to_response creates a response with the default content type of the
renderer. To change status, headers or content type of a view that uses renderer=, set them on request.response
before returning the dictionary; the request and response page of this section covers that object.
Source: Using Templates Directly
The reason to prefer the renderer configuration is testability: a view that returns a dictionary can be unit-tested by comparing dictionaries, without parsing HTML, while a view that returns a response needs its body to be inspected. The official documentation makes the same argument. Calling the engine’s own API from a view, as in the raw Mako example of the Templates chapter, is possible but loses three things: automatic reloading, asset overriding and the ability to use the template as a renderer.
Jinja2 with pyramid_jinja2
Install the binding with pip install pyramid_jinja2 (Jinja2 comes with it) and activate it in one of two
equivalent ways: call config.include("pyramid_jinja2"), or list pyramid_jinja2 in the pyramid.includes setting
of the .ini file (see
Settings, Environment & .ini Files). Activation does
three things: it registers a renderer for the .jinja2 extension together with a jinja2.Environment, it adds the
configurator directives add_jinja2_renderer, add_jinja2_search_path, add_jinja2_extension and
get_jinja2_environment, and it adds the package that called include to the template search path.
The binding does not register the URL filters automatically. Installing route_url, static_url and the others is a
setting, as in the previous example. Everything else in the Jinja2 language, such as inheritance, macros, filters,
tests and expressions, is documented by Jinja2 itself and is not repeated here.
Settings
Settings may be given in the .ini file or in the settings dictionary passed to the Configurator. Boolean values
are true or false. Those marked as dictionaries or lists accept either Python objects or newline-delimited strings
in the .ini file.
| Setting | Meaning |
|---|---|
|
The template search path: a list of directories, or a newline-delimited string, each optionally an asset
specification such as |
|
|
|
|
|
|
|
Names mapped to callables or dotted names, one |
|
Jinja2 extensions (dotted names); |
|
The gettext domain (default: the name of the including package, otherwise |
|
Template encoding, |
|
A file-system bytecode cache and its directory; off by default. |
|
A callable applied to every output expression. |
|
The matching |
Two details contradict older tutorials. The reload_templates setting without a prefix was deprecated in
pyramid_jinja2 1.5; use pyramid.reload_templates. And jinja2.auto_reload is not a setting to touch: reloading is
driven by pyramid.reload_templates.
Turn on jinja2.undefined = strict in development and in tests. The silent default is the source of the most
frequent Jinja2 bug in Pyramid applications: a misspelled variable, or a key forgotten in the returned dictionary,
renders as an empty string and the page looks almost right. The next example shows the same template with and without
the setting.
write_files({"shelvesui/templates/greeting.jinja2": "Hello, {{ nmae }}!\n"})
def greeting(request):
return {"name": "Ada"}
def run(extra):
cfg = {"jinja2.directories": "shelvesui:templates", **extra}
with Configurator(settings=cfg) as config:
config.include("pyramid_jinja2")
config.add_route("greeting", "/hi")
config.add_view(greeting, route_name="greeting", renderer="greeting.jinja2")
app = config.make_wsgi_app()
try:
return TestApp(app).get("/hi")
except Exception as error: # an unhandled view error propagates through WebTest
return error
lenient = run({})
strict = run({"jinja2.undefined": "strict"})
print(lenient.status, repr(lenient.text))
print(type(strict).__name__, strict)
The lenient application answers 200 OK with 'Hello, !\n'; the strict one raises UndefinedError: 'nmae' is undefined
out of the application (WebTest lets it propagate; a WSGI server would answer 500 Internal Server Error), and the
message names the misspelled variable.
Source: pyramid_jinja2 settings
Finding Templates
pyramid_jinja2 supports three ways of naming a template, and they can be mixed.
An asset specification such as shelvesui:templates/shelf.jinja2 always works and is overridable (next section).
It is the most robust form: it keeps working if the view module moves.
A caller-relative name such as templates/shelf.jinja2 is resolved relative to the Python package of the module
that contains the view configuration, and is converted into an asset specification internally. It is what the
cookiecutter uses and it avoids name collisions between add-ons that both ship a layout.jinja2. It needs the view
to be defined in a module of the package, so it cannot be demonstrated with a view defined in a script.
A search-path name such as shelf.jinja2 is looked up in jinja2.directories when it is not found next to the
calling module. It is the classic Jinja2 behaviour, and the one used by {% extends "layout.jinja2" %} in the
examples on this page. Use an explicit asset specification in jinja2.directories: a relative directory such as
jinja2.directories = templates is resolved relative to the first package that included pyramid_jinja2, which can
surprise you in a project with several packages.
The following example creates a package with a view module and a template below it, and renders it with a caller-relative name; the template is not on any search path.
write_files({
"adminui/__init__.py": "",
"adminui/templates/dashboard.jinja2": "<h1>{{ count }} shelves</h1>\n",
"adminui/views.py": """\
from pyramid.view import view_config
@view_config(route_name="dashboard", renderer="templates/dashboard.jinja2")
def dashboard(request):
return {"count": 3}
""",
})
import adminui # the package written above
with Configurator() as config:
config.include("pyramid_jinja2")
config.add_route("dashboard", "/admin")
config.scan("adminui")
admin = TestApp(config.make_wsgi_app())
print(admin.get("/admin").text.strip())
The output is <h1>3 shelves</h1>. Pyramid resolved templates/dashboard.jinja2 against the adminui.views
module, that is against the full asset specification adminui.views:templates/dashboard.jinja2.
When a template extends or includes another one, the parent is looked up first relative to the child, then on the
search path. Both {% extends "layout.jinja2" %} and an asset specification in the extends tag work.
Filters for URL Generation
Templates need URLs: other routes, static files, resources. pyramid_jinja2 ships filters that call the Pyramid URL functions with the current request taken from the template context.
Filter (module pyramid_jinja2.filters) |
Calls |
|---|---|
|
|
|
|
|
|
|
|
They must be registered through jinja2.filters, which is why the first example had that setting. A filter takes the
route name or the path as the value being filtered and extra arguments as keyword arguments: {{ 'shelf'|route_url(name='ada') }}
produces the absolute URL of the route shelf. The filters are optional: because request is always in the template
context, {{ request.route_url('shelf', name='ada') }} and {{ request.static_url('shelvesui:static/site.css') }}
do the same, and many projects prefer them for being explicit. Routes and static views are explained in the URL
dispatch and static assets pages of this section.
Another Extension, Another Search Path, Another Environment
The .jinja2 renderer is only the default. config.add_jinja2_renderer(".html") registers another extension with its
own jinja2.Environment, so that shelf.html renders through Jinja2 as well. Its settings_prefix argument points the
renderer at another group of settings, which makes it possible to have, for example, one environment for HTML pages
with autoescaping and another for plain text email bodies without. The other directives take a name argument to
choose the renderer: add_jinja2_search_path(path, name=".html") and add_jinja2_extension(ext, name=".html").
config.get_jinja2_environment() returns the jinja2.Environment of a renderer, but only after the configuration has
been committed, because the environment is created in an action. This is how to register one more filter from code
instead of from the .ini file:
def initials(value):
return "".join(word[0] for word in value.split()).upper()
write_files({
"shelvesui/templates/owner.html": "<p>{{ owner|initials }}</p>\n",
"mailui/__init__.py": "",
"mailui/templates/receipt.email": "Dear {{ name }}, your <shelf> is ready\n",
})
with Configurator(settings={
"jinja2.directories": "shelvesui:templates",
"mail.jinja2.directories": "mailui:templates",
"mail.jinja2.autoescape": "false",
}) as config:
config.include("pyramid_jinja2")
config.add_jinja2_renderer(".html")
config.add_jinja2_renderer(".email", settings_prefix="mail.jinja2.")
config.commit()
config.get_jinja2_environment(".html").filters["initials"] = initials
config.add_route("owner", "/owner")
config.add_route("receipt", "/receipt")
config.add_view(lambda request: {"owner": "ada lovelace"}, route_name="owner", renderer="owner.html")
config.add_view(lambda request: {"name": "Ada"}, route_name="receipt", renderer="receipt.email")
app = config.make_wsgi_app()
site = TestApp(app)
print(site.get("/owner").text.strip())
print(site.get("/receipt").text.strip())
The first line printed is <p>AL</p>. The second shows the email renderer, whose environment has autoescaping turned
off, so <shelf> is written verbatim in the plain-text mail body. This is the legitimate use of turning autoescape
off; never do it for a renderer whose output is HTML.
Chameleon with pyramid_chameleon
Install pip install pyramid_chameleon and activate it with config.include("pyramid_chameleon") or
pyramid.includes = pyramid_chameleon. The extensions .pt and .txt become renderers: .pt is an XML-based
Zope Page Template (ZPT), .txt a text template. Both understand ${expression} substitutions, and ZPT also the
tal: and metal: attributes (tal:repeat, tal:content, metal:define-macro and others; the language is
documented by Chameleon).
write_files({
"chamui/__init__.py": "",
"chamui/templates/shelf.pt": """\
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal">
<body>
<h1>${title}</h1>
<ul><li tal:repeat="book books">${book}</li></ul>
<div metal:define-macro="card"><p class="card">${title}</p></div>
<p>trusted: ${structure: trusted}</p>
</body>
</html>
""",
"chamui/templates/hello.txt": "Hello, ${name}!\n",
})
def shelf_data(request):
return {"title": "Ada & co", "books": ["Dune", "<i>x</i>"], "trusted": "<b>bold</b>"}
with Configurator() as config:
config.include("pyramid_chameleon")
config.add_route("pt", "/pt")
config.add_route("txt", "/txt")
config.add_route("macro", "/macro")
config.add_view(shelf_data, route_name="pt", renderer="chamui:templates/shelf.pt")
config.add_view(lambda request: {"name": "<world>"}, route_name="txt", renderer="chamui:templates/hello.txt")
config.add_view(shelf_data, route_name="macro", renderer="chamui:templates/shelf#card.pt")
chameleon_app = TestApp(config.make_wsgi_app())
print(chameleon_app.get("/pt").text)
print(chameleon_app.get("/txt").text, chameleon_app.get("/txt").content_type)
print(chameleon_app.get("/macro").text)
The ZPT page escapes ${title} to Ada & co and each book (<i>x</i> for the markup one), while the
structure: prefix inserts trusted as raw markup, the Chameleon equivalent of Jinja2’s safe. The text template
renders Hello, <world>! without escaping, as a response whose content type is still text/html, which is the default of
every renderer that does not choose its own; use .txt templates only for trusted content or set
request.response.content_type yourself. The last request shows a Pyramid feature specific to the Chameleon and Mako
bindings: the renderer name shelf#card.pt renders only the macro named card of shelf.pt.
Macros need one more step than in Jinja2. To use a macro defined in another template, the view obtains the template
object with pyramid.renderers.get_renderer("templates/master.pt").implementation() and passes it to the template,
or, for a master page used by every template, adds it to the renderer globals in a BeforeRender subscriber (next
section). The pyramid_chameleon documentation shows both.
Two settings matter: pyramid.reload_templates (see Reloading Templates) and pyramid.debug_templates (environment variable
PYRAMID_DEBUG_TEMPLATES), which adds the template file name and arguments to the traceback of a failing template; it
slows rendering and is for development only.
Source: pyramid_chameleon
Mako with pyramid_mako
Install pip install pyramid_mako and activate it with config.include("pyramid_mako") or
pyramid.includes = pyramid_mako. It registers the renderers for .mako and .mak. As in Chameleon, ${name}
substitutes a value, and the view must return a dictionary or a Response.
write_files({
"makoui/__init__.py": "",
"makoui/templates/shelf.mako": """\
<h1>${title}</h1>
<p>escaped: ${trusted}</p><p>raw: ${trusted | n}</p>
<p>context class: ${_context.__class__.__name__}</p>
<%def name="card()"><p class="card">${title}</p></%def>
""",
})
def mako_data(request):
return {"title": "Ada & co", "trusted": "<b>bold</b>"}
with Configurator() as config:
config.include("pyramid_mako")
config.add_route("mako", "/mako")
config.add_route("def", "/def")
config.add_view(mako_data, route_name="mako", renderer="makoui:templates/shelf.mako")
config.add_view(mako_data, route_name="def", renderer="makoui:templates/shelf#card.mako")
mako_app = TestApp(config.make_wsgi_app())
print(mako_app.get("/mako").text)
print(mako_app.get("/def").text)
The values are escaped by default (<b>bold</b>), and the n filter written after a pipe turns the default
escaping off for one expression. The system value context is renamed _context in Mako templates because context
is an internal Mako name. shelf#card.mako renders only the def named card.
The settings of pyramid_mako are prefixed with mako.: mako.directories (the search path), mako.module_directory
(where compiled modules are cached), mako.input_encoding, mako.error_handler, mako.default_filters (default
['h']; setting it to None disables escaping everywhere, which is almost never what you want), mako.imports,
mako.future_imports, mako.strict_undefined, mako.preprocessor, mako.preprocessor_wants_settings and the
mako.cache.* and mako.cache_impl family. mako.strict_undefined = true is the Mako counterpart of
jinja2.undefined = strict and is recommended for the same reason. Mako’s own filesystem_checks option is driven by
pyramid.reload_templates; unlike plain Mako, reloading is off unless that setting is true.
config.add_mako_renderer(".html", settings_prefix="mymako.") adds another extension with another group of settings.
Source: pyramid_mako
Asset Specifications and Overriding Templates
A template named by an asset specification is an asset, and every asset can be overridden without editing the
package that ships it. This is what makes a reusable add-on’s templates customisable: the add-on renders
addon:templates/shelf.jinja2, and your application substitutes its own file.
write_files({
"themeui/__init__.py": "",
"themeui/templates/shelf.jinja2": "<h1>Custom theme: {{ title }}</h1>\n",
"addonui/__init__.py": "",
"addonui/templates/shelf.jinja2": "<h1>Default: {{ title }}</h1>\n",
})
def render_default(config):
config.add_route("override", "/override")
config.add_view(
lambda request: {"title": "Ada"},
route_name="override",
renderer="addonui:templates/shelf.jinja2",
)
with Configurator() as config:
config.include("pyramid_jinja2")
render_default(config)
before = TestApp(config.make_wsgi_app()).get("/override").text.strip()
with Configurator() as config:
config.include("pyramid_jinja2")
render_default(config)
config.override_asset(
to_override="addonui:templates/shelf.jinja2",
override_with="themeui:templates/shelf.jinja2",
)
after = TestApp(config.make_wsgi_app()).get("/override").text.strip()
print(before)
print(after)
The first application renders <h1>Default: Ada</h1> and the second <h1>Custom theme: Ada</h1>. Overriding
works for templates named by an asset specification and for caller-relative names, which are converted into
specifications, and not for templates rendered through the engine’s own API. override_asset can also override a whole directory, as in to_override="addonui:templates/". The
Extending an Existing Application chapter of the narrative documentation describes the rules, and the extending page
of this section covers them as a topic of their own.
Source: Overriding Assets
Renderer Globals with BeforeRender
Values that every page needs (the site name, the current user, a navigation menu) should not be copied into every
view’s dictionary. Pyramid sends a pyramid.events.BeforeRender event just before a renderer is invoked, and a
subscriber can add keys to it. The event behaves like a dictionary holding the renderer globals, which include the
system values, and its rendering_val attribute exposes the dictionary the view returned. This fixes the missing
site_name of the first example.
from pyramid.events import BeforeRender
def add_site_globals(event):
event["site_name"] = "Bookshelf Shelves"
with Configurator(settings={**settings, "jinja2.undefined": "strict"}) as config:
config.include("pyramid_jinja2")
config.add_route("home", "/")
config.add_route("shelf", "/shelves/{name}")
config.add_static_view("static", "shelvesui:static")
config.add_view(show_shelf, route_name="shelf", renderer="shelvesui:templates/shelf.jinja2")
config.add_subscriber(add_site_globals, BeforeRender)
shelf_app = TestApp(config.make_wsgi_app())
page = shelf_app.get("/shelves/ada")
print(page.status)
print(page.text)
The page is rendered now: the title is Ada <Lovelace> & friends | Bookshelf Shelves, the link to the style
sheet is an absolute URL built by static_url, and every <, > and & coming from the view’s data is escaped. A
subscriber must not reuse a key that exists already, because subscribers have no defined order and so nothing decides
which value wins. The narrative documentation says that Pyramid raises a KeyError for such a duplicate; do not rely
on that, because a subscriber that assigned the system name request was accepted without error on 2.1. Give every
global a name that no view and no other subscriber uses. The event is
the right place for read-only helpers and constants; do not put request-specific database queries in it, because it
runs for every rendered template including error pages.
The subscriber may also be declared with @subscriber(BeforeRender) and picked up by config.scan(). The events page
of this section covers subscribers in general.
Source: Using the Before Render Event
Reloading Templates
During development you want an edited template to appear on the next request. Pyramid has one switch for every
binding: the setting pyramid.reload_templates = true, or the environment variable PYRAMID_RELOAD_TEMPLATES=1, for
example PYRAMID_RELOAD_TEMPLATES=1 pserve development.ini. pyramid_jinja2 maps it to Jinja2’s auto_reload and
pyramid_mako to Mako’s filesystem_checks. The cookiecutter’s development.ini sets it to true and its
production.ini sets it to false, because checking the modification time of every template on every render costs
time. The settings page of this section lists the other development switches (for example pyramid.reload_assets for
static files, which is a different setting).
def reload_demo(flag):
cfg = {"jinja2.directories": "shelvesui:templates", "pyramid.reload_templates": flag}
with Configurator(settings=cfg) as config:
config.include("pyramid_jinja2")
config.add_route("reload", "/reload")
config.add_view(lambda request: {"name": "Ada"}, route_name="reload", renderer="reload.jinja2")
client = TestApp(config.make_wsgi_app())
path = project / "shelvesui/templates/reload.jinja2"
path.write_text("version 1 {{ name }}", encoding="utf-8")
first = client.get("/reload").text
import os, time
path.write_text("version 2 {{ name }}", encoding="utf-8")
os.utime(path, (time.time() + 5, time.time() + 5))
return first, client.get("/reload").text
print(reload_demo("true"))
print(reload_demo("false"))
With reloading on, the second request prints version 2 Ada. With it off, a template that was already loaded is
served from the cache and the second request still says version 1 Ada, until the process is restarted. A failed
deployment where "the template change does not show up" is usually this cache, and the fix is a restart rather than
a setting.
Autoescaping and Cross-Site Scripting
Rendering untrusted data into HTML without escaping is cross-site scripting (XSS). Each binding has a default and a way to opt out, and the opt-out is where the vulnerabilities come from.
| Engine | Default | Raw output (dangerous) |
|---|---|---|
Jinja2 (pyramid_jinja2) |
Escaped; pyramid_jinja2 forces |
The |
Chameleon ZPT ( |
|
The |
Chameleon text ( |
Not escaped; it is a text format |
Not applicable |
Mako (pyramid_mako) |
Escaped by |
The |
The rules that follow from the table are short. Never mark user-supplied text as safe. If a page must show
user-written rich text, sanitise it with a library such as nh3 or bleach on input or output, and mark only the
sanitised result. Escaping is context-sensitive: an escaped value is safe inside element content and quoted attribute
values but not inside <script> blocks, inline event handlers or href values that begin with javascript:. To pass
data to JavaScript, serialise it with Jinja2’s tojson filter into a <script type="application/json"> element or a
data- attribute, and validate URLs that users can enter. A Content Security Policy header is the second line of
defence and is set in a tween or a response callback (see the events page of this section).
from markupsafe import Markup
write_files({
"shelvesui/templates/note.jinja2": (
"<p id=\"a\">{{ note }}</p><p id=\"b\">{{ note|safe }}</p><p id=\"c\">{{ trusted }}</p>"
"<script type=\"application/json\">{{ data|tojson }}</script>\n"
),
})
attack = "<script>alert(1)</script>"
with Configurator(settings={"jinja2.directories": "shelvesui:templates"}) as config:
config.include("pyramid_jinja2")
config.add_route("note", "/note")
config.add_view(
lambda request: {"note": attack, "trusted": Markup("<em>ok</em>"), "data": {"q": "</script>"}},
route_name="note",
renderer="note.jinja2",
)
note_app = TestApp(config.make_wsgi_app())
print(note_app.get("/note").text)
Paragraph a contains <script>alert(1)</script> (safe), paragraph b contains the live script (the safe
filter disabled the protection), paragraph c renders <em>ok</em> because the application deliberately constructed a
Markup value, and the JSON element contains \u003c/script\u003e instead of a literal closing tag, as produced by tojson, so that
a value cannot close the element early. Review every use of safe, structure:, n and Markup in code review as
you would review eval.
CSRF Tokens in Templates
Every template rendered through a Pyramid renderer has a function get_csrf_token() in its globals. It returns the
current CSRF token as a string, to be placed in a hidden form field or an X-CSRF-Token header. The CSRF page of this
section covers token storage and verification; the template side is one line:
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">.
import secrets
from pyramid.session import SignedCookieSessionFactory
write_files({
"shelvesui/templates/add-book.jinja2": """\
<form method="post" action="/shelves/ada/books">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input name="title"><button>Add</button>
</form>
""",
})
with Configurator(settings={"jinja2.directories": "shelvesui:templates"}) as config:
config.include("pyramid_jinja2")
config.set_session_factory(SignedCookieSessionFactory(secrets.token_hex(32)))
config.add_route("add_book", "/books/new")
config.add_view(lambda request: {}, route_name="add_book", renderer="add-book.jinja2")
form_app = TestApp(config.make_wsgi_app())
form_page = form_app.get("/books/new")
token = form_page.html.find("input", {"name": "csrf_token"})["value"]
print(len(token) >= 32, form_page.forms[0].method)
The check prints True post. The default token storage is the session, so a session factory (or another storage policy
set with set_csrf_storage_policy) must be configured; without one get_csrf_token() raises an AttributeError
saying that no session factory is registered, and the template fails with it.
Testing Templates
A renderer-configured view returns a dictionary, so its unit test needs no template at all. To test a template, render
it with pyramid.renderers.render inside a pyramid.testing.setUp() configuration that has the binding included. For
a functional test, use WebTest as in the examples above: response.html is a BeautifulSoup tree and response.forms
holds the forms of the page, and the application already runs with jinja2.undefined = strict.
from pyramid import testing
write_files({"shelvesui/templates/book-row.jinja2": "<li>{{ title }}{% if read %} (read){% endif %}</li>"})
def test_book_row():
config = testing.setUp(settings={"jinja2.directories": "shelvesui:templates", "jinja2.undefined": "strict"})
try:
config.include("pyramid_jinja2")
request = testing.DummyRequest()
html = render("book-row.jinja2", {"title": "Dune", "read": True}, request=request)
assert html == "<li>Dune (read)</li>", html
finally:
testing.tearDown()
test_book_row()
print("template test passed")
Forgetting config.include("pyramid_jinja2") in a unit test raises ValueError: No such renderer factory .jinja2,
because the binding’s renderer is only registered when it is included. Testing is covered fully in the testing page of this
section.
Other Template Languages and Custom Renderers
Any engine can be used as a renderer by writing a renderer factory and registering it with
config.add_renderer(".ext", factory); the official Renderers chapter shows the contract (a callable receiving the
value returned by the view and the system values, returning a string). Using an engine only from inside views without
a binding works too, but, as noted above, loses reloading and overriding. Because Pyramid is synchronous, a template is
rendered inside the view’s worker thread and a slow template blocks that thread; there is no streaming or async
rendering as in the Flask (Jinja Templates) and Tornado (Templates & UI Modules) sections of this site.
Source: Adding and Changing Renderers
Common Pitfalls
-
Forgetting to include the binding. The error is about an unknown renderer or extension. Include the binding in
main()and in unit tests. -
Silent undefined variables. Set
jinja2.undefined = strictormako.strict_undefined = truein development and tests. -
Passing
requesttorender. Without therequestkeyword argument,request,contextand renderer globals are missing from the template. -
Raw output.
safe,structure:,nand unescaped.txttemplates are XSS holes when fed user data. -
Relative search paths.
jinja2.directories = templatesis resolved relative to the first including package; usepackage:templates. -
CSRF token without a session.
get_csrf_token()needs a session factory or a CSRF storage policy. -
Reloading in production. Leave
pyramid.reload_templatesoff inproduction.ini, and restart after deploying new templates. -
Text templates are not HTML. Chameleon
.txtoutput is not escaped and keeps thetext/htmlcontent type unless you change it. -
Forgetting the template files in the distribution. Include
*.jinja2,*.ptand*.makoinMANIFEST.in(or the package-data settings ofpyproject.toml) so they are installed with the package. -
Templates in a worker. A template that queries the database lazily (for example by touching a relationship inside a loop) runs those queries during rendering; load the data in the view, where the transaction manager is still active (see the SQLAlchemy page of this section).
Recent Changes and Outdated Material
-
The three bindings changed little during the Pyramid 2.0 and 2.1 cycles; pyramid_jinja2 2.10.1 (2024) is the last release, pyramid_mako 1.1.0 dates from 2019 and pyramid_chameleon 0.3 from 2014. They continue to work with Pyramid 2.1 (the examples ran), but check the maintenance status before adopting an engine.
-
The
pcreatescaffolds mentioned in older pyramid_jinja2 pages (pcreate -s pyramid_jinja2_starter) are gone; use the cookiecutter, covered in the cookiecutter page of this section. Thesetup.py extract_messagesflow in the pyramid_jinja2 internationalisation section predatespyproject.tomlprojects; the internationalization page of this section shows the current commands. -
The unprefixed
reload_templatessetting of pyramid_jinja2 was deprecated in 1.5; usepyramid.reload_templates. -
model_urlandmodel_pathfilters are deprecated names for theresource_urlandresource_pathfilters. -
pyramid_jinja2 2.10.1 still declares Python 3.7 as its minimum version; Pyramid 2.1 itself needs Python 3.10.
Summary
Pyramid separates rendering from view logic: views return dictionaries and a renderer named in the view
configuration turns them into HTML. The renderer comes from a binding package that matches the file extension, and
pyramid_jinja2 is the recommended one. Name templates by asset specification so they can be overridden, enable
strict undefined variables in development and in tests, add shared values with BeforeRender, keep autoescaping on,
and leave template reloading off in production.