Extending & Add-ons

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: a small catalogue of public shelves owned by users. Here it is used to build a tiny add-on that awards badges to shelves, and then to extend a second, unrelated shelf application without touching its source.

Most frameworks answer "how do I reuse somebody else’s code?" with a plugin or "app" concept: Flask has extensions and blueprints, Django has installable apps listed in INSTALLED_APPS. Pyramid deliberately has neither. It has one small mechanism, config.include, which runs a function that receives a configurator, and everything else is a convention built on top of it. An add-on is a package that provides such a function. An existing application is extended by including its functions from a new package and adding or overriding registrations afterwards. This page explains the mechanism, the conventions, the tools for overriding, and where to look for existing add-ons.

The nearest equivalents elsewhere on this site are Flask Extensions & Ecosystem and Flask Blueprints, and Django’s reusable apps (Project Structure & Apps). The mechanics of config.include, conflict detection and add_directive are introduced in Configuration & the Configurator; this page builds on them and does not repeat the conflict rules in full.

Extensible Is Not Pluggable

The official chapter on extending an application draws a distinction that explains Pyramid’s design. A pluggable application is one that an arbitrary integrator can drop into an arbitrary larger project, as Django promises for its apps. Pyramid does not claim to provide that: it does not mandate a database layer, a template language or a way to register URLs, so two Pyramid applications built with different technology choices are not guaranteed to combine. The documentation says that the idea of a pluggable application therefore belongs to layers built above Pyramid, such as a content management system, which can impose the needed opinions.

What Pyramid does guarantee is that every application is extensible: if it was built following one rule, a third party can reuse, override and extend it without modifying its source. The rule is to put overridable imperative configuration into functions that are used through config.include(), instead of calling configurator methods inline in the body of main(). The vocabulary of the chapter is simple. The developer is the author of the original application and the integrator is whoever wants to reuse or modify it in a context the developer did not anticipate. The fundamental plug points are the three things the configuration declares: routes, views and assets.

Config.include as the Composition Model

Everything on this page follows from the behaviour of config.include(callable, route_prefix=None). A callable is anything that takes a configurator: a function, the dotted name of a function, or a module or dotted module name, in which case Pyramid looks for a function called includeme in it. The callable is given a new Configurator that shares the registry of the includer but has its own include path, so that Pyramid can later tell which statements came from the includer and which from the included code. That distinction is what makes overriding work: when two statements have the same discriminator, the one made by the includer wins over the one made by the code it included, without raising a conflict.

The first example is the rule from the official chapter applied to the Bookshelf Shelves application. The routes and the views are separate configuration functions, and main() only composes them.

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


def home(request):
    return Response("shelves home")


def shelf(request):
    return Response("shelf " + request.matchdict["name"])


def add_routes(config):
    config.add_route("home", "/")
    config.add_route("shelf", "/shelves/{name}")


def add_views(config):
    config.add_view(home, route_name="home")
    config.add_view(shelf, route_name="shelf")


def includeme(config):
    config.include(add_routes)
    config.include(add_views)


with Configurator() as config:
    config.include(includeme)
    app = config.make_wsgi_app()

assert TestApp(app).get("/shelves/ada").text == "shelf ada"

An integrator can now include add_routes or add_views selectively, or include includeme and replace single registrations afterwards. route_prefix is the argument that lets the integrator choose where the included routes live: config.include(add_routes, route_prefix="/library") mounts every route of the function under that prefix. A prefix applies to the routes added inside the included callable and to everything it includes in turn.

Because include accepts dotted names, the callables do not even have to be imported: config.include("pyramid_jinja2") or config.include(".routes") (relative to the current package) are the same operation. The pyramid.includes setting of a configuration file is the declarative form of the same thing; it is covered with the other settings in Settings, Environment & .ini Files and is shown again below for the add-on.

flowchart TD main["main: Configurator"] --> inc["include shelf_badges"] inc --> defaults["add-on defaults: directive, request method, default badge"] main --> own["application statements made after the include"] defaults --> commit["commit: actions sorted and conflicts resolved"] own -->|"same discriminator, includer wins"| commit commit --> reg["final registry and WSGI app"]
Figure 1. Include order and overriding. The add-on registers defaults when it is included, the application registers its own statements afterwards, and at commit time the includer wins any tie.

Writing an Add-on

An add-on in Pyramid is just a Python package that depends on Pyramid and extends what the configurator can do or what the request offers. The Pylons Project catalogue separates add-ons from plain libraries: a package that does not rely on Pyramid is only a library and is not listed. There is no add-on API, no plugin registry and no base class to subclass. What an add-on author writes is:

  • an includeme(config) function, which is the single entry point integrators use;

  • optionally, new configurator methods (directives) registered with add_directive;

  • optionally, new request attributes registered with add_request_method;

  • reading of namespaced settings, with defaults;

  • default registrations (routes, views, subscribers) that the application can override.

The example add-on is shelf_badges. It lets an application declare badges that shelves earn, such as a reader badge for shelves with at least a given number of books. It adds a directive config.add_badge(), a method request.badges(shelf) that evaluates all declared badges for a shelf, and a small JSON view listing the declared badge names. In a real package the following would be the content of shelf_badges/__init__.py; to keep the page runnable as a single file the last lines register the module under that name, which is what pip install and an import would otherwise do.

import sys
import types

from pyramid.config import Configurator
from pyramid.settings import asbool
from webtest import TestApp


# --- shelf_badges/__init__.py ---------------------------------------------
def default_reader(shelf):
    return len(shelf["books"]) >= 3


def add_badge(config, name, test):
    """Directive: declare a badge. `test` receives a shelf mapping."""

    def register():
        config.registry.shelf_badges[name] = test

    intr = config.introspectable(
        "shelf badges", name, "badge " + name, "shelf badge"
    )
    intr["name"] = name
    config.action(("shelf_badges", name), register, introspectables=(intr,))


def badges_for(request, shelf):
    """Request method: names of the badges the shelf has earned."""
    names = [n for n, test in request.registry.shelf_badges.items() if test(shelf)]
    if asbool(request.registry.settings.get("shelf_badges.hide_empty", "false")):
        names = [n for n in names if shelf["books"]]
    return sorted(names)


def list_badges(request):
    return {"badges": sorted(request.registry.shelf_badges)}


def includeme(config):
    config.registry.shelf_badges = {}
    config.add_directive("add_badge", add_badge)
    config.add_request_method(badges_for, "badges")
    config.add_route("shelf_badges.list", "/badges")
    config.add_view(
        list_badges, route_name="shelf_badges.list", renderer="json"
    )
    config.add_badge("reader", default_reader)


# stands in for `pip install shelf_badges` in this single-file demo
module = types.ModuleType("shelf_badges")
module.includeme = includeme
sys.modules["shelf_badges"] = module
# ---------------------------------------------------------------------------

with Configurator() as config:
    config.include("shelf_badges", route_prefix="/api")
    config.add_badge("curated", lambda shelf: shelf["curated"])
    app = config.make_wsgi_app()

assert TestApp(app).get("/api/badges").json == {"badges": ["curated", "reader"]}

Several details here are conventions that every add-on should follow.

Name things so they cannot collide. Route names, view names, setting keys and registry attributes are shared with the application and with every other add-on. The example prefixes the route name (shelf_badges.list) and, below, its setting keys with the package name. Package names follow the Pylons Project advice to call add-ons pyramid_foo where foo describes the functionality, for example pyramid_mailer, so that they can be found on PyPI.

Put all registrations in includeme, nothing at import time. Importing the add-on must not change global state. All effects come from the configurator the integrator hands over. That is also what makes the add-on testable and what makes route_prefix work: the example registers /badges and the integrator chose /api.

Do the real work in config.action. A directive that can be written purely in terms of other directives (like add_subscriber) needs no action. The one above sets state on the registry, so it defers the work: the call to config.action(discriminator, callable) records an action that runs at commit. The discriminator is the uniqueness key; two actions with the same discriminator conflict unless one of them was made by an includer or an intermediate commit() separates them. The tuple ("shelf_badges", name) makes "badge named X" unique. This is also why the state-holder config.registry.shelf_badges = {} is created immediately in includeme and not inside an action: the actions of the directives run later and expect it to exist. If actions must run in a particular order, the order argument and the PHASE0_CONFIG to PHASE3_CONFIG constants control it, as described in the configuration page of this section.

Contribute introspection data. The introspectable passed to action appears in the introspector under the category name chosen by the add-on, so that tools and the debug toolbar can list the badges the application declares. The four arguments of config.introspectable are the category, a discriminator, a title and a type name.

Wrap directives that create conflicting actions. The add_directive method has an action_wrap argument that is True by default and wraps the directive so that a conflict error reports the line of the application that called it instead of the add-on’s internals. Leave it on for directives that call config.action with discriminators. The directive registry itself does not take part in conflict detection: a later add_directive of the same name silently replaces the earlier one.

Request Methods and Their Conflict Checking

config.add_request_method(callable, name, property=False, reify=False) adds a method or, with property=True or reify=True, a property to every request. The documentation recommends it over replacing the whole request with a custom request factory, because several add-ons can each add their own attributes, while there is room for only one factory. The same function is used to check for clashes: it takes part in conflict detection by attribute name, so two add-ons that both claim request.badges stop the application from starting instead of the second one silently winning at runtime. Passing callable=None records the claim without adding anything, which an add-on can use to reserve a name.

from pyramid.exceptions import ConfigurationConflictError


def other_badges(request, shelf):
    return []


def includeme_other(config):
    config.add_request_method(other_badges, "badges")


with Configurator() as config:
    config.include("shelf_badges")
    config.include(includeme_other)
    try:
        config.commit()
    except ConfigurationConflictError as exc:
        print("conflict:", str(exc).splitlines()[0])
    else:
        raise AssertionError("expected a conflict")

The message names the discriminator (here the request extension badges) and the source line of each statement that claimed it, and when the statements come from included code it also shows their include paths, which is how the conflict is traced back to the add-on that caused it. The two usual ways out are to configure only one of the add-ons or to make the application itself register the attribute, because the includer wins.

Settings

Add-ons read their configuration from the application settings, using a key prefix named after the package. The values arrive as strings when they come from an .ini file, so the add-on converts them with pyramid.settings.asbool and aslist (and int()), and supplies a default for every key. The application passes the settings in the configurator constructor or through main(global_config, **settings).

settings = {"shelf_badges.hide_empty": "true"}
with Configurator(settings=settings) as config:
    config.include("shelf_badges")
    config.add_badge("tiny", lambda shelf: len(shelf["books"]) < 3)
    app = config.make_wsgi_app()

request_app = TestApp(app)
probe = []


def _probe(request):
    probe.append(request.badges({"books": [], "curated": False}))
    probe.append(request.badges({"books": ["Dune"], "curated": False}))
    return {}


with Configurator(settings=settings) as config:
    config.include("shelf_badges")
    config.add_badge("tiny", lambda shelf: len(shelf["books"]) < 3)
    config.add_route("probe", "/probe")
    config.add_view(_probe, route_name="probe", renderer="json")
    TestApp(config.make_wsgi_app()).get("/probe")

assert probe == [[], ["tiny"]]

With hide_empty set, an empty shelf is awarded no badges at all, so the first call returns an empty list, while the shelf with one book earns tiny. Settings that are read once at startup belong in includeme; settings that can change per request do not exist in Pyramid, because the settings dictionary is meant to be read-only after startup. The settings page of this section lists the framework’s own keys, so that an add-on’s prefix does not shadow them.

Defaults the Application Can Override

The add-on registered a default badge, reader, and a default route and view. Because the application is the includer, it can replace any of them with ordinary statements placed after the include: no flag in the add-on is needed and no conflict is raised.

def my_list(request):
    return {"badges": ["custom"]}


with Configurator() as config:
    config.include("shelf_badges")
    config.add_badge("reader", lambda shelf: len(shelf["books"]) >= 10)
    config.add_view(my_list, route_name="shelf_badges.list", renderer="json")
    app = config.make_wsgi_app()

assert TestApp(app).get("/badges").json == {"badges": ["custom"]}

The second add_view has the same predicates as the add-on’s view, which is the requirement stated in the official chapter: "the same view predicate attributes as the original". If the application used different predicates, say a request_method, both views would be registered and the router would choose by predicate specificity instead. The same logic applies to the badge: the directive’s discriminator is the badge name, so the application’s reader replaces the default. Because of this rule an add-on should expose its defaults as ordinary registrations rather than hide them in private code, and document their names (route names, badge names, view names) as part of its public contract.

Packaging and Loading

An add-on is distributed as a normal Python package. A minimal pyproject.toml declares the dependency on Pyramid and the trove classifier that the catalogue and PyPI use to group Pyramid packages.

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "pyramid_shelf_badges"
version = "0.1.0"
description = "Badges for Bookshelf Shelves"
requires-python = ">=3.10"
dependencies = ["pyramid>=2.0"]
classifiers = ["Framework :: Pyramid"]

[tool.setuptools]
packages = ["shelf_badges"]

It is loaded either in code, with config.include("shelf_badges"), or from the application’s .ini file with the pyramid.includes setting, one dotted name per line. That form lets an operator turn an add-on on or off without a code change, and it is the standard way of adding developer-only add-ons to a development.ini that is never used in production.

[app:main]
use = egg:bookshelf_shelves
pyramid.includes =
    shelf_badges
shelf_badges.hide_empty = false

A configuration file is read by pserve through PasteDeploy and Plaster, and the same loading happens in the next example, which proves that a pyramid.includes entry is equivalent to the explicit include.

with Configurator(settings={"pyramid.includes": "shelf_badges"}) as config:
    config.add_badge("curated", lambda shelf: shelf["curated"])
    app = config.make_wsgi_app()

assert TestApp(app).get("/badges").json == {"badges": ["curated", "reader"]}

Testing an Add-on

Integrators test their own application, but the add-on author needs tests of the add-on in isolation. Two levels are enough. A unit test builds a configuration with pyramid.testing.testConfig (or setUp and tearDown), includes the add-on and checks the registry; a functional test wraps config.make_wsgi_app() in TestApp, as the examples on this page do. The full testing toolkit is the subject of the testing page.

from pyramid import testing

with testing.testConfig(settings={"shelf_badges.hide_empty": "false"}) as cfg:
    cfg.include("shelf_badges")
    cfg.commit()
    assert "reader" in cfg.registry.shelf_badges

    request = testing.DummyRequest()
    request.registry = cfg.registry
    assert badges_for(request, {"books": ["a", "b", "c"], "curated": False}) == ["reader"]

Extending an Existing Application

The second use of include is the integrator’s. Suppose a different team published a shelf application, shelf_app, and you want a variant with another view, another logo and one different route handler, without forking it. The official chapter prescribes a new package that includes the original’s configuration functions and then registers its own statements. The three plug points are treated differently, and the examples below use real packages on disk because asset specifications need them. This block writes shelf_app (the original, built by following the rule: the configuration lives in functions) and shelf_custom (the override package) into a temporary directory.

import sys
import tempfile
from pathlib import Path

root = Path(tempfile.mkdtemp())
sys.path.insert(0, str(root))

FILES = {
    "shelf_app/__init__.py": '''
from pyramid.config import Configurator


def add_routes(config):
    config.add_route("home", "/")
    config.add_route("shelf", "/shelves/{name}")
    config.add_route("about", "/about")
    config.add_route("page", "/page")


def add_views(config):
    config.add_view("shelf_app.views.home", route_name="home", renderer="json")
    config.add_view("shelf_app.views.shelf", route_name="shelf", renderer="json")
    config.add_view("shelf_app.views.page", route_name="page",
                    renderer="shelf_app:templates/page.jinja2")
    config.add_static_view("static", "shelf_app:static")


def includeme(config):
    config.include(add_routes)
    config.include(add_views)
    config.scan("shelf_app.decorated")


def main(global_config=None, **settings):
    with Configurator(settings=settings) as config:
        config.include("pyramid_jinja2")
        config.include(includeme)
    return config.make_wsgi_app()
''',
    "shelf_app/views.py": '''
def home(request):
    return {"app": "shelf_app", "view": "home"}


def shelf(request):
    return {"app": "shelf_app", "shelf": request.matchdict["name"]}


def page(request):
    return {"title": "Shelves"}
''',
    "shelf_app/decorated.py": '''
from pyramid.view import view_config


@view_config(route_name="about", renderer="json")
def about(request):
    return {"app": "shelf_app", "view": "about"}
''',
    "shelf_app/templates/page.jinja2": "<h1>{{ title }} (original)</h1>\n",
    "shelf_app/static/logo.txt": "original logo\n",
    "shelf_custom/__init__.py": "",
    "shelf_custom/views.py": '''
def shelf(request):
    return {"app": "shelf_custom", "shelf": request.matchdict["name"]}


def about(request):
    return {"app": "shelf_custom", "view": "about"}
''',
    "shelf_custom/templates/page.jinja2": "<h1>{{ title }} (custom)</h1>\n",
    "shelf_custom/static/logo.txt": "custom logo\n",
}
for name, text in FILES.items():
    path = root / name
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text.lstrip("\n"))

import shelf_app

original = TestApp(shelf_app.main())
assert original.get("/shelves/ada").json == {"app": "shelf_app", "shelf": "ada"}
assert original.get("/about").json["app"] == "shelf_app"
assert original.get("/static/logo.txt").text == "original logo\n"
assert "(original)" in original.get("/page").text

The original application works on its own. Because the code is organised as includeme calling add_routes and add_views, the new package can reuse it in three ways, which correspond to the three plug points.

Overriding Views

The new package includes the original, then registers a view with the same predicates as the one to replace. The original shelf_app.views.shelf is never called again; the integrator’s view runs in its place. Views that the integrator does not touch keep working. The official advice is that the new view code is usually a copy of the original with small changes, because a view callable cannot be wrapped by name from outside.

from pyramid.config import Configurator


def custom_main(global_config=None, **settings):
    with Configurator(settings=settings) as config:
        config.include("pyramid_jinja2")
        config.include("shelf_app.includeme")
        config.add_view(
            "shelf_custom.views.shelf", route_name="shelf", renderer="json"
        )
    return config.make_wsgi_app()


custom = TestApp(custom_main())
assert custom.get("/shelves/ada").json == {"app": "shelf_custom", "shelf": "ada"}
assert custom.get("/").json == {"app": "shelf_app", "view": "home"}

Overriding Routes

Routes are different, because the order of add_route calls matters (the first matching route wins), and an override that is appended after the original routes would always lose to them. The official advice is blunt: copy all the add_route statements into the override package, change what must change, and do not include the original’s route function. The original’s add_routes therefore must be excluded, which is possible only because the original put its routes in a function of their own and the application did not scan or call it from the part you keep.

def routes_only_custom(config):
    config.add_route("home", "/")
    config.add_route("shelf", "/library/{name}")  # the URL changes
    config.add_route("about", "/about")
    config.add_route("page", "/page")


def routes_main():
    with Configurator() as config:
        config.include("pyramid_jinja2")
        config.include(routes_only_custom)          # instead of shelf_app.add_routes
        config.include(shelf_app.add_views)         # reuse the original views
        config.scan("shelf_app.decorated")
    return config.make_wsgi_app()


moved = TestApp(routes_main())
assert moved.get("/library/ada").json == {"app": "shelf_app", "shelf": "ada"}
assert moved.get("/shelves/ada", status=404)

Because routes and views are linked only by route name, the original views keep working on the new URL pattern.

Applications That Use Decorators

If the original registers its views with @view_config and a scan, running the scan of its package from the new package puts the original’s statements and the new statements at the same include level, so an override is a conflict, not a replacement. The conflict is resolved the way the configuration page explains for any pair of registrations at the same level: call commit() after the scan, so that the original statements become final, and then add the overriding ones.

from pyramid.exceptions import ConfigurationConflictError


def decorated_main(commit):
    with Configurator() as config:
        config.include("pyramid_jinja2")
        config.include(shelf_app.add_routes)
        config.scan("shelf_app.decorated")
        if commit:
            config.commit()
        config.add_view("shelf_custom.views.about", route_name="about", renderer="json")
    return config.make_wsgi_app()


try:
    decorated_main(commit=False)
except ConfigurationConflictError:
    print("without commit(): conflict")
else:
    raise AssertionError("expected a conflict")

assert TestApp(decorated_main(commit=True)).get("/about").json["app"] == "shelf_custom"

The alternative from the official chapter is to leave out the scan altogether. The decorators then do nothing, and the new package converts their configuration into imperative statements or into configuration of its own. That is the only way to remove a decorated view of the original entirely.

Overriding Assets

Assets (static files and templates, addressed by asset specification) are overridden with config.override_asset(to_override, override_with). Both arguments are asset specifications, and both must end in a slash if they name a directory. The call works for static views that serve the original package, and for templates that are rendered by asset specification, because both resolve the specification at request time.

def assets_main():
    with Configurator() as config:
        config.include("pyramid_jinja2")
        config.include("shelf_app.includeme")
        config.override_asset("shelf_app:static/logo.txt", "shelf_custom:static/logo.txt")
        config.override_asset("shelf_app:templates/", "shelf_custom:templates/")
    return config.make_wsgi_app()


themed = TestApp(assets_main())
assert themed.get("/static/logo.txt").text == "custom logo\n"
assert "(custom)" in themed.get("/page").text
assert "(original)" in original.get("/page").text

An override is a map from one asset (or directory) to another, so everything not mentioned keeps the original. The details that matter are in the static assets page of this section (stacked overrides, absolute paths, cache busting of overridden files, and the lookup change of 2.1); the key point for integrators is that the override package never needs to copy files it does not change. override_asset is a PHASE1_CONFIG action, so it can be written anywhere in the integrator’s configuration. Template overrides depend on the template add-on honouring asset specifications, which pyramid_jinja2 does.

Turning the Pattern into a Procedure

Putting the parts together, the official procedure for extending an existing application is:

  • create a new package (a cookiecutter project is the easy start, see Cookiecutter Projects & pserve);

  • install it in the same environment as the original application, for example with pip install -e .;

  • in its main(), include the original’s configuration functions (or scan it), and register the new views and assets after those includes;

  • for routes, copy the ordered add_route statements and omit the original’s routing function;

  • for decorated code, add a commit() between the scan and the overriding statements.

Two limits are worth stating. Overriding is possible only where the original exposes the configuration: if the original builds everything inline in main(), the integrator can only fork it. And a view override is a replacement of a whole view callable, never a patch of part of it.

The Add-on Catalogue

The Pylons Project keeps a catalogue called Extending Pyramid at trypyramid.com. It lists add-ons (packages that depend on Pyramid) and "development environments" (frameworks and CMSes built on Pyramid), each marked as supported by the Pylons Project or by the community, and filterable by category: asset management, authentication, caching and sessions, debugging, email, forms, internationalization, logging, renderers, search, storage, task queueing, template languages, testing, and others. The catalogue links to the PyPI page, the source repository and the documentation of each package. Because PyPI marks Pyramid packages with the Framework :: Pyramid classifier, a search by classifier finds add-ons that the catalogue does not list.

A listing is not a promise of maintenance. The catalogue is edited by hand and several entries have not had a release for many years. The table below uses the date of the latest PyPI release, as checked on 2026-10-07, and groups the state into four levels: active (a release within the last 12 months), maintained (12 to 36 months), quiet (3 to 5 years) and dormant (more than 5 years). Dormant does not mean broken: small add-ons that do one thing often stop changing because they are finished, and the ones marked as installed were included in the Pyramid 2.1 environment used for this section without error. It does mean that you, not an active maintainer, will have to confirm compatibility with the Pyramid and Python versions you use, and fix problems.

Package Purpose Latest PyPI release Status

pyramid_jinja2

Jinja2 templates (the cookiecutter default); installed and included in the 2.1 check environment.

2.10.1, February 2024

Maintained

pyramid_chameleon

Chameleon templates; installed and included in the 2.1 check environment.

0.3, July 2014

Dormant

pyramid_mako

Mako templates; installed and included in the 2.1 check environment.

1.1.0, August 2019

Dormant

pyramid_tm

One transaction per request, see the SQLAlchemy page of this section; installed and included in the 2.1 check environment.

2.6, November 2024

Maintained

zope.sqlalchemy

Joins the SQLAlchemy session to the transaction manager.

4.1, November 2025

Active

pyramid_retry

Retries a request after transient errors; installed and included in the 2.1 check environment.

2.1.1, March 2020

Dormant

pyramid_exclog

Logs exceptions that reach exception views; installed and included in the 2.1 check environment.

1.1, March 2022

Quiet

pyramid_debugtoolbar

Development-time debug toolbar; never include it in production.

4.12.1, February 2024

Maintained

pyramid_nacl_session

Encrypted, tamper-proof cookie sessions.

1.0.0, August 2020

Dormant

pyramid_redis_sessions

Server-side sessions in Redis.

1.0.1, February 2015

Dormant

pyramid_beaker

Beaker session and cache integration.

0.9, April 2024

Maintained

pyramid_mailer

Outgoing email.

0.15.1, December 2016

Dormant

pyramid_zodbconn

ZODB connections for traversal applications.

0.8.1, July 2017

Dormant

pyramid_services

Service-locator style dependencies built on the registry.

2.2, April 2019

Dormant

pyramid_layout

Layouts and panels for templates.

1.0, August 2014

Dormant

pyramid_handlers

Class-based handlers; superseded by view classes and @view_defaults.

0.5, March 2012

Dormant, obsolete

pyramid_zcml

ZCML declarative configuration.

1.2.0, January 2019

Dormant

cornice

REST services toolkit.

6.1.0, February 2024

Maintained

pyramid_openapi3

OpenAPI 3 validation for views.

0.22.0, June 2026

Active

pyramid_celery

Celery integration.

4.0.0, February 2021

Dormant

pyramid_multiauth

Combine several authentication sources.

1.1.0, June 2026

Active

pyramid_storage

File storage abstraction.

2.0.0, April 2026

Active

deform and colander

Forms and schemas, see the forms page of this section.

deform 3.0.1, February 2026; colander 2.0, January 2023

Active (deform), quiet (colander)

Kotti

CMS built on Pyramid (a "pluggable layer" in the sense of the official chapter).

2.0.9, May 2022

Quiet

Substance D

Application framework and CMS on Pyramid.

1.0.post1, November 2018

Dormant

Source: Extending Pyramid (trypyramid.com), and the PyPI release history of each package (example: pyramid_jinja2)

Several add-ons were written for Pyramid 1.x and may rely on removed or deprecated behaviour. For instance, an add-on that installs the old authentication and authorization policy pair, deprecated since Pyramid 2.0, needs to be checked against the security policy API described in the security page of this section. The practical checklist before adopting one is: read the changelog for the Pyramid versions it supports; run the add-on’s tests (or yours) with -W error::DeprecationWarning; confirm that it has no open issue about your Python version; and keep a fallback, because a small dormant add-on can usually be replaced by a few lines of add_request_method and add_subscriber.

The Component Registry in Brief

All of the above works because Pyramid keeps its configuration in a single object, the application registry. It is a Zope Component Architecture (ZCA) component registry from zope.interface, which stores views, routes, renderers, security policies and settings as utilities and adapters registered against interfaces. config.registry is that object at configuration time, and request.registry is the same object while a request is processed. The official chapter on the ZCA says that Pyramid tends to hide the ZCA from application developers, who need not understand it to write an application; it is a framework implementation detail that add-on authors sometimes use directly.

The registry is a place where an add-on can publish services for the application to retrieve by interface. This needs no more than registerUtility at configuration time and getUtility or queryUtility at request time.

from zope.interface import Interface, implementer


class IShelfCounter(Interface):
    def count(shelf):
        """Return how many books a shelf holds."""


@implementer(IShelfCounter)
class SimpleCounter:
    def count(self, shelf):
        return len(shelf["books"])


def counter_includeme(config):
    config.registry.registerUtility(SimpleCounter(), IShelfCounter)


def count_view(request):
    counter = request.registry.getUtility(IShelfCounter)
    return {"count": counter.count({"books": ["Dune", "Emma"]})}


with Configurator() as config:
    config.include(counter_includeme)
    config.add_route("count", "/count")
    config.add_view(count_view, route_name="count", renderer="json")
    app = config.make_wsgi_app()

assert TestApp(app).get("/count").json == {"count": 2}

A design choice follows from this. In a plain Python application the same service would simply be an attribute set on the registry, as the earlier shelf_badges example did, or a function in a module. Interfaces pay off when several implementations must be swappable by an integrator (the "adapter" idea) and when the lookup should be by contract rather than by name; many add-ons, such as pyramid_services, are built exactly on that. Registering with an interface is also what lets an integrator replace an add-on’s implementation by registering a different one after the include, with the includer-wins rule described earlier.

The Global API and Why Pyramid Avoids It

Pyramid creates one registry per application, not one global registry per process, so that several Pyramid applications can run in one process. As a result the ZCA’s global functions, such as zope.component.getUtility, do not see the application registry by default: they consult the process-wide global registry, and code written for Zope seems to fail. The official documentation lists three ways to deal with this:

  • do not use the global API at all, and use the method API of the registry instead (request.registry.getUtility(...), or pyramid.threadlocal.get_current_registry() where no request is at hand);

  • call config.hook_zca() so that the global functions use the registry of the application that is serving the current request (it can be reversed with unhook_zca()), which can disturb a Zope application running in the same process;

  • pass the global registry to the Configurator constructor with registry= and call config.setup_registry() yourself, so that Pyramid uses the ZCA global registry as its own.

The first is the recommended default. The second is shown below: it needs the zope.component package, which is not a dependency of Pyramid itself.

import zope.component

from pyramid.config import Configurator


def global_api_view(request):
    counter = zope.component.getUtility(IShelfCounter)
    return {"count": counter.count({"books": ["Dune"]})}


with Configurator() as config:
    config.hook_zca()
    config.include(counter_includeme)
    config.add_route("global_api", "/global-api")
    config.add_view(global_api_view, route_name="global_api", renderer="json")
    app = config.make_wsgi_app()

try:
    assert TestApp(app).get("/global-api").json == {"count": 1}
finally:
    config.unhook_zca()

Pitfalls and Security Notes

  • An add-on runs with the privileges of your process. Including it executes its code, and config.scan() of a package imports every module in it. Treat a new dependency as you would any other: check the maintainer, the release history and the source, pin versions, and use hash-checked lock files. The pyramid_-prefixed naming is a convention, not a guarantee, so an attacker can publish a look-alike name.

  • Do not include development add-ons in production. The debug toolbar and its exception interception are for development. Keep them in development.ini (through pyramid.includes) and out of production.ini; the logging and debug page of this section covers the settings.

  • Never put secrets in an add-on’s defaults. Defaults in code are public once the package is published. Take keys, tokens and passwords from the settings, which come from the environment or from a secret store, and fail at startup if a required one is missing.

  • Import-time side effects break composition. Code that registers state, connects to a database or reads files at import time cannot be included twice, cannot be tested in isolation and cannot be reordered. Put effects in includeme and in actions.

  • Forgetting the scan. A decorated view of an included package that is not scanned does not exist, and the application answers 404 instead of reporting an error. Add-ons that use decorators must document which package to scan, or call the scan themselves inside includeme.

  • autocommit=True disables conflict detection and two-phase configuration. With it, the order of statements becomes significant again and an add-on that relies on the includer-wins rule can be silently overridden by the wrong code. Do not use it for applications that use add-ons.

  • Route ordering. An add-on that adds routes places them where it is included. Include add-ons that own broad patterns (a catch-all or *subpath) last, and give an application a way to set the prefix.

  • A request attribute is global. Every add-on that adds request.x competes for the namespace; claim names that describe your add-on, and use add_request_method so that clashes are caught at commit time.

  • Overrides are not access control. override_asset with an absolute path serves whatever is in that directory under the public static URL. Point it only at directories that contain nothing private, as explained in the static assets page of this section.

What Changed and Where Older Material Is Outdated

Older material or practice Current state in Pyramid 2.1

pcreate scaffolds and add-ons that ship a scaffold entry point

The pcreate scaffolds are not part of Pyramid 2.1; project templates are cookiecutters now, so any add-on documentation that tells you to create a project from its scaffold is outdated.

Authentication and authorization policy pairs shipped by an add-on

Deprecated since Pyramid 2.0 in favour of a single security policy. The add-on may continue to work but should be reviewed.

pyramid_handlers (add_handler)

Dormant since 2012; the official chapter itself mentions it only as an example. View classes with @view_defaults and @view_config cover the same need.

config.include with several callables in one call

Accepted only a single callable since Pyramid 1.2.

Treating add-ons as pluggable applications

The documentation says explicitly that Pyramid does not provide pluggable applications; layers such as Kotti or Substance D supply that on top.

The Pylons Project add-on list as a guarantee of compatibility

The list is hand-edited; consult the release date and classifiers, as in the table above.

ZCML as the standard way to make an application overridable

Imperative configuration with include is the documented rule, and pyramid_zcml is an optional extra whose last release is from 2019.

Pyramid 2.1 itself did not change the extension mechanism described here. The changes to this page’s area are the Pyramid 2.0 security policy and its deprecations, and a 2.1 change to override_asset stacking explained in the static assets page of this section.

Comparison with Other Frameworks

Framework Reuse mechanism How to override the reused code

Pyramid

config.include, add-ons that provide includeme, directives and request methods.

Registrations by the includer win; override_asset for files; copy the ordered routes.

Flask

Extensions with init_app and blueprints (see Extensions & Ecosystem).

Later registration or configuration; blueprints can override templates and static files by order of registration.

Django

Installable apps in INSTALLED_APPS (see Project Structure & Apps).

Template and static file overriding by app order; URL ordering in the URLconf.

FastAPI

Routers with include_router (see Introduction & Architecture).

Dependency overrides; ordering of routers.

The decisive difference is that Pyramid detects ambiguous registrations and refuses to start when they cannot be resolved, where Flask and Django let a later registration win silently.

Summary

Pyramid’s extension model is one function, config.include, with conventions on top. Add-ons are packages with an includeme; they add directives, request methods and defaults, read prefixed settings and register their pieces with actions, so that conflicts are detected and the application, as the includer, wins every tie. Extending a third-party application means writing a new package that includes the original configuration and then adds or overrides views, routes (by copying them) and assets with override_asset. The Pylons Project catalogue is the place to look for add-ons, but release dates and your own tests decide which are safe to use. The registry underneath is a ZCA component registry that most applications never touch directly.