Command Line & p-scripts

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), 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 and every command line and its output was produced against the Bookshelf Shelves project of this section (installed in editable mode, with the files shown below). A web framework is only half of the tooling a team needs: the other half is the set of commands that start the application, show what it is made of, poke it without a browser and run maintenance jobs in the same environment as the web process. Pyramid ships seven such commands, collectively called p-scripts ("Pyramid console scripts"), plus a small API (pyramid.paster) for writing your own. They are Pyramid’s counterpart of the flask command described in CLI & Shell in Flask, of manage.py in Django and of the main() plus tornado.options combination in Running & Deploying Tornado. All of them read the project layout generated in Cookiecutter Projects & pserve; this page assumes that page and Settings, Environment & .ini Files.

The Big Picture

The p-scripts have one idea in common: the .ini file is the entry point. None of them imports your application by name. Each takes a config URI (development.ini, or development.ini#main to pick a section), asks plaster for a loader for that file, and the loader (the plaster_pastedeploy plugin for .ini files) builds the WSGI application through PasteDeploy. From then on every script does something different with the application it got back.

flowchart LR C[config URI such as development.ini#main] --> L[plaster loader] L --> F[app factory main with global_config and settings] F --> A[WSGI application and registry] A --> S1[pserve: hand to a server] A --> S2[pshell: open a Python shell] A --> S3[proutes, pviews, ptweens: print configuration] A --> S4[prequest: run one request] A --> S5[your script: bootstrap]
Figure 1. One config URI, one application, seven different uses

The seven commands, with what they print or do:

Command Purpose Needs a running server?

pserve

Serve the application with the server named in the .ini file (and optionally reload it).

It is the server.

pshell

Interactive Python shell with the application, its registry and a request already loaded.

No

proutes

Table of every route: name, pattern, view and request methods.

No

pviews

For one URL, every route and view that could match, with predicates and permission.

No

ptweens

The tween chain, implicit and explicit.

No

prequest

Run one request through the application and print the response.

No (no socket is opened)

pdistreport

Pyramid version, platform and every installed distribution.

No (does not load the application)

All of them except pdistreport take the same two kinds of positional arguments: the config URI and, after it, any number of config variables written name=value. Those variables become PasteDeploy defaults, so a .ini value may say %(http_port)s and the command line supplies it. Since Pyramid 2.1 every script also passes the name of the running script to the application factory as __script__ (see Telling the application it runs in a script).

Running a p-script

Each p-script is a console script installed in the bin directory of the virtual environment, so pserve development.ini works once the environment is active. The same commands are plain modules, which lets you pass options to the Python interpreter itself (a warning filter, -X dev, a debugger):

pserve development.ini --reload
python -m pyramid.scripts.pserve development.ini --reload
python -X dev -m pyramid.scripts.proutes development.ini

The module form was added in Pyramid 1.5. The module names are pserve, pshell, proutes, pviews, ptweens, prequest and pdistreport, all under pyramid.scripts.

The Demo Project

To make the output below reproducible, the Bookshelf Shelves project generated in the previous pages is reduced to what the scripts need. The debug toolbar that the cookiecutter adds to development.ini is removed (it is a development aid, covered in the logging page of this section, and its presence changes the output of proutes and ptweens). The routes include two routes with the same pattern, distinguished by a request method predicate:

# norun: bookshelf_shelves/routes.py
def includeme(config):
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.add_route('shelves', '/shelves')
    config.add_route('shelf', '/shelves/{shelf_id}', request_method='GET')
    config.add_route('shelf_archive', '/shelves/{shelf_id}', request_method='POST')
    config.add_route('verify', '/verify/{code}')

The views read from a small in-memory model. The verify route deliberately has no view yet, which is useful to see how the scripts report a route that leads nowhere:

# norun: bookshelf_shelves/views/default.py
from pyramid.httpexceptions import HTTPNotFound
from pyramid.view import view_config

from ..models import SHELVES, get_shelf


@view_config(route_name='home', renderer='json')
def home(request):
    return {'project': 'Bookshelf Shelves'}


@view_config(route_name='shelves', renderer='json', request_method='GET')
def list_shelves(request):
    return {'shelves': list(SHELVES.values())}


@view_config(route_name='shelves', renderer='json', request_method='POST')
def add_shelf(request):
    return {'created': request.json_body['name']}


@view_config(route_name='shelf', renderer='json')
def show_shelf(request):
    shelf = get_shelf(request.matchdict['shelf_id'])
    if shelf is None:
        raise HTTPNotFound()
    return shelf


@view_config(route_name='shelf_archive', renderer='json', request_param='confirm')
def archive_shelf(request):
    return {'archived': request.matchdict['shelf_id']}

A tween records how long each request took, so that ptweens has something to show (tweens are explained in the events, tweens and hooks page of this section):

# norun: bookshelf_shelves/tweens.py
import time


def timing_tween_factory(handler, registry):
    def timing_tween(request):
        started = time.perf_counter()
        response = handler(request)
        elapsed_ms = (time.perf_counter() - started) * 1000
        response.headers['X-Elapsed-Ms'] = f'{elapsed_ms:.1f}'
        return response
    return timing_tween

The application factory includes the routes, registers the tween and scans for views, exactly as the cookiecutter main does. The configuration lives in development.ini; the last sections are the ones the p-scripts add to the cookiecutter file and are explained below:

# norun: bookshelf_shelves/__init__.py
from pyramid.config import Configurator


def main(global_config, **settings):
    """Return the Bookshelf Shelves WSGI application."""
    with Configurator(settings=settings) as config:
        config.include('.routes')
        config.add_tween('bookshelf_shelves.tweens.timing_tween_factory')
        config.scan()
    return config.make_wsgi_app()
# development.ini (excerpt)
[app:main]
use = egg:bookshelf_shelves

pyramid.reload_templates = true
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en

[server:main]
use = egg:waitress#main
listen = localhost:6543

[pshell]
setup = bookshelf_shelves.pshell_setup.setup
models = bookshelf_shelves.models

[proutes]
format = name pattern view method

[pserve]
open_url = http://localhost:6543/

The logging sections of the file ([loggers], [handlers], [formatters]) are unchanged from the cookiecutter and are read by pserve, pshell, proutes, pviews, ptweens and prequest through the loader. The package is installed with pip install -e ., which is what makes egg:bookshelf_shelves resolvable and what generates the console scripts described in Exposing a Script as a Command.

pserve: Serving the Application

pserve reads the [server:main] section to find a server and the [app:main] section to find the application, builds both, and calls the server with the application. Its options (all verified against pserve --help of 2.1):

Option Meaning

config_uri

The .ini file, optionally file#app_name. Required in practice.

config_vars

Zero or more name=value pairs used for %(name)s interpolation in the file.

-n NAME, --app-name NAME

Load the application section with this name instead of main.

--server-name SECTION_NAME

Use the [server:SECTION_NAME] section instead of [server:main].

-s SERVER_TYPE, --server SERVER_TYPE

Use the named server type. Do not rely on it (see the pitfalls below).

--reload

Restart the process when a watched file changes.

--reload-interval SECONDS

Seconds between checks when the file monitor has to poll.

-b, --browser

Open the page named by open_url in the [pserve] section.

-v, --verbose / -q, --quiet

More or less of pserve’s own messages.

The plain form starts Waitress on the address of the listen setting. The output shows the process id from pserve and one Serving on line per address Waitress binds:

pserve development.ini
Starting server in PID 61363.
2026-10-07 18:32:50,250 INFO  [waitress:449][MainThread] Serving on http://[::1]:18765
2026-10-07 18:32:50,250 INFO  [waitress:449][MainThread] Serving on http://127.0.0.1:18765

Source: Pyramid: pserve

Config variables

A value that changes between machines (a port, a path) can be left as an interpolation in the file and supplied when the command runs, the way the official page describes with http_port=8080:

[server:main]
use = egg:waitress#main
listen = localhost:%(http_port)s
pserve development.ini http_port=8080

The same variables reach your application factory in the global_config argument, and they are what the options argument of pyramid.paster.get_app() and bootstrap() stands for in scripts. For anything secret, prefer environment variables read in the application over command-line variables, which are visible in the process list.

Source: Pyramid: pserve

Reloading while you develop

--reload starts a monitor process that runs the real server in a child and restarts it when a Python module the child imported changes. Since Pyramid 1.8 the monitor is the hupper library. It also watches the .ini file itself and the server’s configuration file. Touching a module produces:

Starting monitor for PID 61433.
Starting server in PID 61433.
...Serving on http://127.0.0.1:18766
.../bookshelf_shelves/models.py changed; reloading ...
Gracefully killing the server.
Starting monitor for PID 61472.
Starting server in PID 61472.
...Serving on http://127.0.0.1:18766

Files that are not Python imports, such as templates compiled lazily or data files, are not watched by default. A [pserve] section tells the monitor about them (watch_files, relative to the .ini file or an asset specification like package:path) or to ignore noisy ones (ignore_files). If the optional watchdog package is installed hupper uses operating-system notifications instead of polling, and --reload-interval then matters much less:

[pserve]
watch_files =
    data/shelves.csv
ignore_files =
    *.swp
open_url = http://localhost:6543/

--reload is a development tool. It doubles the number of processes and exists to restart the whole interpreter, so do not put it in a production.ini startup command.

Pitfalls

  • -s is unreliable. In the 2.1 baseline of this section pserve -s SERVER_TYPE raised an AttributeError. Name the server in the file instead, as use = egg:waitress#main or use = egg:waitress#wsgiref, and select a different section with --server-name.

  • -b needs open_url. The browser is opened for the URL in the [pserve] section. Without it pserve only tries a port setting of the server section, which a Waitress listen line does not provide, so it prints a warning that it could not determine the server URL and opens nothing.

  • pserve is not a daemon manager. --daemon, --pid-file, --log-file, --monitor-restart, --status, --user, --group and --stop-daemon and the start, stop, restart and status commands were removed in Pyramid 1.8 after a deprecation period. Old tutorials that use them predate that. Use a process manager (systemd, a container runtime, Kubernetes) instead, as described in Docker and Kubernetes.

  • The messages go to stderr. Since Pyramid 2.0 pserve writes its own verbose messages to standard error, to avoid the buffering problems of standard output. -q silences those messages but not the server’s own logging, which is configured by the logging sections of the file.

  • Production often does not use pserve at all. A WSGI server such as Gunicorn can load the same .ini file, which is covered by the deployment page of this section; the .ini file stays the single source of configuration.

pshell: The Interactive Shell

pshell builds the application exactly as pserve would and drops you into a Python prompt with the pieces already bound. It is the quickest way to try a query, to see what route_url generates or to call a view by hand:

pshell development.ini
Python 3.13.15 (main, Aug 25 2026, 13:50:12) [Clang 22.1.3 ] on darwin
Type "help" for more information.

Environment:
  app          The WSGI application.
  registry     Active Pyramid registry.
  request      Active request object.
  root         Root of the default resource tree.
  root_factory Default root factory used to create `root`.

Custom Variables:
  models       bookshelf_shelves.models

>>> request.route_url('shelf', shelf_id='1')
'https://shelves.example.com/shelves/1'
>>> models.get_shelf('2')
{'id': '2', 'name': 'Poetry', 'books': 1}

The Environment block is fixed; the Custom Variables block comes from the [pshell] section. Its options are:

Option Meaning

config_uri#section

The application to load (main when omitted).

-p SHELL, --python-shell SHELL

Choose a shell implementation, such as python, ipython or bpython.

-l, --list-shells

Print the shells found by entry point and exit.

--setup DOTTED_NAME

A setup callable; overrides the setup key of the file.

The [pshell] section

Every key of the [pshell] section is a variable name and every value a dotted Python name resolved at start-up, which is how the file above exposes models. Two keys are special: setup names a callable that receives the dictionary of variables before the shell starts, and default_shell lists the preferred shells in order (added in Pyramid 1.6). The callable may change the request, add objects or start a transaction:

# norun: bookshelf_shelves/pshell_setup.py
from webtest import TestApp


def setup(env):
    request = env['request']
    request.host = 'shelves.example.com'
    request.scheme = 'https'
    env['testapp'] = TestApp(env['app'])

Setting request.host and request.scheme is what made route_url print https://shelves.example.com/... above; without it the request pretends to be http://localhost (see Anchoring the request). The setup callable can also be a generator, in which case the code after yield runs when the shell exits, which is the place to roll a transaction back or close a client. The following callable, given as --setup, prints its messages before the prompt and after it is left:

# norun: bookshelf_shelves/pshell_gen.py
def setup(env):
    print('opening shelf session')
    env['shelf_count'] = 2
    try:
        yield
    finally:
        print('closing shelf session')
pshell --setup bookshelf_shelves.pshell_gen.setup development.ini
opening shelf session
>>> shelf_count
2
>>> exit()
now exiting InteractiveConsole...
closing shelf session

A setup callable can be tested without a terminal, because it only needs a dictionary with a request and an application. The following block prepares the same pieces pshell provides, using the bootstrap() function introduced in bootstrap:

import pathlib
import tempfile

from pyramid.paster import bootstrap

from bookshelf_shelves.pshell_setup import setup

ini = pathlib.Path(tempfile.mkdtemp()) / 'script.ini'
ini.write_text("""\
[app:main]
use = egg:bookshelf_shelves
pyramid.default_locale_name = en

[app:readonly]
use = egg:bookshelf_shelves
bookshelf.read_only = true
""")

with bootstrap(str(ini)) as env:
    setup(env)
    print(env['request'].route_url('shelf', shelf_id='1'))
    print(env['testapp'].get('/shelves/2').json)
https://shelves.example.com/shelves/1
{'id': '2', 'name': 'Poetry', 'books': 1}

Alternative shells

By default pshell uses the standard library console. The pyramid_ipython and pyramid_bpython packages register an IPython and a bpython shell (both were moved out of Pyramid in 1.6) and, when one is installed, pshell selects it without further options. pshell --list-shells shows what is available; in a plain environment it prints:

Available shells:
  python

A different REPL is a Python function registered under the pyramid.pshell_runner entry point group. The factory returns a function that takes the variables env and a help text. In a pyproject.toml project the entry point is declared like any other (the official page shows the equivalent setup.py form):

[project.entry-points."pyramid.pshell_runner"]
ptpython = "bookshelf_shelves.shells:ptpython_shell_factory"

Source: Pyramid: pshell

Pitfalls

  • The shell is the real application. It reads the same .ini file as the server, so a shell started with production.ini talks to the production database. Open shells with a development or staging file, and close any transaction you begin (a generator setup is the safe place for that; the transaction side of this is covered in SQLAlchemy, Transactions & Migrations).

  • Variables are described by their docstring. The Environment and Custom Variables help prints the docstring of each object you add, so adding a webtest.TestApp or an int prints a long paragraph. The variables still work; the noise is cosmetic.

  • The request is synthetic. It has no body, no cookies and no matched route. Call request.route_url freely, but do not expect request.matchdict or request.session to contain anything.

proutes: Listing Routes

proutes prints a table of every route registered in the application and, for each, the view that answers it:

proutes development.ini
Name             Pattern                View                                             Method
----             -------                ----                                             ------
__static/        /static/*subpath       bookshelf_shelves:static/                        *
home             /                      bookshelf_shelves.views.default.home             *
shelves          /shelves               bookshelf_shelves.views.default.list_shelves     GET
shelves          /shelves               bookshelf_shelves.views.default.add_shelf        POST
shelf            /shelves/{shelf_id}    bookshelf_shelves.views.default.show_shelf       GET
shelf_archive    /shelves/{shelf_id}    bookshelf_shelves.views.default.archive_shelf    POST
verify           /verify/{code}         <unknown>                                        *

Reading the table:

  • The Name column is the route name. A static view appears as a route named __static/ with the pattern /static/*subpath and the asset it serves, which is how static views are implemented.

  • The View column shows the dotted name of the view callable that will be invoked. <unknown> means a route with no view attached, as the deliberate verify route here; a request for it would end in a 404.

  • The Method column shows the request methods the view accepts. A * means any method. When a route is attached to views for several methods (shelves above), it appears once per view. <route mismatch> appears when the view cannot accept any method the route allows.

Choosing columns and filtering

The columns are name, pattern, view and method. -f (--format) takes a comma separated list that selects and orders them for one run, and the [proutes] section sets the default for the project (the demo file above sets format = name pattern view method; a comma or whitespace separator both work). -g (--glob) keeps only the routes whose name or pattern matches a shell-style glob:

proutes -f name,pattern,method -g '/shelves*' development.ini
Name             Pattern                Method
----             -------                ------
shelves          /shelves               GET
shelves          /shelves               POST
shelf            /shelves/{shelf_id}    GET
shelf_archive    /shelves/{shelf_id}    POST

Quote the glob so that the shell does not expand it. Two routes may share a pattern (shelf and shelf_archive), so proutes is also the quickest way to see whether a predicate on one route will shadow another. Routes are matched in the order they were added; the table is in that order too.

Pitfalls

  • proutes lists URL dispatch routes only. A resource tree used with traversal has no route table; the structure is the tree itself (see Traversal & Resources).

  • It shows what the application registered at start-up. A route added dynamically while serving is not listed.

  • The output has trailing spaces on every line because the columns are padded; strip them before comparing the text.

pviews: Which View Answers a URL

pviews answers a different question from proutes: given a concrete URL path, which routes match and which views would be selected, with their predicates and permissions. The arguments are the config URI and the path:

pviews development.ini /shelves/1
URL = /shelves/1

    context: <pyramid.traversal.DefaultRootFactory object at 0x10705cb90>
    view name: shelves

    Route:
    ------
    route name: shelf
    route pattern: /shelves/{shelf_id}
    route path: /shelves/{shelf_id}
    subpath: 1
    route predicates (request_method = GET,HEAD)

        View:
        -----
        bookshelf_shelves.views.default.show_shelf.

    Route:
    ------
    route name: shelf_archive
    route pattern: /shelves/{shelf_id}
    route path: /shelves/{shelf_id}
    subpath: 1
    route predicates (request_method = POST)

        View:
        -----
        bookshelf_shelves.views.default.archive_shelf
        view predicates (request_param confirm)

Both routes are shown because both match the path; the predicates tell you which one a GET and which one a POST would reach, and the view predicate of archive_shelf (request_param confirm) tells you that a POST without a confirm parameter will not find a view. A GET route also lists HEAD, because Pyramid treats GET as accepting HEAD. For a path that matches nothing, the output is a single line:

URL = /nothing

    Not found.

The context, view name and subpath lines come from traversal run on the root factory (the default root factory in this project has no children), not from the route, so for a URL dispatch application read the Route blocks and ignore those three lines. The official page uses development.ini#tutorial (a named section) in its examples; the section name may be omitted, as above.

ptweens: The Tween Chain

A tween is a piece of code between the router and the application that runs for every request. ptweens prints the order in which they wrap the application:

ptweens development.ini
"pyramid.tweens" config value NOT set (implicitly ordered tweens used)

Implicit Tween Chain

Position    Name
--------    ----
-           INGRESS
0           bookshelf_shelves.tweens.timing_tween_factory
1           pyramid.tweens.excview_tween_factory
-           MAIN

INGRESS is where the request enters and MAIN is the router that finds the view; position 0 is the outermost tween. The implicit chain is computed from the over and under arguments of add_tween() calls. An explicit chain is set by the pyramid.tweens setting, which lists every tween (including pyramid.tweens.excview_tween_factory, which turns exceptions into responses) one per line, outermost first. When it is set, ptweens prints both chains and says which one is used:

[app:main]
use = egg:bookshelf_shelves
pyramid.tweens = bookshelf_shelves.tweens.timing_tween_factory
    pyramid.tweens.excview_tween_factory
"pyramid.tweens" config value set (explicitly ordered tweens used)

Explicit Tween Chain (used)

Position    Name
--------    ----
-           INGRESS
0           bookshelf_shelves.tweens.timing_tween_factory
1           pyramid.tweens.excview_tween_factory
-           MAIN

Implicit Tween Chain (not used)
...

The pitfall that makes ptweens worth running is the explicit chain: once pyramid.tweens is set, it replaces the implicit ordering completely, so a tween added by an add-on and not listed in the setting is silently left out. A tween that is in the file but missing from the output is the symptom. The official example, which has the debug toolbar enabled in development.ini, shows it as pyramid_debugtoolbar.toolbar.toolbar_tween_factory with the alias pdbt at position 0; in production that line must not be there.

prequest: One Request Without a Server

prequest runs a single request through the application and prints the response body, without opening a socket. The arguments are the config URI and the path (with an optional query string):

prequest -d development.ini /shelves/1
200 OK
Content-Type: application/json
Content-Length: 43
X-Elapsed-Ms: 0.2
b'{"id": "1", "name": "Classics", "books": 3}'

The X-Elapsed-Ms header is the one the demo tween adds. The options are:

Option Meaning

-d, --display-headers

Print the status and headers before the body.

-m METHOD, --method METHOD

GET (default), HEAD, POST, PUT, PATCH, DELETE, PROPFIND or OPTIONS.

--header NAME:VALUE

Add a request header; repeat for several. content-type sets the content type.

-l USER:PASSWORD, --login USER:PASSWORD

Send an HTTP Basic Authorization header.

-n NAME, --app-name NAME

The application section to load (main by default).

A request a view rejects is reported like any other. A missing shelf and a predicate mismatch both come out as 404, and the second also says why:

prequest -d -m POST development.ini /shelves/1
404 Not Found
X-Elapsed-Ms: 0.3
Content-Type: text/html; charset=UTF-8
Content-Length: 217
<html>
 <head>
  <title>404 Not Found</title>
 </head>
 <body>
  <h1>404 Not Found</h1>
  The resource could not be found.<br/><br/>
predicate mismatch for view archive_shelf (request_param confirm)
...

The same request with the path /shelves/1?confirm=yes returns {"archived": "1"}.

What prequest is and is not

  • It builds a minimal request. The WSGI environment is synthetic: host localhost, port 80, scheme http, protocol HTTP/1.0, client address 127.0.0.1, and an Accept header of text/plain;q=1.0, */*;q=0.1. There are no proxy headers and no cookies, so it cannot reproduce a problem that depends on X-Forwarded-*, on HTTPS or on a session.

  • A response without a charset is printed as a bytes literal. JSON responses have no charset, which is why the body is shown as b'...' above. Text responses with a charset print as text.

  • Posting a body from standard input does not work reliably in 2.1. The official page shows prequest -mPOST development.ini / < somefile, and the script passes standard input to the application as the request body. On Python 3 that stream is a text stream: in the 2.1 baseline of this section a view that read request.json_body saw an empty body and a view that read request.body_file raised a TypeError. Use curl against pserve, or WebTest (see the testing page of this section), for requests with bodies. The method list on the official narrative page (GET, HEAD, POST, DELETE) is also shorter than the one in the 2.1 command (--help lists eight methods).

  • Use it for smoke tests and for reading a response. A health check in CI or a container, or confirming that a page renders in a production-like configuration, are good uses; anything with state belongs in a real test.

pdistreport: What Is Installed

pdistreport is the one p-script that does not load your application. It prints the Pyramid version, the platform and every distribution found in the environment, which is what a maintainer asks for when you report a bug. It takes no options and no config URI:

pdistreport
Pyramid version: 2.1
Platform: macOS-27.0.1-arm64-arm-64bit-Mach-O
Packages:
  alembic 1.20.0
    A database migration tool for SQLAlchemy.
  annotated-types 0.8.0
    Reusable constraint types to use with typing.Annotated
...

It was added in Pyramid 1.5 and, since 2.1, no longer depends on pkg_resources (it reads importlib.metadata). One caveat from the 2.1 source: for each distribution it writes the Summary metadata field, and a distribution without that field makes the command fail with TypeError: write() argument must be str, not None. In the environment used for this page the ZEO distribution has no summary, so pdistreport stopped before printing the list. Fall back to python -m pip list or python -m pip freeze in that case, and report the exact versions yourself.

Writing Your Own Scripts

Maintenance jobs (import a catalogue, send a digest, recompute counters) want the same configuration and the same application code as the web process. A plain script that imports your modules does not have that environment: the registry is missing, request.route_url has no application to resolve routes, renderers cannot find their settings. Pyramid solves it with pyramid.paster, which loads the application from an .ini file and then sets up the environment a request would have.

bootstrap

bootstrap(config_uri, request=None, options=None) loads the application, builds a request, pushes it and the registry on the thread-local stack and returns a dictionary. Used as a context manager (since 1.8) it cleans up for you:

from pyramid.paster import bootstrap

with bootstrap(str(ini)) as env:
    print(sorted(env))
    print(env['request'].route_url('shelf', shelf_id='1'))
['app', 'closer', 'registry', 'request', 'root', 'root_factory']
http://localhost/shelves/1

The keys are:

Key Value

request

A pyramid.request.Request that stands for the current request state.

app

The WSGI application (a router, for a plain Pyramid section).

root

The resource root produced by the root factory.

registry

The application registry; registry.settings holds the settings.

closer

A no-argument callable that pops the thread-local stack. It is called for you by the with block.

root_factory

The root factory used for root. Present in 2.1 although the narrative page does not list it.

If you do not use with, call the closer yourself when the job ends:

env = bootstrap(str(ini))
try:
    ...
finally:
    env['closer']()

Since 2.0 the closer also runs the request’s finished callbacks (see the request and response page of this section for add_finished_callback), so resources released that way, such as database sessions, are released in scripts too, but only if you actually close.

Anchoring the request

By default the request is for http://localhost:80/, so every URL generated in a script, such as a link in an e-mail, points at localhost. This is the most common mistake in Pyramid scripts. Tell Pyramid where the application really lives by building the request yourself and passing it in:

from pyramid.request import Request

request = Request.blank('/', base_url='https://shelves.example.com/app')
with bootstrap(f'{ini}#readonly', request=request) as env:
    request = env['request']
    print(request.application_url)
    print(request.route_url('verify', code='1337'))
    print(env['registry'].settings['bookshelf.read_only'])
https://shelves.example.com/app
https://shelves.example.com/app/verify/1337
true

The example also loads the section named readonly of the file (the #readonly fragment), which is how a project keeps one .ini file with several applications, for instance a job configuration next to the web one. Without a fragment the section is main. The options argument supplies config variables, like the command-line ones of pserve. In a real job read the base URL from your own settings or an environment variable instead of hardcoding it.

get_app, get_appsettings and prepare

bootstrap() is built from smaller functions that are useful on their own:

Function Use

pyramid.paster.get_app(config_uri, name=None, options=None)

Return the WSGI application, for example to give it to WebTest or a WSGI server, without the request environment.

pyramid.paster.get_appsettings(config_uri, name=None, options=None)

Return the key/value pairs of the app section as a dictionary-like object, without building the application. Cheap and side-effect free, ideal for a script that only needs a setting.

pyramid.scripting.prepare(request=None, registry=None)

Push a request and registry onto the thread-local stack and return a dictionary with root, closer, registry, request and root_factory. Also a context manager. Use it when you already have a registry, for instance in a test.

pyramid.scripting.get_root(app, request=None)

Return (root, closer) for a router.

from pyramid.paster import get_app, get_appsettings

settings = get_appsettings(str(ini))
print(settings['pyramid.default_locale_name'], type(settings).__name__)
app = get_app(f'{ini}#readonly')
print(type(app).__name__)
en ConfigDict
Router

For a file that loads more than one Pyramid application, bootstrap() sets up the environment of the last one loaded. The API page describes loading a specific one with get_app() and prepare() together with pyramid.config.global_registries.

Logging in scripts

bootstrap() does not configure logging. The [loggers], [handlers] and [formatters] sections are read by the p-scripts that ship with Pyramid, but a script of your own has to ask for them with setup_logging(); before that call only the Python defaults apply, which means warnings and nothing more:

from pyramid.paster import setup_logging

setup_logging(str(ini))

setup_logging(config_uri, global_conf=None) takes the same file#name form. Call it first, before bootstrap(), so that the messages emitted while the application is built are captured too. A file with no logging sections is accepted. The logging page of this section covers the logging configuration itself.

Telling the application it runs in a script

Since Pyramid 2.1 every script (all p-scripts, and bootstrap() with the value bootstrap) adds an option named __script__ to the variables used to load the application. With plaster_pastedeploy it arrives in global_config, so an application factory can skip work that makes no sense outside a server, such as starting a background thread or connecting to a message broker:

# norun: application factory sketch
def main(global_config, **settings):
    script = global_config.get('__script__')   # None under a plain WSGI server
    with Configurator(settings=settings) as config:
        config.include('.routes')
        if script is None:
            ...  # start workers only when serving requests
        config.scan()
    return config.make_wsgi_app()

The values seen in this project were proutes when started by proutes, prequest when started by prequest and bootstrap when started by bootstrap(); pserve sets pserve. A WSGI server that loads the file itself does not set it, so the value is None there.

Exposing a Script as a Command

A script becomes a first-class command by declaring it as a console-script entry point. The function takes the command-line arguments, calls setup_logging() and bootstrap(), and returns the exit status. The official page uses optparse; argparse is the modern equivalent and is what the p-scripts themselves use since Pyramid 1.8:

# norun: bookshelf_shelves/scripts.py
"""Console scripts for Bookshelf Shelves."""
import argparse
import sys

from pyramid.paster import bootstrap, setup_logging
from pyramid.request import Request

from .models import SHELVES


def shelf_report(argv=None):
    parser = argparse.ArgumentParser(description='Print a report of all shelves.')
    parser.add_argument('config_uri', help='for example development.ini')
    parser.add_argument('--base-url', default='http://localhost:6543',
                        help='public base URL used when generating links')
    args = parser.parse_args(argv)

    setup_logging(args.config_uri)

    request = Request.blank('/', base_url=args.base_url)
    with bootstrap(args.config_uri, request=request) as env:
        request = env['request']
        for shelf in SHELVES.values():
            url = request.route_url('shelf', shelf_id=shelf['id'])
            print(f"{shelf['name']:<10} {shelf['books']:>3} books  {url}")
    return 0


if __name__ == '__main__':
    sys.exit(shelf_report())

With a pyproject.toml project (the cookiecutter 2.1 branch generates one, not a setup.py), the command is declared in [project.scripts], next to the paste.app_factory entry point that makes egg:bookshelf_shelves work:

[project.entry-points."paste.app_factory"]
main = "bookshelf_shelves:main"

[project.scripts]
shelf-report = "bookshelf_shelves.scripts:shelf_report"

The older setup.py form shown in the official page is entry_points={'console_scripts': ['show_settings = myproject.scripts:settings_show']}; both produce the same executable. After editing the entry points, reinstall the project (pip install -e .) so that the new command is generated in the environment’s bin directory. Then:

shelf-report development.ini --base-url https://shelves.example.com/app
Classics     3 books  https://shelves.example.com/app/shelves/1
Poetry       1 books  https://shelves.example.com/app/shelves/2

Testing a script

Because the function takes argv and returns the exit status, it can be called from a test with a temporary .ini file. Capture the output with contextlib.redirect_stdout:

import contextlib
import io

from bookshelf_shelves.scripts import shelf_report

out = io.StringIO()
with contextlib.redirect_stdout(out):
    status = shelf_report([str(ini), '--base-url', 'https://shelves.example.com/app'])

assert status == 0
assert out.getvalue().splitlines()[0] == (
    'Classics     3 books  https://shelves.example.com/app/shelves/1'
)
print(out.getvalue(), end='')
Classics     3 books  https://shelves.example.com/app/shelves/1
Poetry       1 books  https://shelves.example.com/app/shelves/2

The pytest fixtures of the cookiecutter project use the same testing.ini and bootstrap-style loading, which the testing page of this section describes.

Pitfalls in scripts

  • Always close the environment. Without the with block or closer(), the thread-local stack keeps a reference to the request and registry, and finished callbacks (including those that close database sessions) never run. This matters in long-running workers that call bootstrap() repeatedly.

  • Commit explicitly. A script has no request-wide transaction manager unless you start one; code that relies on pyramid_tm committing at the end of a request does nothing in a script. Begin and commit (or abort) the transaction in the script, as described in SQLAlchemy, Transactions & Migrations.

  • Do not use a web .ini blindly. The application section of development.ini may include development aids (the debug toolbar, template reloading). A script should load a section or file meant for it, which is the #readonly pattern above or a dedicated file.

  • Secrets stay in the environment. A script that takes a database URL or API key on the command line leaks it into the process list; read it from the settings or an environment variable instead, as in Settings, Environment & .ini Files.

  • Containers and schedulers. A Kubernetes Job or a container command can run a console script exactly like pserve; see Docker and Kubernetes for the packaging side, and keep the script idempotent because schedulers may retry it.

What Changed Over the Versions

Version Change

1.1

pyramid.paster.bootstrap() added.

1.5

The p-scripts can be run as python -m pyramid.scripts.NAME; pdistreport added.

1.6

pshell user-defined shells through the pyramid.pshell_runner entry point; IPython and bpython support moved to pyramid_ipython and pyramid_bpython; default_shell option.

1.8

pserve --reload uses hupper; the [pserve] section with watch_files; all p-scripts use argparse; bootstrap() and prepare() usable as context managers; pserve daemon support and its options removed.

2.0

pserve writes verbose messages to stderr; finished callbacks run in the closer of prepare() and bootstrap().

2.1

The __script__ option passed to the application factory; pkg_resources removed from pdistreport and pshell.

Where old material is out of date: tutorials and blog posts that start a project with pcreate, use setup.py develop, run pserve --daemon or pserve start, or write [server:main] with use = egg:Paste#http predate Pyramid 1.8 and the cookiecutter workflow. The pshell and proutes examples in the official narrative pages show older interpreter banners and older default columns; run the command in your own project and trust that output.