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 |
Called with the request. This is the shape of most views. |
Function |
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
( |
Class with |
Pyramid instantiates the class with the request (or the context and request), then calls the method named by the |
Instance of a class with |
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"
Source: Pyramid: View Callables
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])
Source: Pyramid: View Callable Responses
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, andconfig.scan()later performs the sameadd_viewcall. 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
Source: Pyramid: View Configuration
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 |
|---|---|
|
The named route matched. Without it, the view applies only when no route matched, which is the traversal case. The
route usually has a |
|
The request method is the given string, or one of a tuple of strings. |
|
The request has the given key in |
|
The route’s |
|
The |
|
The request has a header with that name (case-insensitive), or |
|
With |
|
The regular expression matches |
|
The context resource is an instance of the given class, or provides the given interface. An exception class also
registers an exception view (see |
|
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. |
|
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 ( |
|
The context’s whole physical path equals the given string or tuple (not a prefix, not a regex). |
|
The request provides the given interface. An advanced hook that applications rarely need. |
|
|
|
Deprecated in 2.0 together with principals as a concept; use |
|
A sequence of |
|
Any predicate that an application or add-on registered with |
Source: Pyramid: Predicate Arguments
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
Source: Pyramid: Predicate Arguments
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
Source: Pyramid: Predicate Arguments
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
Source: Pyramid: Security
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 |
|---|---|
|
The permission the current identity must have in the context for the view to run. Evaluated by the security policy’s
|
|
A renderer name such as |
|
The method of a view class to call instead of |
|
Adds |
|
|
|
A function, or an iterable of them, that wraps the view. Each receives a view taking |
|
The name of another view that receives this view’s body and response, to compose a page from several views. |
|
A view mapper that changes how the view is called. An extension hook; applications rarely use it. |
|
|
Source: Pyramid: Non-Predicate Arguments
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>"
Source: Pyramid: Non-Predicate Arguments
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
Source: Pyramid: Non-Predicate Arguments
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.
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.makeinpyramid/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
POSTto aGET-only view gives 404, not 405, and why a missingAcceptmatch 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
headeris tried before a view withrequest_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_viewcalls 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 thePOSTexamples above.pyramid.debug_notfound(environment variablePYRAMID_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 neighbourspyramid.debug_routematchandpyramid.debug_authorization. -
pviewsprints the views that might match a URL, with their predicates, in the order Pyramid tries them. -
prouteslists 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 |
|
One view per method with |
One URL, JSON or HTML |
Inspect |
|
Public or members only |
|
|
Parameter-based dispatch |
Branch on |
|
Pitfalls
| Symptom | Cause and fix |
|---|---|
A decorated view answers 404 |
The module was never scanned, or |
|
The view returned a dict or list and no renderer is configured. Add |
|
Pyramid treats a predicate mismatch as "no view found". Register a catch-all view for the route that raises
|
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. |
|
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. |
|
Removed in 2.0. Use an explicit media type and |
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. |
|
It works on classes only. Put the arguments on the function’s own |
|
|
Security Notes
-
Predicates are not authorization.
xhr,headerandrequest_paramare controlled by the client. Protect data withpermissionand 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
permissionis open to every caller, even when a security policy is installed. Preferconfig.set_default_permission(...)and explicitly opt out public views. -
State-changing views need CSRF protection. Use
require_csrf=Trueper view or thepyramid.require_default_csrfsetting for the whole application. AGETview 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_notfoundandpyramid.debug_authorizationreveal internals.
What Changed, and Where Older Tutorials Are Outdated
| Version | Change |
|---|---|
1.4 |
View predicates became pluggable with |
1.5 |
|
1.8 |
|
2.0 |
|
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.