Traversal & Resources
|
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 (the 2.1-branch documentation and the 2.1 release installed from PyPI) on
Python 3.13, checked on 2026-10-07; Pyramid 2.1 itself supports Python 3.10 and newer. Every Python example was
executed with those versions and exercised through webtest.TestApp. The running example is the Bookshelf Shelves
scenario: public shelves owned by users, each holding books, addressed by URLs such as /shelves/ada/dune/edit.
Most frameworks answer the question "which code handles this URL?" with a table of URL patterns. Pyramid answers it with a table too (routes and view configuration are registered on the Configurator), but it also offers a second, older answer that Flask, Django, Tornado and FastAPI do not have: traversal. Instead of matching the URL against patterns, Pyramid walks the URL one segment at a time through a tree of Python objects, much as a file server walks a directory tree. The object where the walk ends is the context of the request, and the view is chosen from the type of that object plus a leftover name. This page explains the tree, the walk, the helper APIs that need a tree, the reasons to prefer it for some problems, and how to combine it with URL dispatch in a hybrid application. URL dispatch itself, the more common choice, is covered by URL Dispatch, and so are views (views and view configuration) and ACL-based authorization (authorization, ACLs and permissions); here they appear only as far as traversal needs them.
For readers coming from the other frameworks on this site, the nearest relatives are the converter-based routes of
Flask routing, the path() converters of
Django URLconfs, and the explicit loading of an object by id in a
FastAPI dependency. In all of them you write the "find the object"
step by hand inside each handler; with traversal the URL itself selects the object before the view runs.
The Resource Tree
A resource tree is a set of nested, dictionary-like objects that starts at a root resource. Pyramid does not
supply the classes: any Python object is a resource. The only convention is the one Python itself uses to tell a
container from a leaf: an object with a __getitem__ method is a container, and one without it is a leaf. A
container’s __getitem__ receives a name and either returns the child registered under it or raises KeyError.
For the Bookshelf Shelves scenario the tree is a root, a shelves container, one container per shelf, and one leaf
per book:
class Resource:
"""A location-aware node: it knows its name and its parent."""
__name__ = None
__parent__ = None
def __init__(self, name, parent=None):
self.__name__ = name
self.__parent__ = parent
class Container(Resource):
"""A node with children. Raising KeyError ends traversal here."""
def __init__(self, name, parent=None):
super().__init__(name, parent)
self._children = {}
def __getitem__(self, name):
return self._children[name]
def add(self, child):
child.__parent__ = self
self._children[child.__name__] = child
return child
class Book(Resource):
"""A leaf: no __getitem__, so traversal cannot go below it."""
def __init__(self, name, title, author):
super().__init__(name)
self.title = title
self.author = author
class Shelf(Container):
def __init__(self, name, owner):
super().__init__(name)
self.owner = owner
class Shelves(Container):
pass
class Bookshelf(Container):
"""The root of the tree: its name is the empty string."""
def __init__(self):
super().__init__("")
ROOT = Bookshelf()
shelves = ROOT.add(Shelves("shelves"))
ada = shelves.add(Shelf("ada", owner="ada"))
ada.add(Book("dune", "Dune", "Frank Herbert"))
ada.add(Book("emma", "Emma", "Jane Austen"))
grace = shelves.add(Shelf("grace", owner="grace"))
grace.add(Book("sicp", "Structure and Interpretation of Computer Programs", "Abelson and Sussman"))
print(ROOT["shelves"]["ada"]["dune"].title)
The output is Dune: the expression ROOT["shelves"]["ada"]["dune"] is exactly what traversal performs for the
path /shelves/ada/dune. A resource tree is therefore ordinary Python: the same classes can be backed by a database
(each __getitem__ queries for the child), by a persistent object database such as the ZODB (see the data-store
pages of this section), or, as here, by plain dictionaries.
Source: Traversal: The Resource Tree
Root Factories
Pyramid does not hold the root object itself. It holds a root factory, a callable that receives the request and
returns the root resource, and it calls it at the start of every request. A class whose __init__ takes the request
works as a factory, and so does a plain function. The factory is configured with the root_factory argument of the
Configurator (also settable with config.set_root_factory()), either as a callable or as a dotted name.
def root_factory(request):
# A real application would open its database session or ZODB connection
# here and return the root object stored in it. This demo shares one tree.
return ROOT
When no root factory is given, Pyramid uses DefaultRootFactory, which returns an empty resource that has no children.
That default is also what makes URL-dispatch-only applications work: the context of every such request is that empty
root. A route may name its own factory, which replaces the global one for that route only; this is the hook hybrid
applications and per-route ACLs use (see below).
Source: Traversal: The Resource Tree
Location Awareness: __name__ and __parent__
Traversal itself needs nothing but __getitem__. Many other Pyramid features, however, walk the tree upwards from
a resource, and for that every resource must be location aware: it needs two attributes.
| Attribute | Meaning |
|---|---|
|
The name by which the parent’s |
|
A reference to the parent resource, and |
The APIs that depend on this are request.resource_url(), resource_path(), resource_path_tuple(),
find_resource(), find_root(), find_interface(), traverse(), virtual_root() and, usually, the ACL-based
permission checks, which inherit ACLs through __parent__ (the authorization page of this section covers that). The
Resource base class above sets both attributes in its constructor and Container.add() sets the parent, which is
the easiest way to keep them consistent. The official documentation also mentions the add-on package
pyramid_traversalwrapper, which wraps each traversed resource in a proxy that computes both attributes
dynamically; this page does not depend on it.
How Traversal Works
Given a request, Pyramid performs the following steps. They are the same whether or not you ever look at them, so it pays to know them by heart.
-
The router builds the request and calls the root factory with it to get the root resource.
-
The path (
PATH_INFO) is split on/into segments. Each segment is URL-unquoted and UTF-8 decoded; empty segments and.are ignored and..removes the previous segment. A path that is not valid UTF-8 raises aURLDecodeError. -
Starting at the root, Pyramid calls
__getitem__with the first segment, then calls the result’s__getitem__with the second segment, and so on. -
Traversal stops at the first of: the path is exhausted, a
__getitem__raisesKeyError, the current resource has no__getitem__, or the next segment starts with@@. -
The last resource found is the context. If the path was exhausted, the view name is the empty string (the default view). Otherwise the view name is the first segment not consumed (for
@@, the segment without the prefix), and the remaining segments are the subpath. -
View lookup then asks the registry for a view registered for the type of the context (a class or an interface) and for that view name.
Note that failing to find a child is not an error: it only turns the name that could not be resolved into the view name. A 404 happens only later, when no view matches the combination of context and name.
The same algorithm is available as a plain function, pyramid.traversal.traverse(), which is the easiest way to see
what a URL resolves to without any request or view. It returns a dictionary with the same names that the router later
sets on the request.
from pyramid.traversal import traverse
info = traverse(ROOT, "/shelves/ada/dune/edit")
print(info["context"].__name__, repr(info["view_name"]), info["subpath"], info["traversed"])
info = traverse(ROOT, "/shelves/ada/missing/x/y")
print(info["context"].__name__, repr(info["view_name"]), info["subpath"])
info = traverse(ROOT, "/shelves/ada/")
print(info["context"].__name__, repr(info["view_name"]), info["subpath"])
info = traverse(ROOT, "/shelves/ada/@@dune")
print(info["context"].__name__, repr(info["view_name"]))
The four lines are dune 'edit' () ('shelves', 'ada', 'dune'), ada 'missing' ('x', 'y'), ada '' () and
ada 'dune'. The second shows that an unresolvable name stops the walk without error. The last shows the @@ prefix,
the "goggles", which forces the rest of the segment to be a view name even though a child called dune exists.
What the Request Exposes
The result of the walk is placed on the request, so any view or tween can read it.
| Request attribute | Value |
|---|---|
|
The resource where traversal ended. |
|
The name left over, or |
|
A tuple with the segments after the view name. |
|
A tuple with the segments that were successfully traversed (relative to the virtual root, if any). |
|
The physical root object returned by the root factory. |
|
The virtual root and its path (see Virtual Hosting and the Virtual Root); the root itself and |
A view may declare (context, request) as its arguments instead of only request; both forms see the same context,
and request.context is the way to read it in the one-argument form. The next example registers views for the tree
above. A view is selected by the class of the context (the context argument) and by the name argument, which is
compared with the view name from traversal.
from pyramid.config import Configurator
from webtest import TestApp
def trace(context, request):
return {
"context": context.__name__,
"view_name": request.view_name,
"subpath": list(request.subpath),
"traversed": list(request.traversed),
}
def shelf_default(context, request):
return {"shelf": context.__name__, "owner": context.owner, "books": sorted(context._children)}
def book_default(context, request):
return {"title": context.title, "author": context.author}
def book_edit(context, request):
return {"editing": context.title, "edit_url": request.resource_url(context, "edit")}
def includeme(config):
config.add_view(trace, context=Resource, name="trace", renderer="json")
config.add_view(shelf_default, context=Shelf, renderer="json")
config.add_view(book_default, context=Book, renderer="json")
config.add_view(book_edit, context=Book, name="edit", renderer="json")
def make_app(**kwargs):
with Configurator(root_factory=root_factory, **kwargs) as config:
config.include(includeme)
return config.make_wsgi_app()
app = TestApp(make_app())
print(app.get("/shelves/ada").json)
print(app.get("/shelves/ada/dune").json)
print(app.get("/shelves/ada/dune/edit").json)
print(app.get("/shelves/ada/dune/trace/a/b").json)
print(app.get("/shelves/ada/@@trace").json["view_name"])
print(app.get("/shelves/ada/ghost", status=404).status)
The first view is the default view of a shelf (registered without a name), the second the default view of a book, the
third the named view edit; trace is registered for the base class, so it answers for every resource and prints
what traversal found. The output of the trace request is {'context': 'dune', 'view_name': 'trace', 'subpath':
['a', 'b'], 'traversed': ['shelves', 'ada', 'dune']}. A request for a name no view is registered for, such as
ghost, answers 404. Because /shelves/ada/@@trace selects trace explicitly, the lookup works even if a shelf
had a child resource called trace.
The decorator @view_config(context=Book, name='edit') is the declarative form of the config.add_view calls above
and works only after a config.scan(); the view configuration page of this section covers it.
Source: Traversal
Views Registered for Interfaces
A view can be registered for a zope.interface interface instead of a class. Any resource that declares the interface
matches, whatever its class, which separates "what the resource is" from the class that implements it. A view
registered for the concrete class wins over a view registered for an interface of that class when both are otherwise
identical.
from zope.interface import Interface, implementer
class ICitable(Interface):
"""Marker: the resource can be cited."""
@implementer(ICitable)
class CitableBook(Book):
pass
def cite(context, request):
return {"citation": f"{context.author}, {context.title}"}
grace.add(CitableBook("hopper", "Understanding Computers", "G. Hopper"))
with Configurator(root_factory=root_factory) as config:
config.include(includeme)
config.add_view(cite, context=ICitable, name="cite", renderer="json")
iface_app = TestApp(config.make_wsgi_app())
print(iface_app.get("/shelves/grace/hopper/cite").json)
iface_app.get("/shelves/ada/dune/cite", status=404)
Only the book that declares ICitable has a cite view; a plain Book answers 404.
Working With Resources
Because every resource knows its parent and name, Pyramid can compute URLs and paths from a resource and search the tree in both directions.
Generating URLs and Paths
request.resource_url(resource, *elements, query=..., anchor=...) builds an absolute URL from the resource’s position
in the tree and the current application URL. Without extra elements it ends with a slash, because a resource is a
"place"; with extra elements the final slash is not added. request.resource_path() returns only the path.
pyramid.traversal.resource_path() and resource_path_tuple() do the same without a request and ignore virtual roots; unlike the request method, resource_path() adds no trailing slash (/shelves/ada/dune).
from pyramid.location import inside, lineage
from pyramid.traversal import find_interface, find_resource, find_root, resource_path, resource_path_tuple
def book_links(context, request):
return {
"url": request.resource_url(context),
"edit": request.resource_url(context, "edit"),
"forced_view": request.resource_url(context, "@@edit"),
"search": request.resource_url(context.__parent__, query={"q": "dune"}, anchor="results"),
"path": request.resource_path(context),
"path_tuple": resource_path_tuple(context),
"root": request.resource_url(request.root),
"lineage": [r.__name__ for r in lineage(context)],
"shelf": find_interface(context, Shelf).__name__,
"inside_shelf": inside(context, context.__parent__),
"same_root": find_root(context) is request.root,
"found": find_resource(context, "/shelves/grace/sicp").title,
"relative": find_resource(context.__parent__, "emma").title,
}
with Configurator(root_factory=root_factory) as config:
config.include(includeme)
config.add_view(book_links, context=Book, name="links", renderer="json")
links_app = TestApp(config.make_wsgi_app())
for key, value in links_app.get("/shelves/ada/dune/links").json.items():
print(key, value)
The output includes url http://localhost/shelves/ada/dune/, edit http://localhost/shelves/ada/dune/edit,
search http://localhost/shelves/ada/?q=dune#results, path /shelves/ada/dune/, path_tuple ['', 'shelves', 'ada',
'dune'] and lineage ['dune', 'ada', 'shelves', '']. The helpers are:
| API | Purpose |
|---|---|
|
Generator: the resource, then its parent, and so on up to the root. |
|
True if |
|
The physical root (the one at the end of the lineage). |
|
The first resource in the lineage that is of that class or provides that interface; |
|
Resolve an absolute path (starting with |
|
The full algorithm from any resource, returning the dictionary shown earlier. |
Source: Resources
Customising the URL of a Resource
A resource may define a __resource_url__ method; when present, resource_url() calls it as __resource_url__(request,
info), where info is a dictionary with physical_path, virtual_path and app_url. Returning a string replaces
the default URL (it must be fully qualified, end in a slash and carry no query string or anchor); returning None
keeps the default. This is the hook for, say, serving shelf URLs from a different host name. For deeper changes the
IResourceURL adapter can be replaced. Note that the hook is ignored when resource_url() receives a route_name
(hybrid URLs, below).
class ShareableShelf(Shelf):
def __resource_url__(self, request, info):
return "https://share.example.org" + info["virtual_path"]
shelves.add(ShareableShelf("linus", owner="linus"))
with Configurator(root_factory=root_factory) as config:
config.add_view(lambda context, request: {"url": request.resource_url(context)},
context=Shelf, renderer="json")
share_app = TestApp(config.make_wsgi_app())
print(share_app.get("/shelves/linus").json)
print(share_app.get("/shelves/ada").json)
The first response is {'url': 'https://share.example.org/shelves/linus/'} and the second the normal
http://localhost/shelves/ada/.
Virtual Hosting and the Virtual Root
Traversal can start inside the tree: if the WSGI environment contains HTTP_X_VHM_ROOT (the header X-Vhm-Root, a
convention inherited from Zope’s virtual hosting), the path given by that header is treated as the virtual root.
Traversal then begins at that resource, and generated URLs omit the prefix. A front-end proxy can thus publish the
subtree of a single shelf as the whole site of a custom domain.
with Configurator(root_factory=root_factory) as config:
config.include(includeme)
config.add_view(book_links, context=Book, name="links", renderer="json")
vhm_app = TestApp(config.make_wsgi_app())
data = vhm_app.get("/dune/links", extra_environ={"HTTP_X_VHM_ROOT": "/shelves/ada"}).json
print(data["url"], data["path"], data["path_tuple"])
The request path /dune/links resolves under /shelves/ada; the book’s URL is reported as
http://localhost/dune/, while resource_path_tuple() (which ignores virtual roots) still shows the physical
location. The security consequence is that the environment key is trusted: if the application is reachable directly,
a client could send the header itself. Set or strip X-Vhm-Root in the reverse proxy, and never expose the WSGI
server without that proxy when the header is in use. The official Virtual Hosting chapter documents the details and
the Zope-style _vh_ path segments.
Source: Virtual Hosting
Much Ado About Traversal: Why It Exists
The official chapter with that name, adapted from a post by Rob Miller, argues from the history of web servers. A static
server maps /joeschmoe/photos/photo1 to folders and a file; CGI and its successors added "if the file is a script,
run it". Traversal is the same idea with nested dictionary-like objects instead of directories, and view lookup is
the "what to do with this kind of file" step: the view is chosen by the type of the object plus the name. URL dispatch,
in contrast, compares the string with a set of patterns and calls the first function that matches.
Traversal does not replace URL dispatch; it fits some problems better:
| Question | URL dispatch | Traversal |
|---|---|---|
Where does the object come from? |
Each view loads it from the ids in |
The URL selects it before any view runs; the view receives |
One URL, different code per kind of object (a shelf, a book, a magazine) |
Branch inside one view, or several routes with predicates. |
Register one view per type; |
Hierarchical or arbitrarily deep data (folders, wikis, CMS pages) |
A route per depth or a catch-all pattern. |
The depth is whatever the tree has. |
Permissions per object |
A route factory per route, with the ACL assembled by hand. |
ACLs live on the objects and are inherited through |
Generating URLs |
|
|
Flat REST-style APIs with fixed patterns |
The natural fit. |
Overkill; use routes. |
A practical rule is that traversal pays off when the data really is a tree and the type of the object decides what a URL means, as in a content management system or the shelf tree of the Bookshelf Shelves scenario, and URL dispatch pays off for flat, fixed URL sets. The official documentation itself recommends starting with URL dispatch if you are new to Pyramid. The rest of this section’s Bookshelf Shelves application therefore uses both: traversal for the shelves, routes for the JSON API.
Source: Much Ado About Traversal
Hybrid Applications: Traversal After a Route
A hybrid application combines both mechanisms: a route matches first, and then traversal runs against a root chosen by the route. The official documentation warns that hybrid behaviour is harder to reason about, so it recommends it only when needed. There are four differences from pure traversal:
-
at least one route is defined;
-
the root object comes from the route’s
factoryargument (or, if absent, from the global root factory); -
the traversal path is not the whole
PATH_INFObut the part selected by the route’s pattern (*traverse) or by the route’straverseargument; -
only views that name the matched route in
route_nameare considered, unless the route setsuse_global_views.
A Route Ending in *traverse
A star argument at the end of the pattern captures the remaining segments, and that remainder becomes the traversal
path. Here a route mounts the shelf tree under /api/:
def api_book(context, request):
return {"title": context.title, "url": request.resource_url(context, route_name="api")}
def api_shelf(context, request):
return {"shelf": context.__name__, "url": request.resource_url(context, route_name="api")}
with Configurator(root_factory=root_factory) as config:
config.add_route("api", "/api*traverse", factory=root_factory)
config.add_view(api_shelf, route_name="api", context=Shelf, renderer="json")
config.add_view(api_book, route_name="api", context=Book, renderer="json")
config.add_view(trace, route_name="api", context=Resource, name="trace", renderer="json")
config.add_view(lambda request: {"global": True}, name="plain", renderer="json")
api_app = TestApp(config.make_wsgi_app())
print(api_app.get("/api/shelves/ada").json)
print(api_app.get("/api/shelves/ada/dune").json)
print(api_app.get("/api/shelves/ada/dune/trace").json["context"])
api_app.get("/api/plain", status=404)
print(api_app.get("/plain").json)
The first response is {'shelf': 'ada', 'url': 'http://localhost/api/shelves/ada/'}: resource_url() accepts
route_name (and route_kw for other dynamic parts of the pattern, and route_remainder_name when the star
argument is not called traverse) to generate URLs under the route. The plain view has no route_name, so it is
reachable by plain traversal (/plain, no route matched) but not under /api (404): this is the fourth difference.
Passing use_global_views=True to add_route makes such views visible to the route as well.
The traverse Argument of a Route
When the traversal path should not be the tail of the URL, give the route a traverse argument. It is a pattern that
uses the same {name} markers as the route pattern and builds the path from what the route matched. Its markers
must exist in the route pattern, otherwise a runtime error occurs, and it is ignored if the pattern already has a
*traverse star. Here the short URL /s/ada/edit traverses to the shelf ada, because the traversal path becomes
/shelves/ada:
def shelf_edit(context, request):
return {"editing_shelf": context.__name__, "shelf_segment": request.matchdict["shelf"]}
with Configurator(root_factory=root_factory) as config:
config.add_route("shelf_edit", "/s/{shelf}/edit", traverse="/shelves/{shelf}", factory=root_factory)
config.add_view(shelf_edit, route_name="shelf_edit", context=Shelf, renderer="json")
arg_app = TestApp(config.make_wsgi_app())
print(arg_app.get("/s/ada/edit").json)
arg_app.get("/s/ghost/edit", status=404)
The response is {'editing_shelf': 'ada', 'shelf_segment': 'ada'}. For the unknown shelf traversal stops at the
shelves container with the view name ghost; no view for a Shelves context and that name is registered, so the
response is 404, which is exactly the "object not found" behaviour you would otherwise code by hand.
Route Factories, *subpath and Plain URL Dispatch
-
A route’s
factoryreturns the root for that route only. This is how URL-dispatch applications get a context with an ACL (the authorization page of this section uses it for the JSON API) without having any tree. -
A route pattern ending in
*subpathperforms no traversal but passes the captured remainder asrequest.subpath; the static view helper uses it. -
For a route without
*traverse, traversal still runs, over the empty path: the context is the factory’s root and the view name is''.
Replacing the Traverser
The algorithm above is implemented by pyramid.traversal.ResourceTreeTraverser, an adapter registered for the root
object. config.add_traverser(adapter, iface=None) replaces it, for all roots when iface is omitted or only for
roots providing an interface. A traverser is a callable class that receives the root in its constructor and the
request when called, and must return the same dictionary the default produces (context, view_name, subpath,
traversed, virtual_root, virtual_root_path, root). Subclassing the default keeps the work to the part that
changes; the add-on pyramid_traversalwrapper is a published example (it wraps resources so you need not manage
__name__ and __parent__). A custom traverser is rarely necessary: first consider a __getitem__ that
resolves your naming scheme.
from pyramid.traversal import ResourceTreeTraverser
seen = []
class RecordingTraverser(ResourceTreeTraverser):
def __call__(self, request):
result = super().__call__(request)
seen.append((request.path_info, result["context"].__name__, result["view_name"]))
return result
with Configurator(root_factory=root_factory) as config:
config.include(includeme)
config.add_traverser(RecordingTraverser)
rec_app = TestApp(config.make_wsgi_app())
rec_app.get("/shelves/ada/dune/edit")
print(seen)
The recorded tuple is ('/shelves/ada/dune/edit', 'dune', 'edit').
Source: Hooks: Changing the Traverser
Pitfalls
-
Missing
__name__or__parent__. Traversal works without them; the firstresource_url()call does not. The error is anAttributeErrorraised from the URL machinery, far from the class that forgot the attribute. Give every resource a base class that sets them. -
The root
__name__must be''orNone. Anything else is prepended to every URL and path. -
__getitem__must raiseKeyErrorfor a missing child. Any other exception (ValueErrorfromint(name),IndexError, a database error) propagates as a 500 instead of ending traversal. Validate the name and translate failures. -
A leaf must not have
__getitem__. If a class already has one for another reason, raiseKeyErrorfrom it. -
Traversal input is user input. Every segment is passed to your
__getitem__; so is thetraversevalue built from the route’s match dictionary. Never use a segment as a file name, a query fragment or an attribute name. Traversal never callsgetattron your objects: only__getitem__is consulted, so internal attributes are not exposed by default; keep it that way. -
Encoded slashes. The path is unquoted before it is split, so
%2Facts as a separator and cannot appear inside a segment. Do not use names that contain/. -
Names that equal view names. A child called
editand a view namededitcollide; the child wins. Prefix the view name with@@in links (resource_url(obj, '@@edit')) when ambiguity is possible. -
Invalid UTF-8.
/%FFraisesURLDecodeError, which nothing catches by default, so the client sees a server error; register an exception view that answers 400. -
Hybrid surprises. A view without
route_namesilently stops matching under a route. If the view "exists" but answers 404 after a refactoring, checkroute_nameanduse_global_viewsfirst. -
Authorization. Traversal alone makes every resource reachable; only a permission on the view or a default permission denies access. Inheriting ACLs through
__parent__is the usual pattern and is covered by the authorization page of this section.
The following block reproduces the first two pitfalls so that their symptoms are recognisable. The first request hits a
resource that forgot its location attributes, the second a __getitem__ that raises the wrong exception.
class Careless:
"""No __name__, no __parent__."""
class CarelessRoot(Container):
def __getitem__(self, name):
if name == "careless":
return Careless()
return int(name) # ValueError on non-numeric names
careless_root = CarelessRoot("")
with Configurator(root_factory=lambda request: careless_root) as config:
config.add_view(lambda context, request: {"url": request.resource_url(context)},
context=Careless, renderer="json")
careless_app = TestApp(config.make_wsgi_app())
for path in ("/careless", "/ghost"):
try:
careless_app.get(path)
except Exception as exc:
print(path, type(exc).__name__)
Both requests fail with unhandled exceptions: /careless with AttributeError (no __name__), /ghost with
ValueError. The correct versions add the base class and catch the error in __getitem__:
class SafeRoot(Container):
def __getitem__(self, name):
try:
return self._children[int(name)]
except (ValueError, KeyError):
raise KeyError(name) from None
safe_root = SafeRoot("")
with Configurator(root_factory=lambda request: safe_root) as config:
config.add_view(lambda context, request: {"ok": True}, context=SafeRoot, renderer="json")
safe_app = TestApp(config.make_wsgi_app())
safe_app.get("/ghost", status=404)
print(safe_app.get("/").json)
Compared With Other Frameworks
| Framework | Closest equivalent and the difference |
|---|---|
Flask |
|
Django |
|
Tornado |
Regular-expression routes whose groups are passed to handler methods; see Routing. Loading is manual. |
FastAPI |
Path parameters and dependencies that load an object by id; see Dependency Injection. |
Zope and Plone |
The origin of traversal, view lookup and ACL inheritance; Pyramid keeps the ideas and drops the framework. |
Recent Changes and Outdated Material
-
Pyramid 2.0 dropped Python 2 and replaced the pair of authentication and authorization policies with a single security policy; ACL checks now go through
ACLHelper, described in the authorization page of this section. The traversal and resource APIs described here are the same ones the 2.x tutorials use. -
pyramid.traversal.find_model,model_pathandmodel_path_tuplestill exist as old aliases offind_resource,resource_pathandresource_path_tuple(the project’s pre-"resource" vocabulary); write the new names. -
Old tutorials written for
pcreatescaffolds (zodbandalchemy) andsetup.pydo not apply to 2.1; the traversal concepts in them do. The wiki tutorials of the official documentation are the maintained reference for traversal with the ZODB and with SQLAlchemy. -
Pyramid is synchronous WSGI in 2.1:
__getitem__runs in the worker thread, so a database call there blocks only that thread.
Summary
-
Traversal turns the URL into a context object by repeatedly calling
__getitem__, then selects a view from the object’s type and the leftover name;KeyErroror a missing__getitem__ends the walk without error. -
Give every resource
__name__and__parent__; the root’s name is''orNone. -
request.context,view_name,subpath,traversedandrootexpose the result;resource_url,resource_path,lineage,find_resource,find_rootandfind_interfacework on the tree. -
@@nameforces a view name; interface contexts decouple views from classes. -
Hybrid applications traverse after a route:
*traverseor thetraverseargument chooses the path,factorychooses the root, and only views that name the route (or routes withuse_global_views) match. -
Treat segments as untrusted input and strip
X-Vhm-Rootat the proxy.