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
Source: Pyramid: Installing Pyramid
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')"
Source: Pyramid: Quick Tour of Pyramid
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 |
|---|---|
|
The request and response objects. |
|
The scanner behind |
|
Loading of |
|
File watching and process restarts for |
|
The message type used by Pyramid’s internationalization support. |
|
Interfaces, from which the application registry is built, and deprecation warnings. |
|
Pyramid 2.1 still imports |
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 |
|---|---|
|
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 |
|
|
|
Creates the configurator and, through the context manager, makes its registry the current one while configuring
( |
|
Registers a route named |
|
Registers |
|
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. |
|
Starts the Waitress WSGI server and blocks. Waitress is pure Python, multi-threaded (four worker threads by default,
|
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
Source: Waitress: Using waitress-serve
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
Source: Pyramid: Quick Tour of Pyramid
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()
Source: Pyramid: URL Dispatch
Three Pyramid ideas appear in this small program:
-
request.matchdictholds 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 theapplication/jsoncontent type and builds the response. A view that returns aResponsebypasses the renderer. Without any renderer, a returned dictionary is an error (see the pitfalls). -
HTTP errors are exceptions.
HTTPNotFoundand its siblings inpyramid.httpexceptionsare 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)
Source: WebTest: Documentation
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()
Source: Pyramid: View Configuration
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
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 |
|
|
|
|
Registering a view |
|
|
A handler class listed in the routing table |
|
Server |
any WSGI server; Waitress by default |
any WSGI server; the development server for experiments |
its own, on |
any ASGI server, for example Uvicorn |
Async views |
none in 2.1 |
supported with limits: each |
native |
native |
Source: Pyramid: Pyramid Design Defense
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 |
|---|---|
|
The view returned a string, a dictionary or |
|
The decorator only records the declaration. Call |
|
|
A view answers |
|
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 |
|
|
Pyramid 2.1 still imports |
|
Neither |
Source: Pyramid: View Configuration
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
Source: Pyramid: View Configuration
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
.inifile and never hardcode it. -
Bind development servers to
127.0.0.1. A server on0.0.0.0withpyramid.debug_notfoundor 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.hostsallow-list. -
Return
HTTPNotFoundfor 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, |
Each topic has its own page, starting with the cookiecutter and |
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 usepaster serveorpcreate. The cookiecutter andpserve(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 usespip install -e ".[testing]"and apyproject.toml, even though the narrative chapter on creating a project still mentionssetup.pyin places; the cookiecutter page documents what the generated project really contains. -
Python 2 and
objectbase classes.class MyView(object)andfrom __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.