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.
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.
Source: Extending Pyramid Configuration
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"]}
Source: Including Packages
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.
Source: Static Assets: Overriding Assets
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_routestatements 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 |
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(...), orpyramid.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 withunhook_zca()), which can disturb a Zope application running in the same process; -
pass the global registry to the
Configuratorconstructor withregistry=and callconfig.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. Thepyramid_-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(throughpyramid.includes) and out ofproduction.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
includemeand 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=Truedisables 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.xcompetes for the namespace; claim names that describe your add-on, and useadd_request_methodso that clashes are caught at commit time. -
Overrides are not access control.
override_assetwith 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 |
|---|---|
|
The |
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. |
|
Dormant since 2012; the official chapter itself mentions it only as an example. View classes with |
|
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 |
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 |
|
Registrations by the includer win; |
Flask |
Extensions with |
Later registration or configuration; blueprints can override templates and static files by order of registration. |
Django |
Installable apps in |
Template and static file overriding by app order; URL ordering in the URLconf. |
FastAPI |
Routers with |
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.