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 .ini file 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 .ini files (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, prequest and ptweens all find the application through the same .ini file, and pytest finds the same configuration through testing.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

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

project_name

Free text, default Pyramid Scaffold

Human-readable name. It becomes the description in pyproject.toml and the project value the sample view returns to the template (Bookshelf Shelves in the generated test).

repo_name

Default derived from project_name: lower-cased, with spaces, colons, hyphens and exclamation marks replaced by underscores

Name of the project directory, of the Python package inside it, of the distribution (name in pyproject.toml) and of the egg: reference in the .ini files. It must be a valid module name (see the pitfalls below).

template_language

jinja2 (default), chameleon, mako

Selects the package added to the dependencies (pyramid_jinja2, pyramid_chameleon or pyramid_mako), the config.include(...) line in main, and the template files and their extension (.jinja2, .pt, .mako).

backend

none (default), sqlalchemy, zodb

Selects the persistence layer and, with it, how URLs reach code: none uses URL dispatch, sqlalchemy uses URL dispatch with SQLAlchemy, SQLite, Alembic and pyramid_tm, and zodb uses traversal over a ZODB object tree.

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.

flowchart LR A["cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 2.1-branch"] --> B["clone into ~/.cookiecutters"] B --> C["prompts: project_name, repo_name, template_language, backend"] C --> D["pre-generation hook: repo_name must be a valid module name"] D --> E["render the template files, copy static unrendered"] E --> F["post-generation hook: keep the chosen language and backend, strip prefixes, print next steps"] F --> G["bookshelf_shelves/ project directory"]
Figure: what cookiecutter does between the command and the project directory

Source: pyramid-cookiecutter-starter: hooks and template

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
File or folder Purpose

pyproject.toml

Packaging metadata and dependencies (setuptools build backend), the testing extra, the paste.app_factory entry point, and the pytest and coverage settings.

development.ini

PasteDeploy file for working on the project: template reloading on, the debug toolbar included, listening on localhost:6543, verbose logging.

production.ini

Same structure with the debug toolbar absent, template reloading and all debug flags off, and logging at WARN.

testing.ini

Same structure with the toolbar and reloading off (faster tests) and development-style logging; read by tests/conftest.py.

bookshelf_shelves/__init__.py

The application factory main(global_config, **settings), the function every tool calls to obtain the WSGI application.

bookshelf_shelves/routes.py

includeme(config): adds the static view and the home route. It is pulled in with config.include('.routes').

bookshelf_shelves/views/

View callables, one module per concern; default.py serves the home route and notfound.py the not-found view.

bookshelf_shelves/templates/

layout.jinja2 (page shell), mytemplate.jinja2 (the home page) and 404.jinja2.

bookshelf_shelves/static/

Images and the CSS served at /static with a one-hour cache (cache_max_age=3600).

tests/

Two unit tests of the views and two functional tests that drive the full application through WebTest.

MANIFEST.in

Tells setuptools which non-Python files (.ini, templates, images, CSS) belong in a source distribution.

README.md

Generated getting-started steps for the project.

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_pastedeploy is the plaster loader that understands PasteDeploy-style .ini files. Without it pserve cannot read development.ini. Pyramid 2.1 already requires it (together with plaster, hupper and setuptools<82), so the explicit line only documents that the project relies on it.

  • pyramid_debugtoolbar is a normal runtime dependency, not a development extra. Installing the project in production therefore installs the toolbar too. It is harmless as long as production.ini does not include it (see the security note below), but if you prefer a leaner production install, move it to a dev extra and keep it in pyramid.includes of development.ini only.

  • 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 resolves pyramid, not at the line that looks wrong. Raise the project’s own bound to >=3.10 so the metadata tells the truth.

  • The [project.entry-points."paste.app_factory"] table is what makes use = egg:bookshelf_shelves work. It registers the callable bookshelf_shelves:main under the name main in the group paste.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

pcreate -s starter MyProject

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

The pcreate scaffolds are not part of Pyramid 2.1 (the installed console scripts are pserve, pshell, proutes, pviews, prequest, ptweens and pdistreport).

python setup.py develop

pip install -e ".[testing]"

Running setup.py directly is deprecated by setuptools; the template ships no setup.py to run.

setup.py with install_requires and entry_points

pyproject.toml with dependencies and [project.entry-points]

Same information, standard metadata format.

nosetests (older scaffolds)

pytest with pytest-cov and WebTest

Tests are collected from bookshelf_shelves and tests by the [tool.pytest.ini_options] table.

Output Serving on http://localhost:6543 (one line)

Two Serving on lines, one per address Waitress bound

localhost resolves to both ::1 and 127.0.0.1; see the pserve section.

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.toml gains the dependencies alembic, pyramid_retry, pyramid_tm, SQLAlchemy, transaction and zope.sqlalchemy, and a [project.scripts] entry initialize_bookshelf_shelves_db that points at bookshelf_shelves.scripts.initialize_db:main.

  • The package gains models/ (meta.py, mymodel.py), an alembic/ folder with a versions/ directory, scripts/initialize_db.py and pshell.py; main additionally calls config.include('.models'), which is where the transaction manager and the database session are wired.

  • The .ini files gain sqlalchemy.url = sqlite:///%(here)s/bookshelf_shelves.sqlite, retry.attempts = 3, a [pshell] section with a setup hook, an [alembic] section (script_location and file_template) and logger sections for sqlalchemy and alembic. %(here)s is expanded by PasteDeploy to the directory of the .ini file.

  • 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()

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

global_config

A dictionary built from the [DEFAULT] section of the .ini file (if there is one) plus, since Pyramid 2.1, the key __script__, which every Pyramid console script sets to its own name (pserve, pshell, prequest, proutes, ptweens or pviews). Most applications ignore it.

**settings

Every key and value of the [app:main] section except use. All values are strings, so a line such as pyramid.reload_templates = true arrives as the string "true". Pyramid converts the documented pyramid.* settings (to booleans, lists and so on) when the Configurator is created.

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 .jinja2 renderer through its own includeme function.

  • config.include('.routes') includes the project’s own routes.py. The leading dot is a relative reference resolved against the package that calls include, 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 why views/default.py is picked up without being mentioned.

  • config.make_wsgi_app() finishes the registry, emits the ApplicationCreated event 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', '/')

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

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

config_uri

The configuration file, optionally followed by #name to pick an application section other than main.

config_vars

Zero or more name=value pairs that substitute %(name)s references in the file (see the example below).

-n NAME, --app-name NAME

Load the application section with this name (default main).

--server-name SECTION_NAME

Use the [server:SECTION_NAME] section of the file (default main).

-s SERVER_TYPE, --server SERVER_TYPE

Use a server by its entry-point name from egg:pyramid. Not usable in Pyramid 2.1: see the pitfalls.

--reload

Restart the server when watched files change (hupper).

--reload-interval SECONDS

Polling interval for file changes; a low value costs CPU.

-b, --browser

Open a browser at the server URL; see the pitfalls for how the URL is found.

-v, --verbose

Raise the verbosity (default 1). At level 2 or more a KeyboardInterrupt shows its traceback.

-q, --quiet

Suppress the output that pserve itself prints.

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

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

Details that matter in practice:

  • hupper monitors every module that the application imported (everything in sys.modules) and the .ini file given to pserve. A change to any of them restarts the worker. When the code crashes at start-up, hupper waits for the next change (or a SIGHUP) 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_templates is true, which is the case in development.ini. That setting should be false in production because it slows rendering.

  • By default hupper polls the filesystem. For a larger project install watchman or watchdog, which hupper uses in that order of preference when present.

  • Extra files are watched by naming them in a [pserve] section of the .ini file. Entries are absolute paths, paths relative to the .ini file, or asset specifications; the Pyramid source also reads an ignore_files key from the same section to exclude paths from watching.

  • --reload is 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"])
['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

pyramid.reload_templates

true

false

false

pyramid.includes

pyramid_debugtoolbar

not set

not set

pyramid.debug_authorization, pyramid.debug_notfound, pyramid.debug_routematch

false

false

false

pyramid.default_locale_name

en

en

en

[server:main] listen

localhost:6543

*:6543

localhost:6543

root logger level

INFO

WARN

INFO

project logger level

DEBUG

WARN

DEBUG

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:

flowchart TD A["pserve development.ini --reload"] --> B["hupper: monitor process starts a worker"] B --> C["plaster.get_loader(development.ini, protocols wsgi)"] C --> D["plaster_pastedeploy loader parses the file with PasteDeploy"] D --> E["logging configured from the file sections"] E --> F["server section: use = egg:waitress, entry point main"] D --> G["app section: use = egg:bookshelf_shelves, entry point group paste.app_factory"] G --> H["main(global_config, **settings)"] H --> I["Configurator: include, scan, commit"] I --> J["make_wsgi_app: Router and ApplicationCreated event"] F --> K["Waitress serves the WSGI app on the listen addresses"] J --> K
Figure: from pserve to the first request, with the library responsible for each step

Source: Pyramid: The Startup Process

Step What happens Who does it

1

pserve development.ini parses its arguments. With --reload and no active reloader it starts hupper’s monitor and re-runs itself as the worker; everything below happens in the worker.

pyramid.scripts.pserve, hupper

2

The config_uri is handed to plaster, which looks for a loader for the .ini format among the installed plugins.

plaster

3

plaster_pastedeploy is the loader found; it exposes PasteDeploy through plaster’s interface. Logging is configured from the file’s logging sections (in the worker only, so log files are not opened by two processes).

plaster_pastedeploy, standard logging

4

pserve reads its own [pserve] section (watch_files, ignore_files, open_url), and asks the loader for the server section ([server:main], or the one named with --server-name) and for the application section ([app:main], or the one named with -n).

pyramid.scripts.pserve, PasteDeploy

5

For the application section PasteDeploy reads use = egg:bookshelf_shelves, looks the distribution up, takes its main entry point in the paste.app_factory group and calls it with global_config and the section’s keys. If the section were a [pipeline:main], the filters would wrap the application named at the end of the pipeline.

PasteDeploy, the project’s main

6

main creates the Configurator, includes pyramid_jinja2 and .routes, scans the package, commits the configuration and returns make_wsgi_app(): a router bound to the finished registry. The ApplicationCreated event fires here.

Pyramid

7

pserve prints Starting server in PID ... and calls the server runner (waitress.serve_paste) with the application. Waitress binds the listen addresses, logs Serving on ... and serves requests until stopped.

waitress

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)
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/               *

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"])
{'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, test or pyramid: 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_name must 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 with pip install -e ".[testing]" and update use = egg:... in every .ini file.

  • 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 -s fails in Pyramid 2.1. In the installed 2.1 code the -s / --server option raises an AttributeError (the method it calls does not exist) as soon as it is used. Choose the server in the .ini file instead, for example use = egg:pyramid#wsgiref with host and port keys, which pserve ran successfully during the checks for this page, or keep Waitress.

  • pserve -b cannot find the URL for Waitress. -b takes the address from open_url in the [pserve] section, and otherwise guesses it from a port key of the server section. Waitress in the generated files uses listen, not port, so pserve prints WARNING: could not determine the server's url to open the browser and does not open anything unless you set open_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.includes in development.ini only, and shows it to 127.0.0.1 and ::1 only. If you set debugtoolbar.hosts to other addresses, only list machines you control. Never include the toolbar in production.ini; the documentation notes it also slows rendering by over an order of magnitude.

  • production.ini listens on every interface. The generated value *:6543 makes Waitress accept connections from the whole network. Behind a reverse proxy on the same host, bind to localhost instead (or use a Unix socket), so clients cannot bypass the proxy.

  • Do not commit secrets into the .ini files. They are in version control by default, since the project’s MANIFEST.in and 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.

  • pserve is not a process manager. Waitress is documented as suitable for light production use; for anything else run the same .ini through a production WSGI server and a supervisor, as described in the deployment page.

  • A pkg_resources deprecation warning at start-up. Pyramid 2.1 still imports pkg_resources (its release notes say work continues to remove it) and therefore constrains setuptools<82. With a recent setuptools the import prints a UserWarning when pserve or pytest loads 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 pcreate -s starter

Generate it with cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 2.1-branch; the scaffolds are not shipped with Pyramid 2.1.

Install with python setup.py develop

pip install -e ".[testing]"; the template has no setup.py.

Packaging in setup.py, README.txt and CHANGES.txt

One pyproject.toml, a README.md, pytest and coverage settings inside pyproject.toml.

The narrative chapter Creating a Pyramid Project still shows setup.py

The tutorials of the same documentation and the template itself use pyproject.toml; follow the generated files.

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

Serving on http://localhost:6543

Waitress logs one Serving on line per bound address.

nosetests or the nose plugin

pytest with WebTest, run as pytest -q.

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.