Views & View Configuration

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), checked on 2026-10-07. The code was executed on Python 3.13 with Pyramid 2.1, WebTest 3.0.7 and Waitress 3.0.2; every example that is not marked as a file listing runs as shown. Views are where an application does its work, and view configuration is how Pyramid decides which view handles a request. The two are deliberately separate ideas: a view callable is plain Python that takes a request and produces a response, while the configuration attached to it is a set of statements ("only for POST`", "only for the named route", "only for an authenticated user", "render the result as JSON") that the router compares with every request. This page follows the Bookshelf Shelves scenario of this section and covers the view callable forms, the two registration styles, every predicate, the non-predicate arguments, the way view lookup orders and falls through candidates, content negotiation with the `Accept header, and the tools for debugging a view that is never called.

The pages around this one: Getting Started showed the first two views, Configuration & the Configurator explained config.scan() and conflict detection, and the next pages of this section take the pieces further: URL dispatch decides which route matches, traversal and resources decide the context, exception views turn exceptions into responses, and renderers turn return values into bodies. Those pages are referred to in prose here because they are written after this one. For the same job in other frameworks, see Flask Class-Based Views, Tornado Applications & Request Handlers and Getting Started with FastAPI.

View Callables

A view callable is any callable that Pyramid can invoke with a request: a function, a class, or an instance with a __call__ method. The only hard requirement is that the callable accepts the request, and its ultimate job is to produce a response. Three shapes cover almost every application.

Shape How Pyramid calls it

Function def view(request)

Called with the request. This is the shape of most views.

Function def view(context, request)

Called with the context resource and the request. Pyramid looks at the arguments the function declares and passes both when it declares two. The context is whatever traversal found; with URL dispatch alone it is the default root object (DefaultRootFactory).

Class with __init__(self, request) or __init__(self, context, request)

Pyramid instantiates the class with the request (or the context and request), then calls the method named by the attr argument of the view configuration with no arguments. Without attr it calls __call__. A new instance is created per request, so the instance can safely keep the request and values derived from it as attributes.

Instance of a class with __call__(self, request)

Called like a function. One instance is shared by every request, so it must not keep per-request state.

Source: Pyramid: Views

The first example registers each of the shapes with add_view and calls them through WebTest, the same test client that the rest of the section uses. Nothing here needs a running server.

from pyramid.config import Configurator
from pyramid.response import Response
from webtest import TestApp

SHELVES = {"ada": ["Dune", "Hyperion"], "grace": ["The Dispossessed"]}


def home(request):
    return Response("Bookshelf Shelves")


class ShelfView:
    def __init__(self, request):
        self.request = request

    def __call__(self):
        owner = self.request.matchdict["shelf"]
        return Response(", ".join(SHELVES[owner]))


class Greeter:
    """An instance is a view callable too; it is shared by all requests."""

    def __init__(self, greeting):
        self.greeting = greeting

    def __call__(self, request):
        return Response(self.greeting)


def context_and_request(context, request):
    return Response(f"context is {type(context).__name__}")


with Configurator() as config:
    config.add_route("home", "/")
    config.add_route("shelf", "/shelves/{shelf}")
    config.add_route("welcome", "/welcome")
    config.add_route("context", "/context")
    config.add_view(home, route_name="home")
    config.add_view(ShelfView, route_name="shelf")
    config.add_view(Greeter("Welcome, reader"), route_name="welcome")
    config.add_view(context_and_request, route_name="context")
    app = TestApp(config.make_wsgi_app())

assert app.get("/").text == "Bookshelf Shelves"
assert app.get("/shelves/ada").text == "Dune, Hyperion"
assert app.get("/welcome").text == "Welcome, reader"
assert app.get("/context").text == "context is DefaultRootFactory"

request.matchdict holds the values captured by the route pattern, such as shelf in /shelves/{shelf}; the Getting Started page shows it in a first example.

Pyramid is a WSGI framework and its views are synchronous: there is no async def view in 2.1. Blocking calls are simply made in the view, and concurrency comes from the WSGI server’s threads or processes.

What a View Returns

A view should return an object that implements Pyramid’s response interface: pyramid.response.Response or any of its subclasses. The HTTP exception classes in pyramid.httpexceptions are responses too, so a view may return or raise HTTPFound, HTTPNotFound, HTTPForbidden and the others, and either way the instance becomes the response. exception_response(status_code) builds the right class from a numeric status. When the view configuration names a renderer, the view returns ordinary data (a dict, a list, a string) and the renderer builds the response; if the view returns a real response object the renderer is skipped.

from pyramid.httpexceptions import HTTPFound, HTTPNotFound


def old_shelf_url(request):
    # returned: an HTTP exception is a valid response
    return HTTPFound(location=request.route_url("shelf", shelf="ada"))


def missing_shelf(request):
    # raised: the router turns it into the 404 response
    raise HTTPNotFound("No such shelf")


def plain_dict(request):
    return {"shelves": sorted(SHELVES)}


with Configurator() as config:
    config.add_route("shelf", "/shelves/{shelf}")
    config.add_route("old", "/old")
    config.add_route("missing", "/missing")
    config.add_route("plain", "/plain")
    config.add_view(ShelfView, route_name="shelf")
    config.add_view(old_shelf_url, route_name="old")
    config.add_view(missing_shelf, route_name="missing")
    config.add_view(plain_dict, route_name="plain")  # no renderer: a dict is not a response
    app = TestApp(config.make_wsgi_app())

redirect = app.get("/old")
assert redirect.status_int == 302
assert redirect.headers["Location"].endswith("/shelves/ada")
assert app.get("/missing", status=404).status_int == 404

try:
    app.get("/plain")
except ValueError as exc:
    print(str(exc).splitlines()[0])

The last request in the example is the most common beginner error. A view that returns a dict without a renderer produces a ValueError ("Could not convert return value of the view callable …​ into a response object") and, in a real deployment, a 500 response. The fix is a renderer="json" argument on the view configuration, covered by the renderers page of this section; returning a Response explicitly is always valid. Exception views, which let you replace the page shown for HTTPNotFound or any custom exception, are the subject of the errors page.

Registering a View

A view callable does nothing until a view configuration associates it with a set of circumstances. Pyramid offers two ways to write that association, and they take the same arguments:

  • config.add_view(view, **arguments) is imperative: you call it while building the application. Nothing needs to be scanned, and the registration can use data computed at startup.

  • @view_config(**arguments) is declarative: the decorator only records the arguments on the callable, and config.scan() later performs the same add_view call. A decorator that is never scanned registers nothing and the application answers 404 for that URL.

Every argument of @view_config has the same meaning as the matching argument of add_view, except view itself. The official documentation notes that scanning makes application startup slightly slower than the imperative style, which only matters for very large applications.

The next listings are the files of a small package, shelfapp, that the rest of the page scans. The decorator can be placed on a function, on a class (then __call__ or the attr method is the view) and on individual methods of a class. When it decorates a method, the view is registered for the class, so the constructor takes request or context, request, and the decorated method must return the response; this is equivalent to putting attr="method_name" on a class-level decorator. Stacking several @view_config decorators on the same callable creates one registration for each.

# norun
# file: shelfapp/views.py
from pyramid.httpexceptions import HTTPNotFound
from pyramid.view import view_config, view_defaults

SHELVES = {"ada": ["Dune", "Hyperion"], "grace": ["The Dispossessed"]}


@view_config(route_name="home", renderer="json")
def home(request):
    return {"app": "Bookshelf Shelves", "shelves": sorted(SHELVES)}


@view_defaults(route_name="api_shelf", renderer="json")
class ShelfAPI:
    def __init__(self, request):
        self.request = request
        self.owner = request.matchdict["shelf"]
        if self.owner not in SHELVES:
            raise HTTPNotFound(f"No shelf named {self.owner}")

    @view_config(request_method="GET")
    def read(self):
        return {"shelf": self.owner, "books": SHELVES[self.owner]}

    @view_config(request_method="POST", request_param="title")
    def add_book(self):
        SHELVES[self.owner].append(self.request.params["title"])
        self.request.response.status = 201
        return {"shelf": self.owner, "books": SHELVES[self.owner]}

    @view_config(request_method="DELETE")
    def clear(self):
        SHELVES[self.owner].clear()
        return {"shelf": self.owner, "books": []}


@view_config(route_name="api_book", request_method="GET", renderer="json")
def book_detail(request):
    owner = request.matchdict["shelf"]
    index = int(request.matchdict["book_id"])
    return {"shelf": owner, "title": SHELVES[owner][index]}

@view_defaults is a class decorator that supplies default arguments to every @view_config on the methods of that class, so route_name and renderer are written once. It accepts the same arguments as @view_config; an argument passed to @view_config overrides the default, ordinary Python inheritance applies (a subclass inherits the defaults unless it carries its own bare @view_defaults()), and it works only on classes. It also provides the defaults when a decorated class is passed to add_view. This is the Pyramid way to write what Flask calls a class-based view (see Flask Class-Based Views): one class per resource, one method per HTTP method. The difference is that the HTTP method is selected by a view predicate, not by the name of the method.

The application factory includes the routes and scans the package. The relative name ".views" is resolved against the package of the code that calls scan.

# norun
# file: shelfapp/__init__.py
from pyramid.config import Configurator


def main(global_config, **settings):
    with Configurator(settings=settings) as config:
        config.add_route("home", "/")
        config.add_route("api_shelf", "/api/shelves/{shelf}")
        config.add_route("api_book", "/api/shelves/{shelf}/books/{book_id}")
        config.scan(".views")
    return config.make_wsgi_app()
from shelfapp import main
from shelfapp.views import SHELVES

app = TestApp(main({}))

assert app.get("/").json == {"app": "Bookshelf Shelves", "shelves": ["ada", "grace"]}
assert app.get("/api/shelves/ada").json["books"] == ["Dune", "Hyperion"]
assert app.get("/api/shelves/ada/books/1").json == {"shelf": "ada", "title": "Hyperion"}
assert app.get("/api/shelves/nobody", status=404).status_int == 404

created = app.post("/api/shelves/grace?title=Solaris")
assert created.status_int == 201 and created.json["books"][-1] == "Solaris"

# POST without the title parameter matches no view: 404, not 405
assert app.post("/api/shelves/grace", status=404).status_int == 404
# HEAD is answered by the GET view
assert app.head("/api/shelves/ada").status_int == 200

Two observations in that example are explained in the lookup section below. The view for POST needs a title parameter, so a POST without one falls through every candidate and ends as a 404; and request_method="GET" also matches HEAD, which is why the HEAD request succeeds.

View Predicates

A predicate is a condition on the request or the context. A view is a candidate for a request only when every predicate in its configuration is true. Predicates therefore narrow where a view applies: with none, the view is a catch-all; with several, it applies only when all of them match. Predicates also decide the order in which candidates are tried, described in the lookup section.

The built-in view predicates of Pyramid 2.1 are these. The arguments are the same for add_view and @view_config.

Argument Matches when

route_name

The named route matched. Without it, the view applies only when no route matched, which is the traversal case. The route usually has a *traverse token when views are used with traversal.

request_method

The request method is the given string, or one of a tuple of strings. "GET" also matches HEAD (since 1.4). A method that no view accepts gives a 404, not a 405.

request_param

The request has the given key in request.params (query string and form body). "key=value" requires the value to match, and a sequence requires every entry.

match_param

The route’s request.matchdict contains the "key=value" pair (or every pair of a tuple). Useful when one route pattern serves several actions, such as /books/{id}/{action}.

accept

The Accept header allows the given media type, or is missing. It must be a specific media type such as text/html; media ranges were removed in 2.0. Technically not a predicate: it cannot be wrapped in not_.

header

The request has a header with that name (case-insensitive), or Name:regex when the value must also match the regular expression. A sequence requires every entry.

xhr

With True, the request carries X-Requested-With: XMLHttpRequest. A client can send it freely, so it identifies a convention, not a trusted fact.

path_info

The regular expression matches PATH_INFO.

context

The context resource is an instance of the given class, or provides the given interface. An exception class also registers an exception view (see exception_only).

name

The view name equals the given string. It is used with traversal, where the name is the path segment after the context. The default is the empty string, the "default view" of the context.

containment

A parent in the context’s lineage is an instance of the given class or provides the interface. The resource tree must be location-aware (__name__ and __parent__ attributes).

physical_path

The context’s whole physical path equals the given string or tuple (not a prefix, not a regex).

request_type

The request provides the given interface. An advanced hook that applications rarely need.

is_authenticated

True matches only requests from an authenticated user according to the security policy, False only the others (added in 2.0).

effective_principals

Deprecated in 2.0 together with principals as a concept; use is_authenticated or a custom predicate.

custom_predicates

A sequence of (context, request) callables. Deprecated since 1.5 but still accepted by the 2.1 signature; use a registered predicate instead.

**predicates

Any predicate that an application or add-on registered with add_view_predicate.

exception_only is a non-predicate flag described below, next to decorator and wrapper.

Request Predicates in Practice

The request-based predicates need only routes. The example gives one route per technique so that the outcomes are easy to read, then drives each view through a table of requests. All the views are named functions registered with add_view.

def books_list(request):
    return Response("all books")


def books_by_isbn(request):
    return Response("lookup " + request.params["isbn"])


def books_v2(request):
    return Response("v2 listing")


def books_ajax(request):
    return Response("fragment")


def books_mobile(request):
    return Response("mobile listing")


def book_edit(request):
    return Response("edit " + request.matchdict["id"])


def book_delete(request):
    return Response("delete " + request.matchdict["id"])


def csv_export(request):
    return Response("csv", content_type="text/csv")


with Configurator() as config:
    config.add_route("books", "/books")
    config.add_route("book_action", "/books/{id}/{action}")
    config.add_route("export", "/export/{name}")

    config.add_view(books_list, route_name="books")                                  # catch-all for the route
    config.add_view(books_by_isbn, route_name="books", request_param="isbn")
    config.add_view(books_v2, route_name="books", request_param="v=2")
    config.add_view(books_ajax, route_name="books", xhr=True)
    config.add_view(books_mobile, route_name="books", header="X-Client:mobile-.*")

    config.add_view(book_edit, route_name="book_action", match_param="action=edit")
    config.add_view(
        book_delete,
        route_name="book_action",
        match_param="action=delete",
        request_method="POST",
    )
    config.add_view(csv_export, route_name="export", path_info=r"/export/.*\.csv$")
    app = TestApp(config.make_wsgi_app())

ajax = {"X-Requested-With": "XMLHttpRequest"}
assert app.get("/books").text == "all books"
assert app.get("/books?isbn=9780441172719").text == "lookup 9780441172719"
assert app.get("/books?v=2").text == "v2 listing"
assert app.get("/books?v=3").text == "all books"            # value differs: falls through
assert app.get("/books", headers=ajax).text == "fragment"
assert app.get("/books", headers={"X-Client": "mobile-ios"}).text == "mobile listing"
assert app.get("/books", headers={"X-Client": "desktop"}).text == "all books"

assert app.get("/books/7/edit").text == "edit 7"
assert app.post("/books/7/delete").text == "delete 7"
assert app.get("/books/7/delete", status=404).status_int == 404   # method predicate fails
assert app.get("/books/7/print", status=404).status_int == 404    # no action matches

assert app.get("/export/shelf.csv").content_type == "text/csv"
assert app.get("/export/shelf.txt", status=404).status_int == 404

Pyramid never picks a "best" view by hand: the router tries candidates in a fixed order and invokes the first whose predicates all match, and the lookup section explains that order. The example shows the consequences. A request with isbn is served by the more specific view, while the plain /books request, which fails every extra predicate, is served by the catch-all view that has none. match_param is the idiomatic way to dispatch on a segment of a pattern such as /books/{id}/{action}, but many applications prefer separate routes because pviews, proutes and request.route_url then show the intent directly.

Route predicates and view predicates are different registries. config.add_route accepts its own set (for example request_method and request_param, plus traverse, factory, pregenerator and so on), and a request that does not satisfy a route predicate falls through to the next route instead of to the next view. The URL dispatch page of this section covers them.

Predicates for Traversal and Context

context, name, containment and physical_path only make sense when the request has a context found by traversal. Traversal splits the path into resource names and walks a tree of Python objects, looking up each name with __getitem__; whatever remains after the last resource found is the view name. The traversal page of this section describes it fully. The example uses a tree of a root, shelves and books, and sets __name__ and __parent__ so that the resources are location-aware, which containment and physical_path require.

class Resource:
    def __init__(self, name=None, parent=None):
        self.__name__ = name
        self.__parent__ = parent
        self.children = {}

    def __getitem__(self, key):
        return self.children[key]


class Root(Resource):
    pass


class Shelf(Resource):
    pass


class Book(Resource):
    pass


root = Root()
ada = Shelf("ada", root)
root.children["ada"] = ada
dune = Book("dune", ada)
ada.children["dune"] = dune


def shelf_view(context, request):
    return Response("shelf " + context.__name__)


def book_view(context, request):
    return Response("book " + context.__name__ + " on " + context.__parent__.__name__)


def book_edit_form(context, request):
    return Response("edit form for " + context.__name__)


def physical_info(context, request):
    return Response("info for the resource at /ada/dune")


with Configurator() as config:
    config.set_root_factory(lambda request: root)
    config.add_view(shelf_view, context=Shelf)
    config.add_view(book_view, context=Book, containment=Shelf)
    config.add_view(book_edit_form, context=Book, name="edit")
    config.add_view(physical_info, physical_path="/ada/dune", name="info")
    app = TestApp(config.make_wsgi_app())

assert app.get("/ada").text == "shelf ada"
assert app.get("/ada/dune").text == "book dune on ada"
assert app.get("/ada/dune/edit").text == "edit form for dune"
assert app.get("/ada/dune/info").text == "info for the resource at /ada/dune"
assert app.get("/ada/info", status=404).status_int == 404   # physical path differs

Notes on this listing. A view function that declares two arguments receives the context first, as in the first table. name="edit" makes the view reachable at /ada/dune/edit and the empty name is the default view of a context. A physical_path compares the path of the context, not of the URL, so the info view above is found at /ada/dune/info because the context is /ada/dune. containment accepts a class or an interface and walks the lineage upward from the context.

Authentication Predicates and Permissions

is_authenticated and the permission argument both depend on a security policy, an object registered with config.set_security_policy. The policy implements identity, authenticated_userid, permits, remember and forget. This section shows only enough of one to demonstrate the view arguments; the security policy page of this section builds the real thing with AuthTktCookieHelper, and the authorization page covers ACLs in depth. The policy below trusts a plain request header, which is fine for a test and a security hole in production, because anyone can send any header.

from pyramid.authorization import ACLHelper, Allow, Authenticated, Everyone


class DemoPolicy:
    """Test only: a real policy must verify a signed cookie or token, never a bare header."""

    def __init__(self):
        self.acl = ACLHelper()

    def identity(self, request):
        user = request.headers.get("X-Demo-User")
        return {"userid": user} if user else None

    def authenticated_userid(self, request):
        identity = request.identity
        return identity["userid"] if identity else None

    def permits(self, request, context, permission):
        principals = {Everyone}
        if request.identity is not None:
            principals |= {Authenticated, "owner:" + request.identity["userid"]}
        return self.acl.permits(context, principals, permission)

    def remember(self, request, userid, **kw):
        return []

    def forget(self, request, **kw):
        return []


class ShelfResource:
    """Context for /shelves/{shelf}: everybody reads, only the owner edits."""

    def __init__(self, request):
        owner = request.matchdict["shelf"]
        self.__acl__ = [(Allow, Everyone, "read"), (Allow, "owner:" + owner, "edit")]


def shelf_read(request):
    return Response("public shelf")


def shelf_edit(request):
    return Response("shelf edited")


def whoami(request):
    return Response("signed in as " + request.identity["userid"])


def sign_in_hint(request):
    return Response("please sign in")


with Configurator() as config:
    config.set_security_policy(DemoPolicy())
    config.add_route("shelf", "/shelves/{shelf}", factory=ShelfResource)
    config.add_route("whoami", "/whoami")
    config.add_view(shelf_read, route_name="shelf", request_method="GET", permission="read")
    config.add_view(shelf_edit, route_name="shelf", request_method="POST", permission="edit")
    config.add_view(whoami, route_name="whoami", is_authenticated=True)
    config.add_view(sign_in_hint, route_name="whoami", is_authenticated=False)
    app = TestApp(config.make_wsgi_app())

ada = {"X-Demo-User": "ada"}
grace = {"X-Demo-User": "grace"}
assert app.get("/whoami").text == "please sign in"
assert app.get("/whoami", headers=ada).text == "signed in as ada"
assert app.get("/shelves/ada").text == "public shelf"
assert app.post("/shelves/ada", headers=ada).text == "shelf edited"
assert app.post("/shelves/ada", headers=grace, status=403).status_int == 403
assert app.post("/shelves/ada", status=403).status_int == 403

Two behaviours to remember. First, is_authenticated selects a view: the unauthenticated request is not an error, it simply matches the other registration, so one URL can show a sign-in prompt or the user’s page. permission, by contrast, does not select anything: once predicates have chosen a view, the view’s permission is checked, and a failure produces HTTPForbidden (the forbidden view) rather than falling through to another candidate. Second, a view with no permission is open to everyone, even when a security policy is installed. config.set_default_permission("read") gives every view without an explicit permission a default; a view that must stay public can then opt out with NO_PERMISSION_REQUIRED from pyramid.security.

Inverting a Predicate with not_

Wrapping a predicate value in pyramid.config.not_ (added in 1.5) inverts it, and it works for every predicate argument of add_view and view_config, built-in or custom, except accept. Combined with the specificity rule, a view with not_("POST") is tried before a view without predicates:

from pyramid.config import not_


def read_only_notice(request):
    return Response("reads are cached; writes are not")


def write_endpoint(request):
    return Response("write accepted")


with Configurator() as config:
    config.add_route("stats", "/stats")
    config.add_view(read_only_notice, route_name="stats", request_method=not_("POST"))
    config.add_view(write_endpoint, route_name="stats", request_method="POST")
    app = TestApp(config.make_wsgi_app())

assert app.get("/stats").text == "reads are cached; writes are not"
assert app.post("/stats").text == "write accepted"

Custom Predicates

When the built-in predicates do not express a condition, register a new one with config.add_view_predicate(name, factory). After that, name is accepted as a keyword argument by add_view and view_config. The factory is usually a class with four parts: a constructor __init__(self, val, info) that receives the value written in the view configuration and an IPredicateInfo object; a text() method returning a description used in error messages and in pviews output; a phash() method returning a string (or sequence of strings) that identifies the predicate and value for the uniqueness check, often the same as text; and __call__(self, context, request) returning True or False. A predicate can be registered as a route predicate with add_route_predicate; the call signature there is __call__(self, info, request), where info is a dictionary with match and route.

The example adds a content_type predicate for a bulk-import endpoint of the Bookshelf, so that JSON and CSV uploads reach different views and anything else falls through to a catch-all that answers 415.

import json

from pyramid.httpexceptions import HTTPUnsupportedMediaType


class ContentTypePredicate:
    def __init__(self, val, info):
        self.val = val

    def text(self):
        return f"content_type = {self.val}"

    phash = text

    def __call__(self, context, request):
        return request.content_type == self.val


def import_json(request):
    return Response(f"{len(json.loads(request.body))} titles from JSON")


def import_csv(request):
    return Response(f"{len(request.text.splitlines())} titles from CSV")


def import_unsupported(request):
    raise HTTPUnsupportedMediaType()


with Configurator() as config:
    config.add_route("import", "/api/import")
    config.add_view_predicate("content_type", ContentTypePredicate)
    config.add_view(
        import_json, route_name="import", request_method="POST", content_type="application/json"
    )
    config.add_view(
        import_csv, route_name="import", request_method="POST", content_type="text/csv"
    )
    config.add_view(import_unsupported, route_name="import", request_method="POST")
    app = TestApp(config.make_wsgi_app())

as_json = app.post("/api/import", '["Dune", "Emma"]', content_type="application/json")
assert as_json.text == "2 titles from JSON"
as_csv = app.post("/api/import", "Dune\nEmma\nUlysses", content_type="text/csv")
assert as_csv.text == "3 titles from CSV"
assert app.post("/api/import", "x", content_type="text/plain", status=415).status_int == 415

add_view_predicate also accepts weighs_more_than and weighs_less_than to place the new predicate relative to existing ones in the tie-breaking order described below. The directive is a normal configuration action, so it can be called from an add-on’s includeme, which is how libraries such as pyramid_jinja2 and pyramid_tm extend Pyramid. Keep predicates cheap and free of side effects: they run for every candidate view on every request, and they run before the permission check, so a predicate must never be what keeps data private.

Non-Predicate Arguments

The remaining arguments do not narrow the view; they change what happens once it has been chosen.

Argument Purpose

permission

The permission the current identity must have in the context for the view to run. Evaluated by the security policy’s permits. Without it the view is public (or gets the default permission).

renderer

A renderer name such as json, or a template path or asset specification such as templates/shelf.jinja2. A value with a dot is a path whose extension selects the renderer. If the view returns a response object, the renderer is not called. Details are on the renderers page of this section.

attr

The method of a view class to call instead of __call__.

http_cache

Adds Expires and Cache-Control headers. An integer or timedelta means seconds of freshness, 0 means "never cache", and a tuple (seconds, {"public": True}) also sets Cache-Control directives. (None, {...}) sets directives only.

require_csrf

True (or a token name) checks the CSRF token for unsafe methods (anything except GET, HEAD, OPTIONS and TRACE) before the view runs, and enables the origin check; False disables the check when it is on by default. It needs a session factory, and works with the pyramid.require_default_csrf setting. The CSRF page of this section has the details.

decorator

A function, or an iterable of them, that wraps the view. Each receives a view taking (context, request) and must return one that does too.

wrapper

The name of another view that receives this view’s body and response, to compose a page from several views.

mapper

A view mapper that changes how the view is called. An extension hook; applications rarely use it.

exception_only

True registers only an exception view even when context is an exception class. With False, such a view is registered for exception handling and for ordinary contexts as well. Added in 1.8.

The next example uses http_cache, decorator and wrapper together. http_cache is equivalent to calling response.cache_expires in the view body; a view that wants to opt out for one response sets response.cache_control.prevent_auto = True. The pyramid.prevent_http_cache setting (environment variable PYRAMID_PREVENT_HTTP_CACHE) turns the argument off for a whole deployment, which is useful for development.

A decorator receives a view callable that takes (context, request) and returns the response object, even when the view uses a renderer, so a decorator can add headers uniformly. The decorator must return a response or raise.

import time


def timed(view):
    def wrapper(context, request):
        started = time.perf_counter()
        response = view(context, request)
        response.headers["X-View-Time-Ms"] = f"{(time.perf_counter() - started) * 1000:.3f}"
        return response

    return wrapper


def catalogue(request):
    return {"books": ["Dune", "Hyperion"]}


def inner_page(request):
    return Response("<main>catalogue</main>", content_type="text/html")


def page_frame(request):
    return Response(
        "<body>" + request.wrapped_body.decode() + "</body>", content_type="text/html"
    )


with Configurator() as config:
    config.add_route("catalogue", "/catalogue")
    config.add_route("framed", "/framed")
    config.add_view(
        catalogue,
        route_name="catalogue",
        renderer="json",
        http_cache=(3600, {"public": True}),
        decorator=timed,
    )
    config.add_view(inner_page, route_name="framed", wrapper="frame")
    config.add_view(page_frame, name="frame")
    app = TestApp(config.make_wsgi_app())

cached = app.get("/catalogue")
assert cached.json == {"books": ["Dune", "Hyperion"]}
assert cached.headers["Cache-Control"] == "max-age=3600, public"
assert "Expires" in cached.headers and "X-View-Time-Ms" in cached.headers
assert app.get("/framed").text == "<body><main>catalogue</main></body>"

http_cache with public is only appropriate for responses that do not depend on the user. A view that returns data for the signed-in reader and is cached as public can be served by a shared proxy to another reader. For per-user pages use http_cache=0 or (0, {"private": True}). wrapper is rarely needed in new code, because composing a page from templates and macros is simpler, but it explains a frequent question: a wrapper view is found like any other view by name, with the same request and context.

require_csrf needs a session factory and a token the client sends back. The next example uses a signed-cookie session with a randomly generated key (in a real application the key comes from the environment or a secrets store, never from source code). A token can be sent as the csrf_token form field or as the X-CSRF-Token header.

import secrets

from pyramid.session import SignedCookieSessionFactory


def issue_token(request):
    return Response(request.session.get_csrf_token())


def borrow_book(request):
    return Response("borrowed")


with Configurator() as config:
    config.set_session_factory(SignedCookieSessionFactory(secrets.token_hex(32)))
    config.add_route("token", "/token")
    config.add_route("borrow", "/borrow")
    config.add_view(issue_token, route_name="token")
    config.add_view(borrow_book, route_name="borrow", request_method="POST", require_csrf=True)
    app = TestApp(config.make_wsgi_app())

token = app.get("/token").text
assert app.post("/borrow", status=400).status_int == 400                          # no token
assert app.post("/borrow", headers={"X-CSRF-Token": token}).text == "borrowed"
assert app.post("/borrow", {"csrf_token": token}).text == "borrowed"
assert app.post("/borrow", {"csrf_token": "wrong"}, status=400).status_int == 400

The origin check that require_csrf switches on applies to HTTPS requests, which the plain-HTTP test client above does not exercise. The failure is BadCSRFToken (a 400); the details of the origin check and of the two settings pyramid.csrf_trusted_origins and pyramid.require_default_csrf belong to the CSRF page.

How View Lookup Works

Understanding lookup removes most of the mystery from "why was that view called" and "why 404". For each request the router first determines a route (or does not) and a context (the root, or whatever traversal found) and a view name. Pyramid then collects the views registered for that combination of route, context type and view name, sorts them, and tries them one by one until the predicates of one all match.

flowchart TD A["Router: route, context and view name known"] --> B["Collect views registered for that route, context type and view name"] B --> C["Sort candidates: most predicates first, views with none last"] C --> D{"Next candidate: do all of its predicates match?"} D -->|no| E{"More candidates?"} E -->|yes| D E -->|no| F["HTTPNotFound: not found view"] D -->|yes| G{"Does the view have a permission?"} G -->|no| I["View deriver chain: mapper, renderer, decorator, caching"] G -->|yes| H{"Security policy permits it?"} H -->|yes| I H -->|no| J["HTTPForbidden: forbidden view"] I --> K["View runs; response returned"]
Figure: how Pyramid picks a view for a request

Source: Pyramid: View Configuration Parameters

The rules that follow from the diagram are worth stating plainly.

  • More predicates win. A view with five predicates is evaluated before a view with two, and a view with none is last. The official text says so, and the sort key in the 2.1 source (PredicateList.make in pyramid/config/predicates.py) combines the count with a per-predicate weight.

  • Failing predicates fall through. A candidate whose predicates do not match is skipped, not an error. If every candidate is skipped, the result is the not found view (404). This is why a POST to a GET-only view gives 404, not 405, and why a missing Accept match is also 404 unless a catch-all view exists.

  • Ties are decided by predicate weight, not by registration order. Views with the same number of predicates are ordered by weights that depend on which predicates they use. In the example below a view with header is tried before a view with request_param. That order is an implementation detail: do not build an application that needs both views to match the same request, and make predicates mutually exclusive instead.

  • Permissions are checked after predicates. The first candidate whose predicates match is the one whose permission is checked. A permission failure does not select another view.

  • Identical registrations conflict. Two add_view calls with exactly the same route, name, context and predicates are a configuration conflict, found when the configuration is committed. The fix is the usual one for configuration conflicts: make them differ, or let an including module override an included one, as in the configuration page.

from pyramid.exceptions import ConfigurationConflictError


def by_param(request):
    return Response("param")


def by_header(request):
    return Response("header")


def fallback(request):
    return Response("fallback")


with Configurator() as config:
    config.add_route("probe", "/probe")
    config.add_view(by_param, route_name="probe", request_param="a")
    config.add_view(by_header, route_name="probe", header="X-A")
    config.add_view(fallback, route_name="probe")
    app = TestApp(config.make_wsgi_app())

assert app.get("/probe").text == "fallback"
assert app.get("/probe?a=1").text == "param"
assert app.get("/probe", headers={"X-A": "1"}).text == "header"
assert app.get("/probe?a=1", headers={"X-A": "1"}).text == "header"   # both match: weights decide

try:
    with Configurator() as config:
        config.add_route("probe", "/probe")
        config.add_view(by_param, route_name="probe")
        config.add_view(by_header, route_name="probe")
except ConfigurationConflictError as exc:
    print(str(exc).splitlines()[0])

The conflict is raised when the with block exits and the configuration is committed, not at the add_view call, so a conflict appears as soon as the application starts; it is never discovered at request time.

Content Negotiation with the Accept Header

accept dispatches between views on the media types the client prefers. The usual pattern is one view per representation with a catch-all that raises HTTPNotAcceptable, so that an unsupported type answers 406 instead of 404. The example serves the same shelf as JSON or as HTML.

from pyramid.httpexceptions import HTTPNotAcceptable


def shelf_as_json(request):
    return {"shelf": "ada", "books": SHELVES["ada"]}


def shelf_as_html(request):
    items = "".join(f"<li>{title}</li>" for title in SHELVES["ada"])
    return Response(f"<ul>{items}</ul>", content_type="text/html")


def not_acceptable(request):
    raise HTTPNotAcceptable()


def negotiation_app(order=False):
    with Configurator() as config:
        config.add_route("ada", "/shelves/ada")
        config.add_view(shelf_as_json, route_name="ada", accept="application/json", renderer="json")
        config.add_view(shelf_as_html, route_name="ada", accept="text/html")
        config.add_view(not_acceptable, route_name="ada")
        if order:
            config.add_accept_view_order("text/html")
            config.add_accept_view_order("application/json", weighs_more_than="text/html")
        return TestApp(config.make_wsgi_app())


app = negotiation_app()
assert app.get("/shelves/ada", headers={"Accept": "application/json"}).content_type == "application/json"
assert app.get("/shelves/ada", headers={"Accept": "text/html"}).content_type == "text/html"
assert app.get("/shelves/ada", headers={"Accept": "text/plain"}, status=406).status_int == 406
# a missing header or */* is ambiguous: the default order prefers human-readable types
assert app.get("/shelves/ada", headers={"Accept": "*/*"}).content_type == "text/html"
assert app.get("/shelves/ada").content_type == "text/html"

# an API-first application states the opposite preference
api = negotiation_app(order=True)
assert api.get("/shelves/ada", headers={"Accept": "*/*"}).content_type == "application/json"
assert api.get("/shelves/ada").content_type == "application/json"

When the header does not say which view should win (*/*, a missing header, an invalid header, or several types with equal quality), Pyramid has to choose. add_accept_view_order states a preference between media types; a value is placed relative to another with weighs_more_than or weighs_less_than. The default order, if you configure nothing, prefers readable types:

text/html, application/xhtml+xml, application/xml, text/xml, text/plain, application/json

Types outside the list are ordered unpredictably among otherwise similar views. Views for the same (name, context, route_name) are sorted first by how specific the offer is, so text/plain;charset=utf8 precedes text/plain. It is an error to order a type against a type with parameters; sort type/subtype against type/subtype and type/subtype;params against its own kind. Since 2.0, accept takes only specific media types; a media range such as text/* that older tutorials show is no longer accepted.

For JSON-only endpoints, the simplest approach is not to use accept at all, and to have an API exposed under its own route prefix (/api/...). Reserve accept for URLs that really serve several representations.

Debugging View Configuration

Most "my view is not called" problems are predicate mismatches or an unscanned module. Three tools find them.

  • The 404 itself already hints at the cause: when candidates exist but their predicates rejected the request, the body of the 404 response ends with a line such as predicate mismatch for view ... (request_method = GET,HEAD), as in the POST examples above. pyramid.debug_notfound (environment variable PYRAMID_DEBUG_NOTFOUND) adds the routing and traversal details (matched route, view name, traversed path, root) to the response and to the log. Never enable it in production because it describes the application’s structure to anyone who sends a request. The settings page of this section lists its neighbours pyramid.debug_routematch and pyramid.debug_authorization.

  • pviews prints the views that might match a URL, with their predicates, in the order Pyramid tries them.

  • proutes lists routes with their patterns, the views registered for each, and the request methods.

pviews and proutes take the configuration file and the path of a URL, so they need the .ini file of the application (pviews pviews.ini /api/shelves/ada). The file for the package above is two lines and uses the call: scheme of PasteDeploy to reach the factory:

[app:main]
use = call:shelfapp:main

[server:main]
use = egg:waitress#main
listen = 127.0.0.1:6543

Source: Pyramid: pviews

pviews pviews.ini /api/shelves/ada

The output lists the three views of the shelf API in lookup order; add_book has two predicates and goes first.

URL = /api/shelves/ada

    context: <pyramid.traversal.DefaultRootFactory object at 0x10ae70410>
    view name:

    View:
    -----
    shelfapp.views.ShelfAPI.add_book
    view predicates (request_method = POST, request_param title)

    View:
    -----
    shelfapp.views.ShelfAPI.read
    view predicates (request_method = GET,HEAD)

    View:
    -----
    shelfapp.views.ShelfAPI.clear
    view predicates (request_method = DELETE)

For a URL with a single view, such as /api/shelves/ada/books/0, the same command prints the Route: block with its name and pattern above the view. With several candidates, as above, the 2.1 output lists only the views. proutes gives the overview of the whole application:

Name         Pattern                                 View                                Method
----         -------                                 ----                                ------
home         /                                       shelfapp.views.home                 *
api_shelf    /api/shelves/{shelf}                    shelfapp.views.ShelfAPI.read        GET
api_shelf    /api/shelves/{shelf}                    shelfapp.views.ShelfAPI.add_book    POST
api_shelf    /api/shelves/{shelf}                    shelfapp.views.ShelfAPI.clear       DELETE
api_book     /api/shelves/{shelf}/books/{book_id}    shelfapp.views.book_detail          GET

Source: Pyramid: pviews

The command-line page of this section covers the other p-scripts. The third check, unrelated to predicates, is that the module that holds the decorated views is actually inside the package given to config.scan.

The Same Idea in Other Frameworks

Pyramid’s predicates have no direct counterpart in the other Python frameworks on this site. Flask (Routing & URL Building) registers a URL rule with a list of methods, and any other condition (a header, a query parameter) is checked by hand inside the function. Tornado (Routing) maps a URL to a handler class and an HTTP method to a method of that class (get, post). FastAPI declares the path and method in the decorator and expresses query and header requirements as typed parameters. In Pyramid the same conditions stay declarative, as predicates, and the router picks the matching view before any of your code runs. The price is that the matching rules need to be understood, which is what the lookup section is for.

Need Flask Pyramid

One URL, several methods

@app.route("/x", methods=["GET", "POST"]) and a branch inside the function, or MethodView

One view per method with request_method, or one class with @view_defaults

One URL, JSON or HTML

Inspect request.accept_mimetypes in the function

accept predicate with one view per representation

Public or members only

if not current_user.is_authenticated in the function

is_authenticated=True and False registrations, or a permission

Parameter-based dispatch

Branch on request.args

request_param="isbn"

Pitfalls

Symptom Cause and fix

A decorated view answers 404

The module was never scanned, or config.scan() was given a package that does not import it. Scan the package that holds the views and check with proutes or pviews.

ValueError: Could not convert return value of the view callable

The view returned a dict or list and no renderer is configured. Add renderer="json" or return a Response.

POST gives 404 where 405 was expected

Pyramid treats a predicate mismatch as "no view found". Register a catch-all view for the route that raises HTTPMethodNotAllowed, or accept the 404.

Two views, same URL, the wrong one wins

Both match and the order is an implementation detail. Make predicates mutually exclusive, or give one more predicates.

ConfigurationConflictError at startup

Two registrations have identical discriminators (route, name, context and predicates). Make one differ, or override from the including module.

The same predicate on a view and the matching route

Route predicates fall through to the next route, view predicates to the next view. Put the condition where the fall-through you want happens.

accept used with a media range

Removed in 2.0. Use an explicit media type and add_accept_view_order for ambiguous requests.

A class view that keeps state between requests

A class view is created per request, an instance view is shared. Never store request data on a shared instance.

@view_defaults on a function

It works on classes only. Put the arguments on the function’s own @view_config.

http_cache on personalised pages

public allows shared caches to store the response. Use 0 or private for anything that depends on the user.

Security Notes

  • Predicates are not authorization. xhr, header and request_param are controlled by the client. Protect data with permission and a security policy, never with a predicate. The reverse is also true: the permission is checked on the first matching view only, so make sure the view that matches is the one that carries the permission.

  • No permission means public. A view without permission is open to every caller, even when a security policy is installed. Prefer config.set_default_permission(...) and explicitly opt out public views.

  • State-changing views need CSRF protection. Use require_csrf=True per view or the pyramid.require_default_csrf setting for the whole application. A GET view must not change data.

  • Do not trust the demo policy. The header-based policy in this page exists to keep the example short.

  • Do not leave debug settings on. pyramid.debug_notfound and pyramid.debug_authorization reveal internals.

What Changed, and Where Older Tutorials Are Outdated

Version Change

1.4

View predicates became pluggable with add_view_predicate, and **predicates was added. custom_predicates followed as the older mechanism and was deprecated in 1.5.

1.5

not_ for inverting predicate values.

1.8

exception_only.

2.0

accept takes only specific media types, with no media ranges. is_authenticated is a new predicate; effective_principals is deprecated because principals are no longer part of the security model. ACLHelper and the other helpers replace the old authentication and authorization policy classes.

2.1

The view system uses SHA-256 instead of MD5 for its hashes. Python 3.10 to 3.14 are supported.

Tutorials written for Pyramid 1.x often show accept="text/*", custom_predicates=(func,), effective_principals=... and config.set_authentication_policy with config.set_authorization_policy. Each of them still has a modern replacement: explicit media types with add_accept_view_order, a registered predicate with add_view_predicate, is_authenticated or a custom predicate, and config.set_security_policy. Source: Pyramid: What’s New in Pyramid 2.1

Where to Go Next

The next pages of this section continue with the pieces this page referred to: URL dispatch decides which route matches and what request.matchdict holds, traversal and resources define the context and view names used by the context, name, containment and physical_path predicates, and errors and exception views, renderers and templates fill in the remaining arguments of the table. The security pages of the section replace the demo policy of this page with a real one.