What’s Changed & Migration
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Pyramid 2.1 (released 2026-03-11, requires Python 3.10 or newer), checked on 2026-10-07.
Every Python example that is not marked as a listing of a file was executed on Python 3.13 with Pyramid 2.1 and
WebTest 3.0.7, and the "does this still exist?" claims in the tables were checked by importing the names from the installed
package. The running scenario is Bookshelf Shelves: an application that was written against Pyramid 1.x (users own
shelves, readers see public shelves, a JSON API under /api/books/{id}) and has to reach 2.1. Older tutorials, blog
posts and Stack Overflow answers describe Pyramid 1.x. Most of what they show still runs on 2.1, but a good part of it
is deprecated, and a few names were removed outright. This page collects the differences in one place; each topic is
covered in depth on the page named in the table where it appears.
The Pyramid project is conservative about removal. Its policy is that a deprecated feature emits a
DeprecationWarning, is mentioned in the documentation and in the change history, and is kept for at least three minor
releases before it is removed, and that nothing is ever removed in a micro release. That is why most 1.x code still
runs on 2.1 and why the practical migration is mostly about reading warnings, not about rewriting.
At a Glance
| Release | Headline changes |
|---|---|
2.0 (2021-02-28) |
Single security policy ( |
2.1 (2026-03-11) |
Python 3.10 to 3.14 (3.6 to 3.9 dropped); |
Source: What’s New in Pyramid 2.0
Source: What’s New in Pyramid 2.1
Python and Dependency Support
| Pyramid | Python | Notes |
|---|---|---|
1.10 |
2.7, 3.4 and newer |
The last release that supports Python 2. |
2.0 |
Python 2.7, 3.4 and 3.5 dropped; 3.6 to 3.9 supported |
|
2.1 |
3.10 to 3.14 (3.6 to 3.9 dropped) |
Python 3.12, 3.13 and 3.14 are newly supported. Development dependencies moved from extras to dependency groups. |
The cookiecutter project that Pyramid recommends (see Cookiecutter Projects & pserve)
generates a pyproject.toml that declares requires-python = ">=3.8" even though Pyramid 2.1 itself needs 3.10, so
raise that bound in a freshly generated project. The same package pins setuptools<82 for Pyramid 2.1, which matters
for the pkg_resources section below.
Source: Upgrading Pyramid
Source: What’s New in Pyramid 2.1
From 1.x to 2.0
The change history lists the 2.0 items under features, deprecations and backward incompatibilities. The table merges them and names the replacement.
| Item | Kind | What to do |
|---|---|---|
|
Deprecated |
Write one class implementing |
|
Deprecated |
Use |
|
Deprecated |
Principals are gone from the request and from the policy interface. Derive principals inside your |
|
Deprecated (moved) |
Import them from |
|
Added |
|
|
Changed |
JSON ( |
|
Removed |
Use |
|
Removed |
Deprecated since 1.5; use |
|
Removed |
Use the |
|
Removed |
Use the standard library; the interface carried no functionality inside Pyramid. |
|
Removed |
Use |
Media ranges in the |
Removed |
Deprecated in 1.10 because WebOb 1.8 made them non-compliant; list concrete media types. |
|
Changed |
Only Venusian callbacks registered in the |
Second argument to predicate factories |
Changed |
It is now an |
|
Changed |
|
|
Changed |
By default one cookie without a domain is set, so subdomains no longer receive it. Set |
|
Changed |
The default execution policy no longer calls it. |
|
Changed |
No longer defined by Pyramid; WebOb’s attribute is used and is settable. |
Request properties from |
Changed |
Can be overridden by assignment ( |
|
Added |
Control the trailing slash of a route composed with a route prefix. |
|
Added |
Cache data that comes from outside the request for the duration of one request, for example the user row. |
|
Added |
Serve pre-compressed assets. See Static Assets. |
|
Added |
Tolerate requests without |
|
Added |
Documented interface for the object passed to predicate factories. |
|
Changed |
Written to |
Source: What’s New in Pyramid 2.0
Source: Pyramid Change History
From 2.0 to 2.1
| Item | Kind | What to do |
|---|---|---|
Python 3.6, 3.7, 3.8 and 3.9 |
Removed |
Upgrade the interpreter to 3.10 or newer before upgrading Pyramid. |
|
Removed |
They were removed from Python’s |
|
Added |
Only matters if you implement your own package overrides. |
Cache busters and asset overrides |
Changed |
The cache buster takes the first file that exists in the override stack, not the first override regardless of whether the file exists. |
|
Deprecated |
Point the root directory inside a real package. A non-existent package was almost always a misconfiguration, or an
attempt to create an alias for |
|
Added |
HTTP 418. |
|
Added |
|
View system hashing |
Changed |
SHA-256 replaces MD5. It is an implementation detail and not a security feature. |
|
Changed |
Replaced, but other usages remain; see the dedicated section below. |
|
Changed |
Pinned below 82 so that the |
Exception views and permissions |
Fixed |
Permissions are no longer checked on exception views, which also fixed a decoding failure when logging with
|
|
Fixed |
They returned status lines such as |
|
Fixed |
A token that cannot be encoded as Latin-1 no longer raises |
Tutorials |
Docs |
The SQLAlchemy and ZODB wiki tutorials follow the cookiecutter: |
Source: What’s New in Pyramid 2.1
Source: Pyramid Change History
Legacy to Current
When an older tutorial and the current documentation disagree, this table decides.
| Older tutorials show | Current spelling | Status in 2.1 |
|---|---|---|
|
|
Legacy calls work and warn. |
|
|
Old import works and warns. |
|
|
Legacy class works. |
|
Compute principals from |
Works only with legacy policies; with a security policy it is a one-element list holding |
|
|
Equal to |
|
|
Both work; the cookiecutter and tutorials use the identity. |
|
The same factory storing JSON-serializable values |
Pickle values fail at response time with a |
|
|
Removed. |
|
|
|
|
|
The cookiecutter generates |
|
|
Still accepted in 2.1, deprecated since 1.5; it warns. |
|
|
Removed. |
|
Standard library |
Removed. |
|
|
Removed in 2.1. |
Two entries deserve a correction of common advice. First, custom_predicates is not removed in 2.1: the argument is
still accepted on add_view and add_route and emits a DeprecationWarning that names the replacement, so code that
uses it keeps working but should move to registered predicates. Second, pkg_resources has not been removed from
Pyramid either, as the next sections show.
Migrating Authentication and Authorization
The legacy configuration of a 1.x application is two calls. Installing them in 2.1 still works, and the warning tells you to move.
import secrets
import warnings
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.config import Configurator
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
legacy = Configurator()
legacy.set_authentication_policy(AuthTktAuthenticationPolicy(secrets.token_hex(32)))
legacy.set_authorization_policy(ACLAuthorizationPolicy())
print(sorted({w.category.__name__ for w in caught}))
print(str(caught[0].message)[:68])
Source: Upgrading from Built-in Policies
The replacement is one class. In Bookshelf Shelves the identity is a small dictionary produced from the
authentication cookie, and the shelf’s ACL decides what that identity may do. The example installs the policy on a
fresh configurator and drives it with webtest.TestApp.
import secrets
from pyramid.authentication import AuthTktCookieHelper
from pyramid.authorization import ACLHelper, Allow, Authenticated, Everyone
from pyramid.httpexceptions import HTTPSeeOther
from pyramid.response import Response
from pyramid.security import remember
from webtest import TestApp
USERS = {"ana": ["group:owners"], "ben": []} # fake user store
class ShelfSecurityPolicy:
def __init__(self, secret):
self.helper = AuthTktCookieHelper(secret, hashalg="sha512", http_only=True)
def identity(self, request):
found = self.helper.identify(request)
if found is None or found["userid"] not in USERS:
return None
return {"userid": found["userid"], "principals": USERS[found["userid"]]}
def authenticated_userid(self, request):
identity = request.identity
return None if identity is None else identity["userid"]
def permits(self, request, context, permission):
principals = {Everyone}
identity = request.identity
if identity is not None:
principals.add(Authenticated)
principals.add(identity["userid"])
principals.update(identity["principals"])
return ACLHelper().permits(context, principals, permission)
def remember(self, request, userid, **kw):
return self.helper.remember(request, userid, **kw)
def forget(self, request, **kw):
return self.helper.forget(request, **kw)
class ShelfRoot:
__acl__ = [(Allow, Everyone, "view"), (Allow, "group:owners", "edit")]
def __init__(self, request):
self.request = request
def login(request):
headers = remember(request, request.params["user"])
return HTTPSeeOther(location="/", headers=headers)
def view_shelf(request):
return Response("shelf for " + str(request.identity))
def edit_shelf(request):
return Response("editing as " + request.authenticated_userid)
with Configurator(root_factory=ShelfRoot) as config:
config.set_security_policy(ShelfSecurityPolicy(secrets.token_hex(32)))
config.add_route("login", "/login")
config.add_route("shelf", "/shelf")
config.add_route("edit", "/shelf/edit")
config.add_view(login, route_name="login", request_method="POST")
config.add_view(view_shelf, route_name="shelf", permission="view")
config.add_view(edit_shelf, route_name="edit", permission="edit")
app = TestApp(config.make_wsgi_app())
print(app.get("/shelf").text)
print(app.get("/shelf/edit", status=403).status)
app.post("/login", {"user": "ben"})
print(app.get("/shelf/edit", status=403).status)
app.reset()
app.post("/login", {"user": "ana"})
print(app.get("/shelf/edit").text)
The pieces map one to one: the authentication policy’s cookie logic lives in AuthTktCookieHelper, the authorization
policy’s ACL walk lives in ACLHelper, and the callback that returned principals (groupfinder) is now the code in
identity() that decides which principals the user has. If you must keep a third-party authentication policy for a
while, the official upgrade chapter shows a shim security policy that delegates to the old policy objects; some new
features such as a rich identity are then not available. The full treatment is in
Security Policy & Authentication and
Authorization, ACLs & Permissions.
With a legacy policy installed, the new request properties behave compatibly: request.identity returns the same value
as request.authenticated_userid, request.unauthenticated_userid returns the same value as
request.authenticated_userid, and request.effective_principals is a one-element list that contains Everyone.
Code that relied on real principals therefore has to be migrated together with the policy, not before it.
The ACL names themselves moved packages. The old import still works for now and says so:
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
from pyramid.security import Allow as OldAllow, Everyone as OldEveryone
from pyramid.authorization import Allow, Everyone, ALL_PERMISSIONS, DENY_ALL
print(len(caught), "warnings")
print(caught[0].message.args[0][:80])
print(OldAllow == Allow, OldEveryone == Everyone)
Source: API: pyramid.authorization
Sessions Are JSON Now
In 1.x the signed-cookie session pickled its values, which meant that anybody who obtained the signing secret could
craft a cookie that executed code when the server unpickled it. Pyramid 2.0 changed the default serializer to JSON, and
ISession implementations only have to support JSON-serializable values. The migration for Bookshelf Shelves is
mostly mechanical: stop putting datetime objects, sets, ORM rows or custom classes in the session and store plain
strings, numbers, lists and dictionaries instead.
import datetime
import secrets
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.session import SignedCookieSessionFactory
from webtest import TestApp
def stash_bad(request):
request.session["last_visit"] = datetime.datetime(2026, 10, 7)
return Response("stored")
def stash_good(request):
request.session["last_visit"] = datetime.datetime(2026, 10, 7).isoformat()
return Response(request.session["last_visit"])
with Configurator() as config:
config.set_session_factory(SignedCookieSessionFactory(secrets.token_hex(32)))
config.add_route("bad", "/bad")
config.add_route("good", "/good")
config.add_view(stash_bad, route_name="bad")
config.add_view(stash_good, route_name="good")
sessions_app = TestApp(config.make_wsgi_app())
try:
sessions_app.get("/bad")
except TypeError as exc:
print("TypeError:", exc)
print(sessions_app.get("/good").text)
Source: Upgrading Session Serialization
The failure appears when the response is built, because that is when the factory serializes the session into the
cookie. Existing pickled cookies in browsers are not valid JSON, so the sessions they carried are lost on deploy. The official chapter shows a temporary serializer that tries JSON first and falls back to pickle
for old cookies. That fallback deserializes pickle again, so it is only acceptable while the secret is known to be
safe, should be removed after the maximum session lifetime has passed, and is not reproduced here. A session is
short-lived by design, so asking users to sign in again is normally the better trade. See
Sessions & Flash Messages for the session factory options.
Routing and Request Changes You Can Adopt
Trailing slashes with route prefixes. In 1.x a route added with an empty pattern inside an included module could
not decide whether the prefixed URL should end in a slash. In 2.0 an empty pattern together with inherit_slash=True
keeps the prefix as written:
from pyramid.config import Configurator
from pyramid.request import Request
from pyramid.response import Response
with Configurator() as config:
with config.route_prefix_context("/api/books"):
config.add_route("books", "", inherit_slash=True)
with config.route_prefix_context("/shelves/"):
config.add_route("shelves", "", inherit_slash=True)
config.add_view(lambda request: Response("ok"), route_name="books")
config.add_view(lambda request: Response("ok"), route_name="shelves")
config.commit()
request = Request.blank("/")
request.registry = config.registry
print(request.route_path("books"), request.route_path("shelves"))
Source: API: Configurator.add_route
Request properties can be assigned. A property registered with add_request_method(..., property=True) used to be
read-only. It can now be overridden on a request instance, and deleting the attribute restores the computed value.
This is mostly useful in tests:
from pyramid.config import Configurator
from pyramid.response import Response
from webtest import TestApp
with Configurator() as config:
config.add_request_method(lambda request: "computed", "mood", property=True)
config.add_route("mood", "/mood")
config.add_view(lambda request: Response(request.mood), route_name="mood")
mood_app = TestApp(config.make_wsgi_app())
print(mood_app.get("/mood").text)
from pyramid.testing import DummyRequest
fake = DummyRequest()
fake.mood = "mocked"
print(fake.mood)
Source: API: Request.set_property
Custom predicates. The supported way to add a view or route predicate is to register it. The old custom_predicates
argument still works and warns, as the example shows:
import warnings
from pyramid.config import Configurator
from pyramid.response import Response
from webtest import TestApp
class IsoDatePredicate:
def __init__(self, value, info):
self.value = value
def text(self):
return "date_param = %s" % self.value
phash = text
def __call__(self, context, request):
return ("date" in request.params) == self.value
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
legacy = Configurator()
legacy.add_route("old", "/old", custom_predicates=(lambda info, request: True,))
print([str(w.message)[:48] for w in caught])
with Configurator() as config:
config.add_view_predicate("date_param", IsoDatePredicate)
config.add_route("report", "/report")
config.add_view(lambda request: Response("with date"), route_name="report", date_param=True)
config.add_view(lambda request: Response("without date"), route_name="report", date_param=False)
predicate_app = TestApp(config.make_wsgi_app())
print(predicate_app.get("/report?date=2026-10-07").text, "/", predicate_app.get("/report").text)
The predicate factory receives an IPredicateInfo as its second argument since 2.0, which exposes the package, the
registry, the settings and the dotted-name resolver and nothing else. A factory that used other attributes of the
configurator must be rewritten. See URL Dispatch and
Views & View Configuration for predicates in context.
A new exception. pyramid.httpexceptions.HTTPImATeapot (status 418) exists since 2.1:
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPImATeapot
from webtest import TestApp
def brew(request):
raise HTTPImATeapot("This server brews tea only")
with Configurator() as config:
config.add_route("brew", "/brew")
config.add_view(brew, route_name="brew")
teapot_app = TestApp(config.make_wsgi_app())
print(teapot_app.get("/brew", status=418).status)
Source: API: pyramid.httpexceptions
Other pages. CSRF changes are in CSRF Protection (allow_no_origin, check_origin, the
removed check_csrf predicate and the status-line fix), pre-compressed static files in
Static Assets, and the exception view changes in
Errors & Exception Views.
Project Layout: from setup.py to pyproject.toml
Pyramid 1.x projects were created with pcreate -s <scaffold> <name>, installed with python setup.py develop, and
tested with python setup.py test. All three are gone from the recommended workflow. The cookiecutter generates a
pyproject.toml with the project metadata, the dependencies, a testing extra and the application entry point, and
installation and tests become:
python -m venv .venv
. .venv/bin/activate
pip install -e ".[testing]"
pytest
pserve development.ini --reload
The generated entry point is an ordinary paste.app_factory named main, so the INI files you already have keep
working. In pyproject.toml it is declared like this (a listing of a file, not executed):
[project.entry-points."paste.app_factory"]
main = "bookshelf_shelves:main"
The official page narr/project.html still describes a setup.py layout, and the wiki tutorials' text lags behind the
cookiecutter in places, so treat the files the cookiecutter generates as the reference and the narrative as background.
See Cookiecutter Projects & pserve for the generated tree and
Getting Started for a first application.
Source: Pyramid: Cookiecutters
Source: pyramid-cookiecutter-starter
The pkg_resources Situation
Some summaries say that 2.1 "removed pkg_resources`". The official notes say something more careful: 2.1 replaced
`pkg_resources in pyramid.path.DottedNameResolver, in pdistreport and in pshell, constrained setuptools to a
version below 82 "to remain compatible with required pkg_resources features", and says that work continues to remove
it completely in future releases. In practice an application on Pyramid 2.1 still imports pkg_resources indirectly,
which is why
-
installing a
setuptoolsrelease from 82 onward into the same environment can break Pyramid, so keep thesetuptools<82constraint that Pyramid declares and do not override it in a lock file; -
a recent
setuptoolsprints aUserWarning("pkg_resources is deprecated as an API") and aDeprecationWarningfor thepastenamespace package when Pyramid is imported. Those warnings are noise for your own code and must be filtered in a test suite that treats warnings as errors, as the checklist below shows; -
build tools that remove
setuptoolsfrom isolated environments need it added back as a runtime dependency.
Re-check this section against the change history on every upgrade: it is the item most likely to be out of date.
Source: What’s New in Pyramid 2.1
Upgrade Path
The official guide advises upgrading step by step through the most recent release of each minor version, running the tests each time with deprecation warnings visible, and fixing what they report before moving on. For an application that starts on Pyramid 1.10, the path below follows that advice.
Source: Upgrading Pyramid
Upgrade Checklist
-
Read the change history for every release between your current version and 2.1. The deprecations and the backward incompatibilities are listed under their own headings in each release.
-
Pin the Python version first: 3.10 to 3.14 for Pyramid 2.1. Update the CI matrix and the container base image (see Docker).
-
Upgrade one minor version at a time, and run the tests with warnings visible (
python -Wd -m pytest, or thePYTHONWARNINGS=defaultenvironment variable when runningpserveby hand if the tests cover too little). -
Replace the authentication and authorization policies with one security policy; change
from pyramid.security import Allow, Everyone, ...into imports frompyramid.authorization; replaceeffective_principals,unauthenticated_useridandprincipals_allowed_by_permissionusage. -
Make everything stored in the session JSON serializable; plan for users to sign in again after the deploy because old pickled session cookies are not valid JSON and
AuthTktCookieHelpertickets created with a differenthashalgdo not validate. -
Replace removed names:
pcreate,pyramid.compat,UnencryptedCookieSessionFactoryConfig,signed_serialize, thecheck_csrfpredicate,has_permission()-style functions inpyramid.security, media ranges inaccept,lgettext()andlngettext(). -
Replace
custom_predicateswith registered predicates even though it still works. -
Move packaging to
pyproject.tomlandpip install -e ".[testing]"; raiserequires-pythonto 3.10 or newer; keepsetuptools<82. -
Turn deprecation warnings into errors in the test configuration so that regressions are caught in CI.
-
Roll out behind your usual gradual deployment (see Kubernetes) and watch for sign-in rates after the change of hash algorithm and session format.
Warnings become test failures with a filterwarnings section in pyproject.toml. The first two entries ignore the
pkg_resources noise described above and the last one makes everything else raised from your own code fail. This is a
listing of a file, not an executed block:
[tool.pytest.ini_options]
filterwarnings = [
"error::DeprecationWarning",
"ignore:pkg_resources is deprecated:UserWarning",
"ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning",
"ignore:.*pkg_resources.*:DeprecationWarning",
]
The effect of the filter can be shown without pytest. With DeprecationWarning set to error, the legacy
configuration raises instead of warning:
import warnings
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.config import Configurator
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)
strict = Configurator()
try:
strict.set_authentication_policy(AuthTktAuthenticationPolicy("x" * 32))
except DeprecationWarning as exc:
print("blocked:", str(exc)[:60])
The literal x secret above exists only to show a warning being raised; real secrets come from the environment or from
secrets.token_hex(), never from source control.
Where Older Material Misleads
-
Tutorials that call
pcreate,setup.py develop,setup.py testorpython setup.py develop. Use a cookiecutter andpip install -e. -
Tutorials with
groupfinder,set_authentication_policyandACLAuthorizationPolicy. The concepts survive (the ACL mechanism is not deprecated); the wiring changed. -
Posts that describe sessions as pickled, or that put arbitrary objects in
request.session. -
Posts that say
pyramid_debugtoolbarorrequest.useris required. Neither is part of Pyramid core; the toolbar is a development add-on that must never be enabled in production. -
Pyramid is WSGI and synchronous. Nothing in 2.0 or 2.1 changed that: there are no async views in 2.1. The community cookbook describes an ASGI wrapping recipe (see Introduction & Architecture).
-
pkg_resourcesarticles that either say it is gone or ignore it; see the section above.
For readers coming from other frameworks, the same kind of "what moved" pages exist for Flask and Tornado; Pyramid is the framework of the three that treats deprecation most conservatively, which is why its migration is mostly a matter of warnings. Pyramid applications can be mounted next to Tornado handlers through WSGI Interoperability, and the same policy-migration ideas appear when moving between FastAPI and Django security models.