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 (ISecurityPolicy) replacing separate authentication and authorization policies; request.identity; ACL constants moved to pyramid.authorization; JSON instead of pickle in signed-cookie sessions; Python 2 dropped; pcreate scaffolds removed in favour of cookiecutters; inherit_slash, RequestLocalCache, content_encodings for static views, CSRF allow_no_origin and check_origin.

2.1 (2026-03-11)

Python 3.10 to 3.14 (3.6 to 3.9 dropped); HTTPImATeapot; SHA-256 instead of MD5 inside the view system; l*gettext() methods removed; __script__ option passed to application factories; some pkg_resources usages replaced (but not all); setuptools constrained to <82; tutorials moved to pyproject.toml and SQLAlchemy 2.0 patterns.

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

pyramid.compat (the Python 2 and 3 shims) was removed, so code that imported text_type, string_types and friends from it must import them from the standard library or from the six package.

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.

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

config.set_authentication_policy() and config.set_authorization_policy()

Deprecated

Write one class implementing ISecurityPolicy and install it with config.set_security_policy(). See Security Policy & Authentication.

AuthTktAuthenticationPolicy, SessionAuthenticationPolicy, BasicAuthAuthenticationPolicy, RemoteUserAuthenticationPolicy, RepozeWho1AuthenticationPolicy, ACLAuthorizationPolicy

Deprecated

Use AuthTktCookieHelper, SessionAuthenticationHelper, extract_http_basic_credentials(), a read of request.environ.get('REMOTE_USER') and ACLHelper. RepozeWho1AuthenticationPolicy has no equivalent.

request.effective_principals, request.unauthenticated_userid, the effective_principals view and route predicates, pyramid.security.principals_allowed_by_permission()

Deprecated

Principals are gone from the request and from the policy interface. Derive principals inside your permits() method from request.identity.

pyramid.security.Everyone, Authenticated, ALL_PERMISSIONS, DENY_ALL, Allow, Deny, ACLAllowed, ACLDenied

Deprecated (moved)

Import them from pyramid.authorization. The old import still works and emits a DeprecationWarning.

request.identity, request.is_authenticated, the is_authenticated view and route predicate

Added

identity is whatever your policy’s identity() method returns; is_authenticated is True when it is not None.

SignedCookieSessionFactory default serializer

Changed

JSON (pyramid.session.JSONSerializer) instead of pickle. Session values must be JSON serializable. pyramid.session.PickleSerializer is deprecated.

UnencryptedCookieSessionFactoryConfig, signed_serialize(), signed_deserialize()

Removed

Use SignedCookieSessionFactory.

pyramid.security.has_permission(), authenticated_userid(), unauthenticated_userid(), effective_principals()

Removed

Deprecated since 1.5; use request.has_permission(), request.authenticated_userid and so on.

pcreate and the built-in scaffolds

Removed

Use the pyramid-cookiecutter-starter cookiecutter.

pyramid.compat, pyramid.interfaces.ITemplateRenderer

Removed

Use the standard library; the interface carried no functionality inside Pyramid.

check_csrf predicate

Removed

Use config.set_default_csrf_options(require_csrf=True) and the require_csrf view option. See CSRF Protection.

Media ranges in the accept predicate of add_view and add_route

Removed

Deprecated in 1.10 because WebOb 1.8 made them non-compliant; list concrete media types.

config.scan() default categories

Changed

Only Venusian callbacks registered in the pyramid category run. Pass categories=None to find every decorator.

Second argument to predicate factories

Changed

It is now an IPredicateInfo (package, registry, settings, dotted-name resolver) instead of the configurator.

AuthTktCookieHelper default hashalg

Changed

sha512. Cookies written with the old default are not accepted, so users sign in again after the upgrade.

AuthTktCookieHelper cookie domain

Changed

By default one cookie without a domain is set, so subdomains no longer receive it. Set wild_domain explicitly if you relied on the old behaviour.

request.invoke_exception_view()

Changed

The default execution policy no longer calls it.

request.json_body

Changed

No longer defined by Pyramid; WebOb’s attribute is used and is settable.

Request properties from add_request_method(..., property=True) and request.set_property()

Changed

Can be overridden by assignment (request.foo = value), which makes mocking easier.

config.add_route(..., inherit_slash=True)

Added

Control the trailing slash of a route composed with a route prefix.

pyramid.request.RequestLocalCache

Added

Cache data that comes from outside the request for the duration of one request, for example the user row.

content_encodings on add_static_view() and static_view

Added

Serve pre-compressed assets. See Static Assets.

allow_no_origin and check_origin on set_default_csrf_options() and check_csrf_origin()

Added

Tolerate requests without Origin or Referer, or switch the origin check off.

IPredicateInfo

Added

Documented interface for the object passed to predicate factories.

pserve verbose output

Changed

Written to stderr instead of stdout.

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.

l*gettext() methods in the i18n module (lgettext, lngettext)

Removed

They were removed from Python’s gettext module in 3.11. Use gettext and ngettext. See the Pyramid i18n documentation for the translation string API.

IPackageOverrides.get_spec()

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.

add_static_view() or static_view pointing at a non-existent package

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

pyramid.httpexceptions.HTTPImATeapot

Added

HTTP 418.

__script__ option

Added

pserve, pshell, prequest, proutes, ptweens, pviews and pyramid.paster.bootstrap() pass it when loading the application. With plaster-pastedeploy it arrives in the global_conf argument of your application factory.

View system hashing

Changed

SHA-256 replaces MD5. It is an implementation detail and not a security feature.

pkg_resources in DottedNameResolver, pdistreport and pshell

Changed

Replaced, but other usages remain; see the dedicated section below.

setuptools requirement

Changed

Pinned below 82 so that the pkg_resources features Pyramid still needs remain available.

Exception views and permissions

Fixed

Permissions are no longer checked on exception views, which also fixed a decoding failure when logging with pyramid.debug_authorization enabled.

BadCSRFToken and BadCSRFOrigin

Fixed

They returned status lines such as 400 Bad CSRF Origin; they now return 400 Bad Request.

check_csrf_token of the three storage policies

Fixed

A token that cannot be encoded as Latin-1 no longer raises UnicodeEncodeError.

Tutorials

Docs

The SQLAlchemy and ZODB wiki tutorials follow the cookiecutter: pyproject.toml replaces separate pytest, coverage and setuptools files, and the SQLAlchemy tutorial uses SQLAlchemy 2.0 patterns.

Legacy to Current

When an older tutorial and the current documentation disagree, this table decides.

Older tutorials show Current spelling Status in 2.1

config.set_authentication_policy(...) plus config.set_authorization_policy(...)

config.set_security_policy(MyPolicy(...))

Legacy calls work and warn.

from pyramid.security import Allow, Everyone

from pyramid.authorization import Allow, Everyone

Old import works and warns.

ACLAuthorizationPolicy

ACLHelper().permits(context, principals, permission) inside permits()

Legacy class works.

request.effective_principals

Compute principals from request.identity in your policy

Works only with legacy policies; with a security policy it is a one-element list holding Everyone.

request.unauthenticated_userid

request.identity or the identity helper’s identify()

Equal to authenticated_userid with a security policy.

request.user set by a reified request method

request.identity, loaded once through RequestLocalCache

Both work; the cookiecutter and tutorials use the identity.

SignedCookieSessionFactory(secret) storing arbitrary Python objects

The same factory storing JSON-serializable values

Pickle values fail at response time with a TypeError.

UnencryptedCookieSessionFactoryConfig

SignedCookieSessionFactory

Removed.

pcreate -s alchemy MyProject

cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 2.1-branch

pcreate removed.

python setup.py develop and python setup.py test

pip install -e ".[testing]" and pytest

The cookiecutter generates pyproject.toml and no setup.py.

custom_predicates=(...) on add_view and add_route

config.add_view_predicate() and config.add_route_predicate()

Still accepted in 2.1, deprecated since 1.5; it warns.

check_csrf=True view predicate

require_csrf=True view option with set_default_csrf_options()

Removed.

pyramid.compat helpers

Standard library

Removed.

lgettext() and lngettext()

gettext() and ngettext()

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

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)

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)

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

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)

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)

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.

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 setuptools release from 82 onward into the same environment can break Pyramid, so keep the setuptools<82 constraint that Pyramid declares and do not override it in a lock file;

  • a recent setuptools prints a UserWarning ("pkg_resources is deprecated as an API") and a DeprecationWarning for the paste namespace 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 setuptools from 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.

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.

flowchart TD A["App on Pyramid 1.10 and Python 3.8"] --> B["Fix warnings under 1.10 with python -Wd"] B --> C["Upgrade Pyramid to 2.0.x"] C --> D["Replace policies with a security policy, move ACL imports, make sessions JSON"] D --> E["Run tests with DeprecationWarning as error"] E --> F["Upgrade Python to 3.10 or newer"] F --> G["Upgrade Pyramid to 2.1"] G --> H["Replace lgettext, fix static views and cookiecutter metadata"] H --> I["Deploy and let users sign in again"]
Figure 1. Upgrade path from Pyramid 1.10 to 2.1
Upgrade path from Pyramid 1.10 to 2.1

Source: Upgrading Pyramid

Upgrade Checklist

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

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

  3. Upgrade one minor version at a time, and run the tests with warnings visible (python -Wd -m pytest, or the PYTHONWARNINGS=default environment variable when running pserve by hand if the tests cover too little).

  4. Replace the authentication and authorization policies with one security policy; change from pyramid.security import Allow, Everyone, ... into imports from pyramid.authorization; replace effective_principals, unauthenticated_userid and principals_allowed_by_permission usage.

  5. 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 AuthTktCookieHelper tickets created with a different hashalg do not validate.

  6. Replace removed names: pcreate, pyramid.compat, UnencryptedCookieSessionFactoryConfig, signed_serialize, the check_csrf predicate, has_permission()-style functions in pyramid.security, media ranges in accept, lgettext() and lngettext().

  7. Replace custom_predicates with registered predicates even though it still works.

  8. Move packaging to pyproject.toml and pip install -e ".[testing]"; raise requires-python to 3.10 or newer; keep setuptools<82.

  9. Turn deprecation warnings into errors in the test configuration so that regressions are caught in CI.

  10. 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 test or python setup.py develop. Use a cookiecutter and pip install -e.

  • Tutorials with groupfinder, set_authentication_policy and ACLAuthorizationPolicy. 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_debugtoolbar or request.user is 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_resources articles 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.