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

pyramid_jinja2 2.10.1

.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

pyramid_chameleon 0.3

.pt (ZPT), .txt (text)

2014-07-01

XML-based Zope Page Templates (TAL and METAL) plus ${} expressions. The binding has not been released since 2014 but still works with Pyramid 2.1 (the examples below run); the Chameleon engine itself (4.6.0, 2024-12-31) is maintained.

Mako

pyramid_mako 1.1.0

.mak, .mako

2019-08-18

Python-flavoured templates with ${} expressions, <% %> blocks and defs; the Mako engine itself is actively released (1.4.3, 2026-09-22).

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

request, req

The current request (the request keyword argument when rendering through the API, the view’s request otherwise).

context

The context resource of the request; None if request was not passed to render or render_to_response.

get_csrf_token()

A function returning the current CSRF token (see CSRF Tokens in Templates).

renderer_name

The renderer name used, for example shelvesui:templates/shelf.jinja2.

renderer_info

An object with the attributes name, package and type.

view

The view callable, or the instance of the class for a class-based view; None when render or render_to_response are used.

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.

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.

flowchart LR V["View returns a dict"] --> L{"Extension of the renderer name"} L -->|".jinja2"| J["pyramid_jinja2"] L -->|".pt and .txt"| C["pyramid_chameleon"] L -->|".mak and .mako"| M["pyramid_mako"] J --> G["Dict + system values + BeforeRender globals"] C --> G M --> G G --> R["Engine renders the template"] R --> P["Response, 200 text/html"]
Figure 1. How a returned dictionary becomes HTML

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

jinja2.directories

The template search path: a list of directories, or a newline-delimited string, each optionally an asset specification such as shelvesui:templates. Prefer the explicit asset specification form.

jinja2.autoescape

true by default in pyramid_jinja2 (Jinja2 itself defaults to false).

pyramid.reload_templates

true turns on Jinja2’s auto_reload so edited templates are picked up without restart; development only.

jinja2.undefined

strict makes an undefined variable an error (StrictUndefined), debug prints debug information (DebugUndefined), anything else keeps Jinja2’s silent Undefined.

jinja2.filters, jinja2.tests, jinja2.globals

Names mapped to callables or dotted names, one name = dotted.name per line.

jinja2.extensions

Jinja2 extensions (dotted names); jinja2.ext.i18n, or the extension named by jinja2.i18n_extension, is always added.

jinja2.i18n.domain, jinja2.i18n.gettext, jinja2.newstyle

The gettext domain (default: the name of the including package, otherwise messages), a GetTextWrapper subclass and new-style gettext calls.

jinja2.input_encoding

Template encoding, utf-8 by default.

jinja2.bytecode_caching, jinja2.bytecode_caching_directory

A file-system bytecode cache and its directory; off by default.

jinja2.finalize

A callable applied to every output expression.

jinja2.block_start_string, block_end_string, variable_start_string, variable_end_string, comment_start_string, comment_end_string, line_statement_prefix, line_comment_prefix, trim_blocks, newline_sequence, optimized, cache_size

The matching jinja2.Environment arguments, all prefixed with jinja2..

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.

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

route_url_filter, route_path_filter

request.route_url and request.route_path

static_url_filter, static_path_filter

request.static_url and request.static_path

resource_url_filter, model_url_filter (deprecated)

request.resource_url, for traversal

model_path_filter

request.resource_path

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.

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.

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.

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 autoescape on, which Jinja2 alone does not do

The safe filter, or a markupsafe.Markup value

Chameleon ZPT (.pt)

${...} is escaped

The structure: prefix

Chameleon text (.txt)

Not escaped; it is a text format

Not applicable

Mako (pyramid_mako)

Escaped by mako.default_filters = ['h']

The n filter; or mako.default_filters = None

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.

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 = strict or mako.strict_undefined = true in development and tests.

  • Passing request to render. Without the request keyword argument, request, context and renderer globals are missing from the template.

  • Raw output. safe, structure:, n and unescaped .txt templates are XSS holes when fed user data.

  • Relative search paths. jinja2.directories = templates is resolved relative to the first including package; use package:templates.

  • CSRF token without a session. get_csrf_token() needs a session factory or a CSRF storage policy.

  • Reloading in production. Leave pyramid.reload_templates off in production.ini, and restart after deploying new templates.

  • Text templates are not HTML. Chameleon .txt output is not escaped and keeps the text/html content type unless you change it.

  • Forgetting the template files in the distribution. Include *.jinja2, *.pt and *.mako in MANIFEST.in (or the package-data settings of pyproject.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 pcreate scaffolds mentioned in older pyramid_jinja2 pages (pcreate -s pyramid_jinja2_starter) are gone; use the cookiecutter, covered in the cookiecutter page of this section. The setup.py extract_messages flow in the pyramid_jinja2 internationalisation section predates pyproject.toml projects; the internationalization page of this section shows the current commands.

  • The unprefixed reload_templates setting of pyramid_jinja2 was deprecated in 1.5; use pyramid.reload_templates.

  • model_url and model_path filters are deprecated names for the resource_url and resource_path filters.

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