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.

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

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

__name__

The name by which the parent’s __getitem__ returns the resource. For the root it must be None or the empty string; any other value is prepended to every generated URL and path.

__parent__

A reference to the parent resource, and None for the root. Following the references leads to the root, like cd .. in a file system.

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.

  1. The router builds the request and calls the root factory with it to get the root resource.

  2. 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 a URLDecodeError.

  3. Starting at the root, Pyramid calls __getitem__ with the first segment, then calls the result’s __getitem__ with the second segment, and so on.

  4. Traversal stops at the first of: the path is exhausted, a __getitem__ raises KeyError, the current resource has no __getitem__, or the next segment starts with @@.

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

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

flowchart TD A["PATH_INFO /shelves/ada/dune/edit"] --> B["root factory returns the root resource"] B --> C["root[shelves]: Shelves container"] C --> D["shelves[ada]: Shelf container"] D --> E["ada[dune]: Book leaf"] E --> F{"Book is a container?"} F -- no --> G["context = dune, view_name = edit, subpath = empty"] G --> H["view lookup: view registered for Book with name edit"] H --> I["view called, response rendered"]
Figure 1. Traversal of /shelves/ada/dune/edit: three successful lookups, then the leaf has no child lookup so the next segment becomes the view 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

request.context

The resource where traversal ended.

request.view_name

The name left over, or ''.

request.subpath

A tuple with the segments after the view name.

request.traversed

A tuple with the segments that were successfully traversed (relative to the virtual root, if any).

request.root

The physical root object returned by the root factory.

request.virtual_root, request.virtual_root_path

The virtual root and its path (see Virtual Hosting and the Virtual Root); the root itself and () by default.

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

pyramid.location.lineage(resource)

Generator: the resource, then its parent, and so on up to the root.

pyramid.location.inside(a, b)

True if b is in the lineage of a.

pyramid.traversal.find_root(resource)

The physical root (the one at the end of the lineage).

pyramid.traversal.find_interface(resource, class_or_interface)

The first resource in the lineage that is of that class or provides that interface; None if there is none.

pyramid.traversal.find_resource(resource, path)

Resolve an absolute path (starting with /) or a path relative to resource; raises KeyError if it cannot be resolved. It is the mirror of resource_path().

pyramid.traversal.traverse(resource, path)

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 request.matchdict.

The URL selects it before any view runs; the view receives context.

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; /shelves/ada/dune and /shelves/ada/ada-cv.pdf use different views automatically.

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

Generating URLs

request.route_url('name', **ids)

request.resource_url(obj); no ids to remember.

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.

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 factory argument (or, if absent, from the global root factory);

  • the traversal path is not the whole PATH_INFO but the part selected by the route’s pattern (*traverse) or by the route’s traverse argument;

  • only views that name the matched route in route_name are considered, unless the route sets use_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 factory returns 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 *subpath performs no traversal but passes the captured remainder as request.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').

Pitfalls

  • Missing __name__ or __parent__. Traversal works without them; the first resource_url() call does not. The error is an AttributeError raised 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 '' or None. Anything else is prepended to every URL and path.

  • __getitem__ must raise KeyError for a missing child. Any other exception (ValueError from int(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, raise KeyError from it.

  • Traversal input is user input. Every segment is passed to your __getitem__; so is the traverse value built from the route’s match dictionary. Never use a segment as a file name, a query fragment or an attribute name. Traversal never calls getattr on 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 %2F acts as a separator and cannot appear inside a segment. Do not use names that contain /.

  • Names that equal view names. A child called edit and a view named edit collide; the child wins. Prefix the view name with @@ in links (resource_url(obj, '@@edit')) when ambiguity is possible.

  • Invalid UTF-8. /%FF raises URLDecodeError, 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_name silently stops matching under a route. If the view "exists" but answers 404 after a refactoring, check route_name and use_global_views first.

  • 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

<converter:name> routes plus +get_or_404()-style loading inside the view; see Routing & URL Building. No tree, no type-based view selection.

Django

path() converters and get_object_or_404() in the view; see URLs & Routing. Class-based views give type-based reuse but not URL-driven object selection.

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_path and model_path_tuple still exist as old aliases of find_resource, resource_path and resource_path_tuple (the project’s pre-"resource" vocabulary); write the new names.

  • Old tutorials written for pcreate scaffolds (zodb and alchemy) and setup.py do 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; KeyError or a missing __getitem__ ends the walk without error.

  • Give every resource __name__ and __parent__; the root’s name is '' or None.

  • request.context, view_name, subpath, traversed and root expose the result; resource_url, resource_path, lineage, find_resource, find_root and find_interface work on the tree.

  • @@name forces a view name; interface contexts decouple views from classes.

  • Hybrid applications traverse after a route: *traverse or the traverse argument chooses the path, factory chooses the root, and only views that name the route (or routes with use_global_views) match.

  • Treat segments as untrusted input and strip X-Vhm-Root at the proxy.