Cookiecutter Projects & pserve
|
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, Python 3.10 or newer), the 2.1-branch of
pyramid-cookiecutter-starter (commit f6f9744, 2026-02-25), cookiecutter 2.7.1, plaster_pastedeploy 1.0.1,
PasteDeploy 3.1.0, hupper 1.12.1 and Waitress 3.0.2, checked on 2026-10-07; every Python block was executed on Python
3.13 against a project generated with the answers shown below. A single-file application is the right way to learn
how Pyramid works, but nobody ships one. Real applications are Python packages that can be installed, tested and
started by name, and Pyramid does not generate such a package itself: since the old pcreate scaffolds were removed,
the supported starting point is a cookiecutter template maintained by the Pylons Project. This page generates the
project that becomes Bookshelf Shelves, the scenario of this section (public shelves of books addressed by
traversal, with a JSON API, SQLAlchemy and security added in the following pages), explains every file it creates,
and follows pserve development.ini from the shell prompt to the first HTTP response. It is the Pyramid counterpart
of Application Factory & Project Layout in Flask,
and of the project start-up pages of FastAPI and
Tornado. Django’s startproject plays the role described here for
Django.
Why a Project Instead of a Single File
The single-file application of Getting Started builds a Configurator, adds a route and a view, calls
make_wsgi_app() and hands the result to Waitress in the same script. That script has three limits that the project
layout removes:
-
No packaging. The code cannot be installed, so other tools cannot import it by name. Everything that locates the application by name (the
.inifile loader,pshell,proutes, a production WSGI server) needs an importable package and an entry point that names the function building the application. -
No separation of configuration from code. Server address, logging, debugging switches and template reloading are deployment concerns. A project keeps them in
.inifiles (development.ini,production.ini,testing.ini) that the same code reads at start-up, so the code does not change between machines. -
No standard commands. With a project installed,
pserve,pshell,proutes,pviews,prequestandptweensall find the application through the same.inifile, andpytestfinds the same configuration throughtesting.ini.
The official documentation names the result a project: a directory that contains at least one Python package and the files used to describe, run and test it. Pyramid itself ships only the framework; the project is produced by a cookiecutter template.
Generating the Project
Install cookiecutter
cookiecutter is a general command-line tool that renders a project from a template repository by asking questions. It is not a Pyramid dependency, so install it once, either globally in an isolated environment or in a throw-away one:
python3 -m venv .cc-env
. .cc-env/bin/activate # Windows: .cc-env\Scripts\activate
pip install cookiecutter
cookiecutter --version
Source: cookiecutter: Installation
Virtual environments and package managers are covered in the Python Reference, in
Virtual Environments & Packaging, and are
not repeated here. pipx install cookiecutter and uvx cookiecutter ... work as well, because the tool needs no
connection to the project it generates.
Run the Pyramid Template
The official command pins the template branch that matches the framework release:
cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 2.1-branch
gh: is cookiecutter’s abbreviation for https://github.com/, so the command clones
Pylons/pyramid-cookiecutter-starter into ~/.cookiecutters/ and reads its cookiecutter.json. --checkout
selects a branch, tag or commit after the clone. The repository keeps one branch per Pyramid series (the branch list
at the time of writing includes 1.8-branch, 1.9-branch, 1.10-branch, 2.0-branch and 2.1-branch, besides the
default latest), and the branch decides which Pyramid APIs the generated code uses. Pin the branch that matches the
Pyramid version you intend to run; a project generated from an old branch carries the older idioms (for example the
setup.py-based packaging of earlier series).
If the template was cloned before, cookiecutter first asks whether it may delete and re-clone the cached copy; answer
yes so that --checkout is applied to a fresh clone. It then asks four questions. The answers used throughout this
section are shown after each prompt:
You've cloned ~/.cookiecutters/pyramid-cookiecutter-starter before.
Is it okay to delete and re-clone it? [yes]: yes
project_name [Pyramid Scaffold]: Bookshelf Shelves
repo_name [bookshelf_shelves]: bookshelf_shelves
Select template_language:
1 - jinja2
2 - chameleon
3 - mako
Choose from 1, 2, 3 [1]: 1
Select backend:
1 - none
2 - sqlalchemy
3 - zodb
Choose from 1, 2, 3 [1]: 1
The same project can be generated without a terminal dialogue, which is what scripts and CI jobs should do. Values are
passed as name=value pairs after the template and --no-input accepts every default for the ones not given. The
command below was run against the cloned 2.1-branch and produces exactly the project used on this page:
cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 2.1-branch \
--no-input project_name="Bookshelf Shelves" template_language=jinja2 backend=none
The Four Prompts
| Prompt | Choices or default | Effect |
|---|---|---|
|
Free text, default |
Human-readable name. It becomes the |
|
Default derived from |
Name of the project directory, of the Python package inside it, of the distribution ( |
|
|
Selects the package added to the dependencies ( |
|
|
Selects the persistence layer and, with it, how URLs reach code: |
The three backends are the real choice. none produces the smallest project, the one explained here, and is the right
base for the JSON API of Bookshelf Shelves when the data lives elsewhere. sqlalchemy is the base of the data pages
that follow in this section and adds a database script, an Alembic folder and the transaction packages. zodb is the
natural base for the traversal-based /shelves/... part of the scenario. The sections
below describe what each adds; the dedicated pages go deeper.
What the Template Does While Generating
Two hooks in the template repository frame the rendering. A pre-generation hook checks that repo_name matches the
regular expression ^[_a-zA-Z$]` and exits with an error otherwise, so a name such as `bookshelf-shelves`
or `my.shelves` is rejected before anything is written. The template then renders every file; the files of the three
backends and the three template languages are all present in the template, with prefixes `base_`, `sqlalchemy_` and
`zodb_`. A post-generation hook keeps only the files for the chosen template language and backend, strips the prefix
from the surviving names, and prints the commands to run next. The `+static/* files are copied without rendering.
The Generated Layout
For template_language=jinja2 and backend=none the command creates this project (static images omitted):
bookshelf_shelves/
├── .gitignore
├── MANIFEST.in
├── README.md
├── development.ini
├── production.ini
├── testing.ini
├── pyproject.toml
├── bookshelf_shelves/
│ ├── __init__.py # main(global_config, **settings)
│ ├── routes.py # includeme(config): static view and routes
│ ├── static/ # pyramid.png, pyramid-16x16.png, theme.css
│ ├── templates/ # layout.jinja2, mytemplate.jinja2, 404.jinja2
│ └── views/
│ ├── __init__.py
│ ├── default.py # my_view for the home route
│ └── notfound.py # not-found view
└── tests/
├── __init__.py
├── conftest.py # pytest fixtures
├── test_functional.py # WebTest tests
└── test_views.py # unit tests
Source: Pyramid: The Project Structure
| File or folder | Purpose |
|---|---|
|
Packaging metadata and dependencies (setuptools build backend), the |
|
PasteDeploy file for working on the project: template reloading on, the debug toolbar included, listening on
|
|
Same structure with the debug toolbar absent, template reloading and all debug flags off, and logging at |
|
Same structure with the toolbar and reloading off (faster tests) and development-style logging; read by
|
|
The application factory |
|
|
|
View callables, one module per concern; |
|
|
|
Images and the CSS served at |
|
Two unit tests of the views and two functional tests that drive the full application through WebTest. |
|
Tells setuptools which non-Python files ( |
|
Generated getting-started steps for the project. |
Source: Pyramid: The Project Structure
The project directory and the package inside it share a name. That is a convention of the template, not a Pyramid
rule: the project is the unit you install and deploy, and the package is the importable code. Keep the
distinction in mind when a command takes a distribution name (egg:bookshelf_shelves) and when it takes a module
(bookshelf_shelves:main).
pyproject.toml
This is the generated packaging file, abridged to what matters (the classifiers and keywords are cut):
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
version = "0.0"
name = "bookshelf_shelves"
description = "Bookshelf Shelves"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"plaster_pastedeploy",
"pyramid",
"pyramid_jinja2",
"pyramid_debugtoolbar",
"waitress",
]
[project.optional-dependencies]
testing = [
"WebTest",
"pytest",
"pytest-cov",
]
[project.entry-points."paste.app_factory"]
main = "bookshelf_shelves:main"
[tool.setuptools.packages.find]
exclude = ["tests"]
[tool.pytest.ini_options]
addopts = "--strict-markers"
testpaths = [
"bookshelf_shelves",
"tests",
]
Four lines deserve a closer look:
-
plaster_pastedeployis the plaster loader that understands PasteDeploy-style.inifiles. Without itpservecannot readdevelopment.ini. Pyramid 2.1 already requires it (together withplaster,hupperandsetuptools<82), so the explicit line only documents that the project relies on it. -
pyramid_debugtoolbaris a normal runtime dependency, not a development extra. Installing the project in production therefore installs the toolbar too. It is harmless as long asproduction.inidoes not include it (see the security note below), but if you prefer a leaner production install, move it to adevextra and keep it inpyramid.includesofdevelopment.inionly. -
requires-python = ">=3.8"is stale. Pyramid 2.1 itself requires Python 3.10 or newer, so a pip run on Python 3.9 fails when it resolvespyramid, not at the line that looks wrong. Raise the project’s own bound to>=3.10so the metadata tells the truth. -
The
[project.entry-points."paste.app_factory"]table is what makesuse = egg:bookshelf_shelveswork. It registers the callablebookshelf_shelves:mainunder the namemainin the grouppaste.app_factory.
The testing extra is what pip install -e ".[testing]" installs on top of the runtime dependencies.
Where the Narrative Documentation Disagrees With the Template
The narrative chapter Creating a Pyramid Project in the 2.1 documentation still describes the previous packaging
generation: a setup.py, README.txt, CHANGES.txt, pytest.ini and .coveragerc, installation with pip install -e .
after upgrading pip and setuptools, and sample output of an older Pyramid release. The 2.1-branch template
generates none of those files. It produces a single pyproject.toml (with the pytest and coverage settings inside it),
a README.md, and no setup.py and no CHANGES.txt. The tutorials of the same documentation set already follow the
pyproject.toml layout. When the two disagree, trust the files the template actually writes, and use the narrative
chapter for the concepts, which have not changed. Other differences worth knowing when comparing with older material:
| Older tutorials and the narrative chapter | The 2.1 template | Consequence |
|---|---|---|
|
|
The |
|
|
Running |
|
|
Same information, standard metadata format. |
|
|
Tests are collected from |
Output |
Two |
|
What the Other Backends and Template Languages Add
Generating with backend=sqlalchemy instead of none (same other answers) changes the project like this, as
observed from the generated files:
-
pyproject.tomlgains the dependenciesalembic,pyramid_retry,pyramid_tm,SQLAlchemy,transactionandzope.sqlalchemy, and a[project.scripts]entryinitialize_bookshelf_shelves_dbthat points atbookshelf_shelves.scripts.initialize_db:main. -
The package gains
models/(meta.py,mymodel.py), analembic/folder with aversions/directory,scripts/initialize_db.pyandpshell.py;mainadditionally callsconfig.include('.models'), which is where the transaction manager and the database session are wired. -
The
.inifiles gainsqlalchemy.url = sqlite:///%(here)s/bookshelf_shelves.sqlite,retry.attempts = 3, a[pshell]section with asetuphook, an[alembic]section (script_locationandfile_template) and logger sections forsqlalchemyandalembic.%(here)sis expanded by PasteDeploy to the directory of the.inifile. -
The post-generation message adds the Alembic revision and upgrade commands and the initialisation script.
Generating with backend=zodb adds pyramid_retry, pyramid_tm, pyramid_zodbconn, transaction and ZODB,
a models/ package whose MyModel is a ZODB PersistentMapping, and a root_factory in main built on
pyramid_zodbconn.get_connection, so the URL is resolved by traversal over the stored objects rather than by route
patterns. The .ini files gain zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000, retry.attempts
and a [pshell] section, and the default view is registered with context=MyModel instead of a route name.
Choosing chameleon or mako swaps the dependency line (pyramid_chameleon, pyramid_mako), the
config.include('pyramid_...') line in main, and the three template files for .pt or .mako equivalents; nothing
else in the project changes. The remaining pages of this section use Jinja2, the template language of the
scenario, and cover SQLAlchemy and ZODB in their own pages (the data pages that follow in this section).
The Entry Point: main(global_config, **settings)
Open the generated bookshelf_shelves/__init__.py. This tiny module is where a Pyramid project meets the tools that
start it:
from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
config.include('pyramid_jinja2')
config.include('.routes')
config.scan()
return config.make_wsgi_app()
Source: Pyramid: Startup
main follows the PasteDeploy paste.app_factory protocol: a callable that receives a dictionary of global
configuration and keyword arguments, and returns a WSGI application. The two parameters carry different data:
| Parameter | Content |
|---|---|
|
A dictionary built from the |
|
Every key and value of the |
Source: Pyramid: Startup and Pyramid: What’s New in Pyramid 2.1
Line by line:
-
with Configurator(settings=settings) as config:builds the configurator and, as a context manager, makes it the active configurator for the block and commits the configuration when the block ends without an exception. The commit is the point at which conflicts between configuration declarations are detected and the declarations are applied to the registry. If the block raises, nothing is committed and the exception propagates. -
config.include('pyramid_jinja2')includes another package’s configuration. The template package registers the.jinja2renderer through its ownincludemefunction. -
config.include('.routes')includes the project’s ownroutes.py. The leading dot is a relative reference resolved against the package that callsinclude, so the call keeps working when the project is renamed. -
config.scan()imports every module in the calling package and registers the decorated views found in them (@view_config,@notfound_view_config), which is whyviews/default.pyis picked up without being mentioned. -
config.make_wsgi_app()finishes the registry, emits theApplicationCreatedevent and returns the router, a WSGI application.
The helper the project includes is equally small:
def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
Source: Pyramid: The Project Structure
An includeme(config) function is the standard way to split configuration into pieces; configuration, includes and
the conflict rules are covered in detail in Configuration & the Configurator.
The .ini file never mentions bookshelf_shelves:main directly. Instead use = egg:bookshelf_shelves names the
distribution, and the entry point registered in pyproject.toml resolves the rest. This block asks the installed
metadata the same question PasteDeploy asks:
from importlib.metadata import entry_points
(ep,) = [
e for e in entry_points(group="paste.app_factory")
if e.dist.name == "bookshelf_shelves" and e.name == "main"
]
print(ep.name, "->", ep.value)
print(ep.load().__name__)
main -> bookshelf_shelves:main
main
This is why a freshly renamed or newly installed project may "not be found": the entry point lives in the installed
metadata, not in the source tree. If you change the project name, the module name or the entry-point table, run
pip install -e ".[testing]" again so the metadata is regenerated.
Calling main yourself, without pserve, is straightforward and is exactly what the generated tests/conftest.py
does. The settings come from the same .ini file:
import webtest
from pyramid.paster import get_appsettings
from bookshelf_shelves import main
settings = get_appsettings("testing.ini")
app = main({}, **settings)
print(type(app).__name__)
registry_settings = app.registry.settings
print(settings["pyramid.reload_templates"], registry_settings["pyramid.reload_templates"])
print(registry_settings["reload_templates"], registry_settings["default_locale_name"])
testapp = webtest.TestApp(app, extra_environ={"HTTP_HOST": "example.com"})
print(testapp.get("/").status, testapp.get("/nope", status=404).status)
Source: Pyramid API: pyramid.paster
Router
false False
False en
200 OK 404 Not Found
The first value of the second line is the raw string from the file, the second the converted boolean that the
framework keeps in app.registry.settings (the same mapping a view reads as request.registry.settings); the
converted settings are also available under their short names, as the third line shows. How settings are interpreted is the subject of Settings, Environment & .ini Files.
Installing the Project
The README generated into the project gives the steps. The project is installed into a virtual environment in editable mode, so edits to the source take effect without reinstalling:
cd bookshelf_shelves
python3 -m venv env
env/bin/pip install --upgrade pip
env/bin/pip install -e ".[testing]"
pip install -e ".[testing]" installs the runtime dependencies from dependencies, the testing extra (WebTest,
pytest, pytest-cov) and the project itself. The quotes matter in shells such as zsh, where square brackets are glob
characters. Installing the project is not optional: it registers the paste.app_factory entry point and puts the
package on the import path, and both are required by pserve. The installation also adds the Pyramid console scripts
(pserve, pshell, proutes, pviews, prequest, ptweens, pdistreport) to the environment’s bin directory.
Run the generated tests to confirm the project works before changing anything:
env/bin/pytest -q
....
4 passed
(timing and any deprecation warnings omitted) Source: Pyramid: Running the Tests for Your Application
The four tests are the two unit tests of tests/test_views.py (they call my_view and the not-found view with a real
request obtained from pyramid.scripting.prepare) and the two functional tests of tests/test_functional.py (a
GET / that must answer 200 and a GET /badurl that must answer 404). Both groups use the fixtures of
tests/conftest.py: app builds the application from testing.ini once per session, testapp wraps it in a
webtest.TestApp with HTTP_HOST set to example.com, app_request is a real request built from the application
registry, and dummy_request and dummy_config are lightweight alternatives from pyramid.testing. --ini selects
another .ini file. Add --cov for coverage, since pytest-cov is installed and the coverage source is set in
pyproject.toml. Unit, integration and functional testing are the subject of the testing page of this section.
Running the Project With pserve
With the project installed, the application is started by pointing pserve at one of the .ini files:
env/bin/pserve development.ini
Starting server in PID 43537.
2026-10-07 17:54:10,870 INFO [waitress:449][MainThread] Serving on http://[::1]:6543
2026-10-07 17:54:10,870 INFO [waitress:449][MainThread] Serving on http://127.0.0.1:6543
The command stays in the foreground and serves until it receives Ctrl-C. Browse to http://localhost:6543/ or use
curl:
curl -i http://localhost:6543/
HTTP/1.1 200 OK
Content-Length: 3599
Content-Type: text/html; charset=UTF-8
Date: ...
Server: waitress
Source: Pyramid: Viewing the Application
The first line, Starting server in PID ..., is printed by pserve itself. The Serving on lines come from Waitress
through the logging configuration of the .ini file (the waitress logger inherits the root logger, set to INFO in
development.ini); in production.ini the root level is WARN, so the lines do not appear. The old documentation
shows a single Serving on http://localhost:6543 line; current Waitress prints one line per address it binds, and
localhost resolves to both the IPv6 and the IPv4 loopback address.
The Command-Line Options
| Option | Meaning |
|---|---|
|
The configuration file, optionally followed by |
|
Zero or more |
|
Load the application section with this name (default |
|
Use the |
|
Use a server by its entry-point name from |
|
Restart the server when watched files change (hupper). |
|
Polling interval for file changes; a low value costs CPU. |
|
Open a browser at the server URL; see the pitfalls for how the URL is found. |
|
Raise the verbosity (default 1). At level 2 or more a |
|
Suppress the output that |
Source: Pyramid: pserve
config_vars let one file serve several environments. The generated development.ini can be parameterised by
writing listen = localhost:%(http_port)s and running pserve development.ini http_port=8089. The generated files do
not contain such a placeholder; adding one is your decision. Because every Pyramid console script goes through the same
plaster loader, pshell, proutes and the others accept the same config_uri#name and variables.
Like every Pyramid console script, pserve can also be run as a module, which is useful when the interpreter needs
custom flags:
python3 -m pyramid.scripts.pserve development.ini --reload
Source: Pyramid: Command-Line Pyramid
Reloading Code With hupper
--reload turns pserve into a supervisor. It starts a monitor process and runs the real server in a worker that is
restarted when something changes. The reloader is hupper:
env/bin/pserve development.ini --reload
Starting monitor for PID 43537.
Starting server in PID 43537.
2026-10-07 17:54:10,870 INFO [waitress:449][MainThread] Serving on http://[::1]:6543
2026-10-07 17:54:10,870 INFO [waitress:449][MainThread] Serving on http://127.0.0.1:6543
.../bookshelf_shelves/bookshelf_shelves/routes.py changed; reloading ...
Gracefully killing the server.
Starting monitor for PID 43557.
Starting server in PID 43557.
2026-10-07 17:54:14,865 INFO [waitress:449][MainThread] Serving on http://[::1]:6543
2026-10-07 17:54:14,865 INFO [waitress:449][MainThread] Serving on http://127.0.0.1:6543
Source: Pyramid: Reloading Code
Details that matter in practice:
-
hupper monitors every module that the application imported (everything in
sys.modules) and the.inifile given topserve. A change to any of them restarts the worker. When the code crashes at start-up, hupper waits for the next change (or aSIGHUP) before trying again, so a typo does not kill the supervisor. -
Templates are not watched. They do not need a restart as long as
pyramid.reload_templatesis true, which is the case indevelopment.ini. That setting should be false in production because it slows rendering. -
By default hupper polls the filesystem. For a larger project install
watchmanorwatchdog, which hupper uses in that order of preference when present. -
Extra files are watched by naming them in a
[pserve]section of the.inifile. Entries are absolute paths, paths relative to the.inifile, or asset specifications; the Pyramid source also reads anignore_fileskey from the same section to exclude paths from watching. -
--reloadis for development. It doubles the number of processes and is not how you run an application in production.
The [pserve] section is read like any other section of the file. This block checks that plaster hands the keys back,
using a temporary file so the project is not touched:
import tempfile
from pathlib import Path
import plaster
ini = """\
[app:main]
use = egg:bookshelf_shelves
[server:main]
use = egg:waitress#main
listen = localhost:6543
[pserve]
watch_files =
bookshelf_shelves/templates/layout.jinja2
open_url = http://localhost:6543/
"""
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "dev.ini"
path.write_text(ini)
loader = plaster.get_loader(str(path), protocols=["wsgi"])
section = loader.get_settings("pserve")
print(section["watch_files"].split(), section["open_url"])
Source: Pyramid: Monitoring Custom Files
['bookshelf_shelves/templates/layout.jinja2'] http://localhost:6543/
development.ini, production.ini and testing.ini Compared
The three files have the same structure ([app:main], [server:main] and the logging sections). They differ in a
handful of values:
| Setting | development.ini |
production.ini |
testing.ini |
|---|---|---|---|
|
|
|
|
|
|
not set |
not set |
|
|
|
|
|
|
|
|
|
|
*:6543 |
|
root logger level |
|
|
|
project logger level |
|
|
|
The debug flags are all false even in development.ini; they are switches to turn on when diagnosing a specific
problem, covered in Settings, Environment & .ini Files. The listen value uses Waitress’s syntax: host:port pairs separated by
spaces, with * meaning all interfaces, both IPv4 and IPv6 where enabled (the documentation describes *:6543 as
equivalent to 0.0.0.0:6543 [::]:6543). The default server named in all three files, egg:waitress#main, is
Waitress’s serve_paste entry point in the paste.server_runner group.
The Startup Chain
The documentation chapter Startup describes what happens between pressing Return and the Serving on line. This
section follows the same path through the 2.1 sources. Each step is a separate library with a narrow job, which is
why swapping any one of them (a different server, a different configuration format) is possible without touching the
rest:
Source: Pyramid: The Startup Process
| Step | What happens | Who does it |
|---|---|---|
1 |
|
|
2 |
The |
|
3 |
|
|
4 |
|
|
5 |
For the application section PasteDeploy reads |
|
6 |
|
Pyramid |
7 |
|
|
Source: Pyramid: The Startup Process
Documentation and source differ in one ordering detail. The chapter describes the application being created before
the server is started, which is true of the final call; in the code pserve first asks the loader for the server
callable (which only reads the section) and then for the application (which runs main), and it calls the server as
the very last step. A failing main therefore stops start-up before any port is bound.
The same chain can be driven by hand, which is also how every other Pyramid command-line tool obtains the application:
import plaster
import webtest
from pyramid.paster import get_app
loader = plaster.get_loader("development.ini", protocols=["wsgi"])
print(type(loader).__name__, sorted(dict(loader.get_wsgi_app_settings())))
server = loader.get_wsgi_server("main") # a callable, not started
app = loader.get_wsgi_app("main")
print(callable(server), type(app).__name__)
same_app = get_app("development.ini") # the shortcut used by the p* scripts
print(type(same_app).__name__)
print(webtest.TestApp(app).get("/").status)
Source: Pyramid API: pyramid.paster
Loader ['pyramid.debug_authorization', 'pyramid.debug_notfound', 'pyramid.debug_routematch', 'pyramid.default_locale_name', 'pyramid.includes', 'pyramid.reload_templates']
True Router
Router
200 OK
pyramid.paster.get_app(config_uri) and get_appsettings(config_uri) are the two functions to remember outside the
command line: get_app returns the finished WSGI application (the router, ready to be given to any server or to
WebTest), and get_appsettings returns just the application settings. Scripts, workers and deployment glue that need the application outside pserve use the same two functions, because
they all start from one .ini file. The deployment page of this section shows the production forms.
The server section is only parsed here, not run. The loader also supports the config_uri#name syntax for sections
other than main, which is how a single file can describe several applications.
Other Commands That Use the Same Project
Because the project is installed and described by development.ini, the other console scripts work on it without
extra setup. proutes prints the route table of the generated project:
env/bin/proutes development.ini
Name Pattern View Method
---- ------- ---- ------
__static/ /static/*subpath bookshelf_shelves:static/ *
home / bookshelf_shelves.views.default.my_view *
debugtoolbar /_debug_toolbar/*subpath <unknown> *
__/_debug_toolbar/static/ /_debug_toolbar/static/*subpath pyramid_debugtoolbar:static/ *
Source: Pyramid: Command-Line Pyramid
The two debugtoolbar routes appear because development.ini includes the toolbar; with production.ini they are
gone. pshell development.ini opens a Python shell with the app, registry, request and root already set,
pviews development.ini / lists the views that match a URL, prequest development.ini / calls the application
without a server, and ptweens lists the tween chain. The Command Line & p-Scripts page of this section covers
them in depth.
Making the Starter Yours: the First Bookshelf Shelves Change
The first edit to a generated project is almost always the routes. Because main includes .routes, new routes can be
added without changing main at all. This block reuses the generated routes.py exactly as main would, adds the
first API route of Bookshelf Shelves and checks it with WebTest. It builds a throw-away configurator in memory, so
the generated project stays as it was:
import webtest
from pyramid.config import Configurator
SHELVES = [{"id": "s1", "title": "Distributed systems"}]
def list_shelves(request):
return {"shelves": SHELVES}
with Configurator() as config:
config.include("bookshelf_shelves.routes") # the generated includeme()
config.add_route("api_shelves", "/api/shelves")
config.add_view(list_shelves, route_name="api_shelves", renderer="json", request_method="GET")
testapp = webtest.TestApp(config.make_wsgi_app())
print(testapp.get("/api/shelves").json)
print(testapp.get("/api/shelves", status=200).headers["Content-Type"])
Source: Pyramid: The Project Structure
{'shelves': [{'id': 's1', 'title': 'Distributed systems'}]}
application/json
In the real project the route goes into routes.py and the view into a new module under views/, decorated with
@view_config(route_name="api_shelves", renderer="json") so that config.scan() finds it. Splitting a project into several includeme modules, each owning its routes, is how
Pyramid applications grow, and Configuration & the Configurator explains the rules (prefixes, conflicts) that
apply when includes overlap.
Pitfalls and Security
-
Project names that clash with the standard library or Pyramid. Do not name a project
site,testorpyramid: the first two shadow standard-library packages and the last shadows the framework. The pre-generation hook only checks that the name is a valid module name, so it will not stop you. -
Invalid names are rejected, not repaired.
repo_namemust match^[_a-zA-Z$]`. The default derived from `project_name` replaces spaces, colons, hyphens and exclamation marks, but a dot or a leading digit makes the hook exit with `+ERROR: ... is not a valid Python module name!. -
Stale metadata after a rename.
egg:references resolve through installed entry-point metadata. After renaming the package or the distribution, reinstall withpip install -e ".[testing]"and updateuse = egg:...in every.inifile. -
Wrong Python bound. The generated
requires-python = ">=3.8"does not reflect Pyramid 2.1’s requirement of Python 3.10 or newer. Raise it to>=3.10. -
pserve -sfails in Pyramid 2.1. In the installed 2.1 code the-s/--serveroption raises anAttributeError(the method it calls does not exist) as soon as it is used. Choose the server in the.inifile instead, for exampleuse = egg:pyramid#wsgirefwithhostandportkeys, whichpserveran successfully during the checks for this page, or keep Waitress. -
pserve -bcannot find the URL for Waitress.-btakes the address fromopen_urlin the[pserve]section, and otherwise guesses it from aportkey of the server section. Waitress in the generated files useslisten, notport, sopserveprintsWARNING: could not determine the server's url to open the browserand does not open anything unless you setopen_url. -
The debug toolbar is a development tool. It exposes tracebacks, request data, settings and the application’s routes, and it is only meant for the machine you are working on. The generated file includes it through
pyramid.includesindevelopment.inionly, and shows it to127.0.0.1and::1only. If you setdebugtoolbar.hoststo other addresses, only list machines you control. Never include the toolbar inproduction.ini; the documentation notes it also slows rendering by over an order of magnitude. -
production.inilistens on every interface. The generated value*:6543makes Waitress accept connections from the whole network. Behind a reverse proxy on the same host, bind tolocalhostinstead (or use a Unix socket), so clients cannot bypass the proxy. -
Do not commit secrets into the
.inifiles. They are in version control by default, since the project’sMANIFEST.inand the README expect them there. Keep database passwords, signing keys and tokens out of the file and inject them from the environment or a secrets store at deploy time; Settings, Environment & .ini Files shows how. -
pserveis not a process manager. Waitress is documented as suitable for light production use; for anything else run the same.inithrough a production WSGI server and a supervisor, as described in the deployment page. -
A
pkg_resourcesdeprecation warning at start-up. Pyramid 2.1 still importspkg_resources(its release notes say work continues to remove it) and therefore constrainssetuptools<82. With a recent setuptools the import prints aUserWarningwhenpserveorpytestloads Pyramid. It is harmless; do not remove the setuptools constraint to silence it.
What Changed and Where Older Material Is Outdated
| Older instruction | Current practice (Pyramid 2.1) |
|---|---|
Create a project with |
Generate it with |
Install with |
|
Packaging in |
One |
The narrative chapter Creating a Pyramid Project still shows |
The tutorials of the same documentation and the template itself use |
Python 2.7 or 3.6 to 3.9 |
Pyramid 2.1 supports Python 3.10 to 3.14 (3.12 to 3.14 were added and 3.6 to 3.9 dropped in 2.1; Python 2 was dropped in 2.0). |
|
Waitress logs one |
|
|
The comparison with the other frameworks of this site is short. Flask has no project generator; the official
documentation describes an application factory and an installable package, which is what the template does for
Pyramid (compare Application Factory & Project Layout).
Django’s startproject generates a project with settings and a manage.py that plays the role of pserve and the
.ini file together. FastAPI and Tornado ship no project generator; the start-up pages of
FastAPI and Tornado show how
their own start commands relate.