Getting Started

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), checked on 2026-10-07. Pyramid 2.1 supports Python 3.10 to 3.14 (and PyPy); the code on this page uses Python 3.10+ syntax and was executed on Python 3.13 with Pyramid 2.1, Waitress 3.0.2 and WebTest 3.0.7. The page takes you from an empty directory to a running server, explains what each call of the first application does, shows the same application in its declarative form with @view_config and config.scan(), and ends with the pitfalls that surprise newcomers. The application is the first version of Bookshelf Shelves, the scenario that grows over the following pages of this section: public shelves owned by users, each holding books. It is the Pyramid counterpart of Getting Started with Flask, Getting Started with Tornado and Getting Started with FastAPI.

Two things set the tone for the rest of the section. Pyramid is a WSGI framework and its views are synchronous: there is no async def view in 2.1. And Pyramid is configured rather than decorated: nothing is registered by merely importing a module with a decorator, an object called the Configurator records every route, view and setting in an application registry, and only then is a WSGI application created from it.

Install Pyramid

Create a virtual environment first, as for any Python project. Virtual environments, package managers and pyproject.toml are covered in the Python Reference, in Virtual Environments & Packaging, and are not repeated here. With the standard library:

mkdir bookshelf-shelves
cd bookshelf-shelves
python3 -m venv .venv
. .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install pyramid waitress
python -c "import pyramid, waitress; print('ok')"
pip list

With uv, which creates .venv and records the resolved versions in uv.lock:

uv init bookshelf-shelves --bare
cd bookshelf-shelves
uv add pyramid waitress
uv run python -c "import pyramid, waitress; print('ok')"

The official guides use $VENV/bin/pip instead of activating the environment, so that each snippet states which environment it runs in; either style works. To pin the release, use pip install "pyramid==2.1".

What Gets Installed

pip install pyramid installs the framework and its direct dependencies. Waitress is not among them: Pyramid itself ships no HTTP server, which is why the command above names both packages.

Package Role

webob

The request and response objects. pyramid.request.Request and pyramid.response.Response extend the WebOb classes, so the WebOb documentation applies to them (the page on requests and responses returns to this).

venusian

The scanner behind config.scan() and every decorator such as @view_config.

plaster and plaster_pastedeploy

Loading of .ini files by pserve and the other command-line tools. plaster_pastedeploy brings in PasteDeploy, the file format library.

hupper

File watching and process restarts for pserve --reload.

translationstring

The message type used by Pyramid’s internationalization support.

zope.interface and zope.deprecation

Interfaces, from which the application registry is built, and deprecation warnings.

setuptools (below version 82)

Pyramid 2.1 still imports pkg_resources, so it declares the constraint setuptools<82.

The last row has a visible consequence. With a recent setuptools, importing Pyramid prints a UserWarning saying that pkg_resources is deprecated. The warning is harmless and comes from Pyramid’s dependency on that module, not from your code. The 2.1 release notes constrain setuptools<82 "to remain compatible with required pkg_resources features" and add that the work to remove pkg_resources entirely continues in future releases; 2.1 only replaced some of its usages (in DottedNameResolver and in the pdistreport and pshell commands). Do not silence the warning by upgrading setuptools past 81, because that breaks the import; if the output is too noisy, filter that one warning (for example with PYTHONWARNINGS or a filterwarnings entry in your test configuration). Source: Pyramid: What’s New in Pyramid 2.1

Optional packages come later: pyramid_jinja2 for templates, pyramid_tm and zope.sqlalchemy for transactions, pyramid_debugtoolbar for development, WebTest for functional tests (installed in the testing step below). The cookiecutter on the next page of this section installs most of them for you. Unlike Tornado or Flask, the framework itself is deliberately small, and every capability beyond routing and views is added through config.include.

Supported Platforms and Python Versions

Pyramid 2.1 is tested against Python 3.10, 3.11, 3.12, 3.13, 3.14 and PyPy (3.10-compatible) on Linux, macOS, FreeBSD and Windows. It needs no C compiler: some dependencies may try to build optional speed-ups and silently fall back to pure Python. Python 2 support ended with Pyramid 2.0, and Python 3.6 to 3.9 support ended with 2.1, so material that installs "Pyramid 1.x on Python 2.7" or runs python setup.py develop describes a different world. Source: Pyramid: Installing Pyramid, Python Versions

The Smallest Pyramid App

A Pyramid application has four ingredients: a view callable that takes a request and returns a response, a route that names a URL pattern, a view registration that connects the two, and a WSGI server that serves the application object produced by the configurator. Create hello.py:

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


def hello_world(request):
    return Response("Hello from Bookshelf Shelves")


def make_app():
    with Configurator() as config:
        config.add_route("home", "/")
        config.add_view(hello_world, route_name="home")
        return config.make_wsgi_app()

The official page builds the same program inline under if __name__ == "__main__":. Wrapping the configuration in a function that returns the WSGI application is the same code, with the advantage that tests (see below) and servers can import make_app without starting anything. Add the serving part at the bottom of the file:

# norun
from waitress import serve

if __name__ == "__main__":
    serve(make_app(), host="127.0.0.1", port=6543)

Run it with python hello.py. The process stays in the foreground until you press Ctrl+C, and Waitress does not print an access log by default. Each piece of the first block does one thing:

Piece Role

hello_world(request)

A view callable. Pyramid calls it with one argument, the request, and it must return a response object. Views can also be classes or objects with a different signature (for example (context, request)), and the views chapters of this section cover those forms.

Response("...")

pyramid.response.Response is a WebOb response. A plain string body is sent as text/html; charset=UTF-8 with a computed Content-Length; status, content_type, headers and similar attributes are settable.

with Configurator() as config

Creates the configurator and, through the context manager, makes its registry the current one while configuring (begin() / end()) and runs commit() on exit. The configurator is the API through which every route, view, setting, include and extension point is registered.

config.add_route("home", "/")

Registers a route named home for the path /. Routes are tried in the order they were added, and the first match wins. A route alone does nothing: it selects a name, and views are looked up for that name.

config.add_view(hello_world, route_name="home")

Registers hello_world as the view to call when the home route matched. Without a request_method or other predicate, it answers every method.

config.make_wsgi_app()

Commits the configuration (conflicts are detected here) and returns the router, a standard WSGI callable that holds a reference to the registry. The configuration is finished; the application object is what servers need.

waitress.serve(app, host=..., port=...)

Starts the Waitress WSGI server and blocks. Waitress is pure Python, multi-threaded (four worker threads by default, threads=) and runs on Windows as well as Unix.

The official first-application chapter serves the app with wsgiref.simple_server.make_server because it ships with Python; that server is fine for a first experiment but is not meant for real traffic. The cookiecutter project and the production pages of this section use Waitress, which is why it is introduced here.

Host and Port

waitress.serve listens on 0.0.0.0:8080 when called without arguments, that is on every network interface. The examples bind explicitly to 127.0.0.1 and to port 6543, the port used throughout the official Pyramid documentation, so that a development server is not reachable from other machines. Pass host="0.0.0.0" (or use listen, which takes a space-separated list such as "127.0.0.1:6543 [::1]:6543") only when you want it to be. host and port cannot be combined with listen. Behind a reverse proxy, further options (trusted_proxy and friends, url_scheme) matter; the deployment page of this section covers them. Source: Waitress: Arguments to waitress.serve

The waitress-serve Command

Waitress also installs a waitress-serve command. Because make_app is a factory rather than a WSGI application, pass --call so that the command calls it to get the application, and use module:object notation:

waitress-serve --listen=127.0.0.1:6543 --call hello:make_app

The command imports the module from the current directory, so run it next to hello.py. Neither this command nor python hello.py reloads the server when the source changes. The pserve command used by the cookiecutter does, through hupper; it is introduced on the next page of this section.

Calling the App

With the server running, call it from another terminal:

curl -i http://127.0.0.1:6543/
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: text/html; charset=UTF-8
Date: Wed, 07 Oct 2026 15:54:27 GMT
Server: waitress

Hello from Bookshelf Shelves

Any other path answers 404 Not Found with Pyramid’s default HTML error page, because no route matches:

curl -i http://127.0.0.1:6543/nope
HTTP/1.1 404 Not Found
Content-Length: 157
Content-Type: text/html; charset=UTF-8
Date: Wed, 07 Oct 2026 15:54:27 GMT
Server: waitress

<html>
 <head>
  <title>404 Not Found</title>
 </head>
 <body>
  <h1>404 Not Found</h1>
  The resource could not be found.<br/><br/>
/nope


 </body>
</html>

A browser at http://127.0.0.1:6543/ shows the greeting as well. HTTPie (http :6543/) works the same way.

Testing the First App

Because make_app() returns a WSGI application, the application can be called without a server. WebTest wraps a WSGI app in a TestApp that sends requests in-process and returns parsed responses; it is the tool the official tutorials use for functional tests. Install it with pip install webtest:

from webtest import TestApp

testapp = TestApp(make_app())

response = testapp.get("/")
assert response.status_code == 200
assert response.text == "Hello from Bookshelf Shelves"
assert response.content_type == "text/html"

assert testapp.get("/nope", status=404).status_code == 404
print(response.status, response.content_type, response.text)

TestApp.get raises an error for any status outside 2xx and 3xx unless you pass status= with the expected code (or "*" for any), which is why the 404 call above states what it expects. The testing page covers unit tests with pyramid.testing, integration tests and the cookiecutter’s pytest fixtures; one short unit test is shown below.

The First Bookshelf Shelves View

A route pattern can contain placeholders. Add a second route, /shelves/{shelf}, whose view reads the placeholder from request.matchdict, and use the built-in json renderer so that the view returns a plain dictionary instead of a Response. The shelves live in a module-level dictionary for now; real storage arrives with the database pages, and note that Waitress serves requests on several threads, so shared mutable state like this is only acceptable for a read-only demo.

from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPNotFound
from pyramid.response import Response

SHELVES = {
    "ada": ["Dune", "Hyperion"],
    "grace": ["The Dispossessed"],
}


def home(request):
    return Response("Hello from Bookshelf Shelves")


def shelf_detail(request):
    owner = request.matchdict["shelf"]
    if owner not in SHELVES:
        raise HTTPNotFound(f"No shelf named {owner}")
    return {"shelf": owner, "books": SHELVES[owner]}


def make_app():
    with Configurator() as config:
        config.add_route("home", "/")
        config.add_view(home, route_name="home")
        config.add_route("shelf", "/shelves/{shelf}")
        config.add_view(shelf_detail, route_name="shelf", renderer="json")
        return config.make_wsgi_app()

Three Pyramid ideas appear in this small program:

  • request.matchdict holds the values captured by the route pattern, as strings, already URL-decoded. A pattern such as /shelves/{shelf} matches one path segment; the URL dispatch page of this section covers regular expressions, remainders and route predicates.

  • A renderer turns a return value into a response.* With renderer="json" the view returns data and Pyramid serializes it, sets the application/json content type and builds the response. A view that returns a Response bypasses the renderer. Without any renderer, a returned dictionary is an error (see the pitfalls).

  • HTTP errors are exceptions. HTTPNotFound and its siblings in pyramid.httpexceptions are both exception classes and response classes: raising one produces the error response, and the message becomes part of the response body. The errors page of this section covers exception views, which let you customize them. Source: Pyramid: Renderers

Test it the same way as before:

testapp = TestApp(make_app())

response = testapp.get("/shelves/ada")
assert response.status_code == 200
assert response.content_type == "application/json"
assert response.json == {"shelf": "ada", "books": ["Dune", "Hyperion"]}

missing = testapp.get("/shelves/nobody", status=404)
assert "No shelf named nobody" in missing.text
print(response.json)

The equivalent calls with curl:

curl -i http://127.0.0.1:6543/shelves/ada
curl -i http://127.0.0.1:6543/shelves/nobody
HTTP/1.1 200 OK
Content-Type: application/json
...

{"shelf": "ada", "books": ["Dune", "Hyperion"]}

The second request answers 404 Not Found with the default error page, including the text No shelf named nobody. Source: Pyramid: Renderers, JSON Renderer

A Unit Test Without a Server

A view is an ordinary function, so it can be tested without any WSGI machinery. pyramid.testing.DummyRequest builds a minimal request object that accepts the attributes the view needs:

from pyramid import testing

request = testing.DummyRequest(matchdict={"shelf": "grace"})
assert shelf_detail(request) == {"shelf": "grace", "books": ["The Dispossessed"]}

try:
    shelf_detail(testing.DummyRequest(matchdict={"shelf": "nobody"}))
except HTTPNotFound as error:
    print("raised", error.status)

Unit tests check the view logic; functional tests with TestApp check that routes, views and renderers are wired together. A project normally has both, and the testing page of this section builds a full pyramid of them.

The Same App with view_config and config.scan

Calling add_view for every view in one place does not scale to hundreds of views across many modules. Pyramid offers a declarative way to register the same views: decorate each view with @view_config, then call config.scan() once. The decorator records the arguments and nothing else; the scan imports the modules of a package, finds the recorded declarations through Venusian and performs the same add_view calls you would have written. Routes are not decorated: add_route stays in the configuration code, because the route order is significant and is easier to read in one place.

# norun
from pyramid.config import Configurator
from pyramid.httpexceptions import HTTPNotFound
from pyramid.response import Response
from pyramid.view import view_config

SHELVES = {
    "ada": ["Dune", "Hyperion"],
    "grace": ["The Dispossessed"],
}


@view_config(route_name="home")
def home(request):
    return Response("Hello from Bookshelf Shelves")


@view_config(route_name="shelf", renderer="json")
def shelf_detail(request):
    owner = request.matchdict["shelf"]
    if owner not in SHELVES:
        raise HTTPNotFound(f"No shelf named {owner}")
    return {"shelf": owner, "books": SHELVES[owner]}


def make_app():
    with Configurator() as config:
        config.add_route("home", "/")
        config.add_route("shelf", "/shelves/{shelf}")
        config.scan()
        return config.make_wsgi_app()

config.scan() without an argument scans the package of the code that called it. For a single module such as hello.py that is the module itself; in a real project it is the application package, and every module below it is imported and examined. scan accepts a package or module object or a dotted name (config.scan("bookshelf.views")), categories to restrict which kinds of declarations are processed (the default is the pyramid category), ignore to skip modules, packages or objects (a string or a callable, or a list of them; useful for test modules) and onerror for the Venusian error callback. The behaviour is otherwise identical to the imperative version: the same TestApp assertions pass for both. The configuration page of this section explains how scanning, conflicts and config.include fit together. Source: Pyramid: API, Configurator.scan

The two styles can be mixed freely in one application. A common arrangement is to keep infrastructure, such as routes, settings and config.include calls, imperative, and to declare views with @view_config next to the code that implements them. The same decorator accepts every add_view argument (request_method, renderer, permission, context and so on), and @view_defaults shares arguments across the methods of a view class; both are covered by the views page of this section.

What Happens on One Request

sequenceDiagram participant C as Client (curl) participant W as Waitress participant R as Router (the WSGI app) participant V as View callable C->>W: GET /shelves/ada W->>R: WSGI call with the environ R->>R: match routes in order, first match wins R->>R: look up the view registered for route shelf R->>V: shelf_detail(request) V-->>R: dictionary R->>R: json renderer builds the Response R-->>W: status, headers and body W-->>C: 200 OK, application/json
Figure 1. Waitress hands the request to the router, which matches a route, finds the view registered for it and renders its result

The introduction page of this section places this flow inside the complete request pipeline, with tweens and traversal.

Compared with Flask, Tornado and FastAPI

Pyramid Flask Tornado FastAPI

Hello World entry

Configurator, then make_wsgi_app()

Flask(__name__) is both the registry and the WSGI app

Application([...]) and app.listen

FastAPI() is the ASGI app

Registering a view

add_route and add_view, or @view_config plus scan()

@app.route takes effect at import time

A handler class listed in the routing table

@app.get takes effect at import time

Server

any WSGI server; Waitress by default

any WSGI server; the development server for experiments

its own, on asyncio

any ASGI server, for example Uvicorn

Async views

none in 2.1

supported with limits: each async def view runs on an event loop of its own

native

native

The row about registration is the real difference. Flask and FastAPI mutate the application object while a module is imported, so what the application contains depends on what was imported. Pyramid separates declaring from applying: decorators and add_* calls collect configuration actions, which are checked for conflicts and committed together. The cost is one more concept to learn; the gain is that a configuration can be included, overridden and tested as a unit, which the configuration page of this section develops.

Pitfalls

Symptom Cause and fix

ValueError: Could not convert return value of the view callable ... into a response object

The view returned a string, a dictionary or None and no renderer is configured for it. Return a Response, or add renderer="json" or renderer="string" to the view registration.

404 Not Found for a URL that has a @view_config view

The decorator only records the declaration. Call config.scan() on a package that contains the module, and check that the scanned package really imports it.

ConfigurationExecutionError ... No route named x found for view registration

route_name refers to a route that was never added with add_route. Check the spelling; the order of add_route and add_view calls does not matter, because both are applied when the configuration is committed.

A view answers POST and DELETE that you meant only for GET

add_view and @view_config accept every method unless request_method is given. Use request_method="GET" (which also answers HEAD) or a tuple of methods. A request whose method no view accepts gets 404 Not Found.

A route never matches because an earlier one captured the path

Routes are tried in the order they were added, and the first match wins. Add the more specific route first.

The server is reachable from other machines

waitress.serve defaults to 0.0.0.0. Pass host="127.0.0.1" for development.

UserWarning: pkg_resources is deprecated as an API

Pyramid 2.1 still imports pkg_resources and caps setuptools below 82. Filter the warning; do not upgrade setuptools past the cap.

python hello.py serves stale code after an edit

Neither serve() nor waitress-serve reloads. Restart it, or use pserve --reload as introduced on the next page of this section.

The first and the third row are easy to reproduce: the first is the error raised while the response is built, and the third the error raised when the configuration is committed:

from pyramid.exceptions import ConfigurationExecutionError


def returns_a_string(request):
    return "just a string"


with Configurator() as config:
    config.add_route("bad", "/bad")
    config.add_view(returns_a_string, route_name="bad")
    bad_app = TestApp(config.make_wsgi_app())

try:
    bad_app.get("/bad")
except ValueError as error:
    print("ValueError:", str(error)[:60])

try:
    with Configurator() as config:
        config.add_view(returns_a_string, route_name="no-such-route")
        config.make_wsgi_app()
except ConfigurationExecutionError as error:
    print("ConfigurationExecutionError:", "No route named" in str(error))

Source: Pyramid: Views

The second row, a decorated view that is never scanned, answers 404 because nothing registered it:

# norun
from pyramid.config import Configurator
from pyramid.view import view_config
from webtest import TestApp


@view_config(route_name="decorated", renderer="string")
def decorated(request):
    return "never registered"


with Configurator() as config:
    config.add_route("decorated", "/decorated")
    # no config.scan(): the decorator was never applied
    unscanned = TestApp(config.make_wsgi_app())

assert unscanned.get("/decorated", status=404).status_code == 404

When a view you expect is not found, turn on the pyramid.debug_notfound setting: the 404 response then explains which predicates and which context failed to match. Settings are passed to the configurator (or, in a real project, come from the .ini file, which is the subject of the settings page of this section):

with Configurator(settings={"pyramid.debug_notfound": True}) as config:
    config.add_route("decorated", "/decorated")
    debug_app = TestApp(config.make_wsgi_app())

explanation = debug_app.get("/decorated", status=404)
assert "debug_notfound of url" in explanation.text
print(config.registry.settings["debug_notfound"])

The setting also exists as the PYRAMID_DEBUG_NOTFOUND environment variable. It prints the details to the response and to stderr, so enable it only while debugging and never in production.

Security Notes for This Page

  • The examples contain no secrets. Later pages that need a signing key for sessions or cookies read it from the environment or the .ini file and never hardcode it.

  • Bind development servers to 127.0.0.1. A server on 0.0.0.0 with pyramid.debug_notfound or the debug toolbar enabled exposes internal details to the whole network.

  • The debug toolbar presented on the logging page of this section is a development-only add-on whose interactive traceback console executes code in the server process; keep it out of production and restrict it with its debugtoolbar.hosts allow-list.

  • Return HTTPNotFound for missing resources rather than echoing arbitrary input into pages; the messages passed to the HTTP exception classes become part of the response body.

Reading Companions: Quick Tour and Quick Tutorial

The official documentation has two guides that pair well with this page. They are listed here by the topics they cover and the pages of this section where the same topics are treated in depth:

Official guide What it does Where this section continues

Quick Tour of Pyramid

A fast walk through a single-file app, requests and responses, views, routing, templates, static assets, view classes, a cookiecutter project, pserve, .ini files, the debug toolbar, testing, JSON, logging, sessions, databases and forms, with the configuration of each one shown briefly.

Each topic has its own page, starting with the cookiecutter and pserve page and the configuration page.

Quick Tutorial for Pyramid

Twenty-one numbered steps, from "01: Single-File Web Applications" to "21: Protecting Resources With Authorization"; each step is a small project you can run. The prelude covers cookiecutters.

This page corresponds to step 01. The routing, views, renderers, sessions, forms, database, authentication and authorization pages follow the later steps with the Bookshelf Shelves scenario instead.

Creating Your First Pyramid Application

The reference-style walk through the hello world program that this page builds on.

The configuration page goes deeper into what the configurator does.

Where Older Tutorials Differ

  • Servers. The official examples use wsgiref, and many older ones use paster serve or pcreate. The cookiecutter and pserve (next page) replaced the scaffolds; Waitress is the maintained, production-capable default.

  • Installing in development mode. Older guides use python setup.py develop. The 2.1 generation of the tutorials uses pip install -e ".[testing]" and a pyproject.toml, even though the narrative chapter on creating a project still mentions setup.py in places; the cookiecutter page documents what the generated project really contains.

  • Python 2 and object base classes. class MyView(object) and from __future__ imports are leftovers from Pyramid 1.x. Pyramid 2.x is Python 3 only.

  • Security. Everything about authentication and authorization changed in Pyramid 2.0 (a single security policy); the security pages of this section describe the current API, and the migration page lists the old names.

Where to Go Next

The next page of this section generates a full project with the Pyramid cookiecutter and starts it with pserve. After that the configuration page explains what the Configurator does between add_route and make_wsgi_app, and the routing and views pages grow the two views of this page into the Bookshelf Shelves API.