Settings, Environment & .ini Files

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 (with Waitress 3.0.2, plaster_pastedeploy 1.0.1 and PasteDeploy 3.1.0) on Python 3.13, checked on 2026-10-07. Pyramid 2.1 requires Python 3.10 or newer, although the requires-python line of the project that the 2.1-branch cookiecutter generates still says >=3.8. Every Python example below was executed with webtest.TestApp or with Pyramid’s own loaders, and every setting name was checked against the official narrative chapter and against the installed package source. The running example is the Bookshelf Shelves scenario: public shelves owned by users, which has to run the same way on a laptop, in the test suite and in production with different safety switches and different secrets.

Pyramid has two layers of configuration, and mixing them up is the commonest source of confusion. The first is the settings dictionary: a flat mapping of string keys to values that ends up in registry.settings and that both Pyramid and your code read. The second is the .ini file, which is only one of several ways to fill that dictionary, and which also carries things that are not settings at all: which WSGI server to start, which middleware wraps the application and how logging is wired. This page explains both layers, the environment variables that can override a handful of Pyramid’s own settings, and the habits that keep secrets out of the repository.

flowchart LR I[development.ini or production.ini] --> P[plaster and PasteDeploy] P --> M["main(global_config, **settings)"] M --> C[Configurator] E[PYRAMID_* environment variables] --> C C --> R[registry.settings] R --> V[views, add-ons and your code] I --> S[server:main, Waitress] I --> L[logging sections]

The .ini File Format

The path from an .ini file to registry.settings; the environment joins at the Configurator, after main() has already run.

The configuration files that the cookiecutter starter generates (development.ini, production.ini and testing.ini) use the format defined by PasteDeploy, a library by Ian Bicking that predates Pyramid. Pyramid itself does not care about the file: it loads it through plaster, an abstraction that finds a loader for a configuration URI (development.ini, production.ini#main and so on) and hands back settings, a WSGI application, a WSGI server or a logging setup. For .ini files the loader is plaster_pastedeploy, which uses PasteDeploy underneath. The same config_uri is accepted by pserve, pshell, proutes, pviews, ptweens and prequest, and by the functions in pyramid.paster. Other file formats are possible by registering another plaster loader, but the .ini dialect is what every tutorial and every add-on assumes.

The file generated for the Bookshelf Shelves project by the cookiecutter’s 2.1-branch looks like this (shortened; the logging sections are covered in the logging page of this section):

###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###

[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
pyramid.includes =
    pyramid_debugtoolbar

# By default, the toolbar only appears for clients from IP addresses
# '127.0.0.1' and '::1'.
# debugtoolbar.hosts = 127.0.0.1 ::1

###
# wsgi server configuration
###

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

The file is a standard INI file with section headers in square brackets and key = value lines. Values that continue on indented lines belong to the key above them, which is how pyramid.includes takes a list. PasteDeploy gives the section names a meaning:

Section Meaning

[app:NAME]

A WSGI application. The main name is the default that pserve loads (pserve -n NAME or a #NAME suffix on the URI loads another). The keys in the section become the keyword arguments of the application factory, so they are the settings dictionary.

[server:NAME]

A WSGI server. The default main section normally says use = egg:waitress#main plus the server’s own options such as listen. These keys never reach your application.

[filter:NAME]

WSGI middleware: a callable that wraps an application. Selected with use = egg:... or with an explicit paste.filter_app_factory = module:function line.

[pipeline:NAME]

A list of filter names followed by one application name, applied left to right. Replacing [app:main] with a pipeline named main is how middleware is added in front of Pyramid.

[filter-app:NAME]

A shorthand for one filter wrapping one application, written as next = APP_NAME.

[DEFAULT]

Values shared by every section. They are passed to the application factory as global_config, and they are also visible for interpolation in every section.

[loggers], [handlers], [formatters] and the logger_*, handler_*, formatter_* sections

Python logging.config.fileConfig input. pserve and the pyramid.paster helpers call setup_logging for you.

The use = egg:bookshelf_shelves line deserves a sentence. It is shorthand for egg:bookshelf_shelves#main and means "the PasteDeploy application factory entry point called main of the installed project bookshelf_shelves`". The 2.1 cookiecutter declares that entry point in `pyproject.toml (there is no setup.py any more) under the group paste.app_factory, with main = "bookshelf_shelves:main". The egg in the name is historical: nothing needs an egg, an installed project with the entry point is enough, which is why pip install -e . is the step that makes use = egg:... resolve. Two alternatives need no packaging at all and are handy in tests and small applications: use = call:module:function and the long form paste.app_factory = module:function.

Loading an .ini File from Python

pyramid.paster exposes the same machinery that pserve uses: get_appsettings(config_uri, name=None, options=None) returns the settings of an application section as a dictionary, get_app builds the WSGI application (running its factory, and any filters of a pipeline) and setup_logging configures logging. This is the supported way to start the application from a script, a test or a WSGI server that is not pserve.

The first example builds a tiny application factory and a middleware, the way a project package would define them, and exposes them under a module name that an .ini file can refer to. The factory shows two things that matter later: what main receives (strings) and what the registry holds (converted values).

import sys
import time
import types

from pyramid.config import Configurator
from pyramid.settings import asbool, aslist


def show_settings(request):
    settings = request.registry.settings
    return {
        key: settings[key]
        for key in (
            "bookshelf.page_size",
            "bookshelf.public_shelves",
            "bookshelf.admin_emails",
            "pyramid.reload_templates",
        )
    }


def main(global_config, **settings):
    """Application factory, the entry point of a PasteDeploy [app:...] section."""
    # Everything that comes from the .ini file is a string: convert it here.
    settings["bookshelf.page_size"] = int(settings.get("bookshelf.page_size", "20"))
    settings["bookshelf.public_shelves"] = asbool(
        settings.get("bookshelf.public_shelves", "true")
    )
    settings["bookshelf.admin_emails"] = aslist(settings.get("bookshelf.admin_emails", ""))
    with Configurator(settings=settings) as config:
        config.add_route("settings", "/settings")
        config.add_view(show_settings, route_name="settings", renderer="json")
    return config.make_wsgi_app()


def request_timer(app, global_config, header="X-Request-Time"):
    """Middleware factory: the entry point of a PasteDeploy [filter:...] section."""

    def timer(environ, start_response):
        started = time.perf_counter()

        def timed_start_response(status, headers, exc_info=None):
            elapsed_ms = (time.perf_counter() - started) * 1000
            headers.append((header, f"{elapsed_ms:.1f}ms"))
            return start_response(status, headers, exc_info)

        return app(environ, timed_start_response)

    return timer


# In a real project these functions live in the package (bookshelf_shelves/__init__.py)
# and pyproject.toml publishes `main` as an entry point. Here a throw-away module object
# plays that role so that the .ini files below can name it.
shelves_app = types.ModuleType("shelves_app")
shelves_app.main = main
shelves_app.request_timer = request_timer
sys.modules["shelves_app"] = shelves_app

Now a base file with an application, a filter and a pipeline, written to a temporary directory and loaded with the pyramid.paster helpers. Note that the application section is no longer called main: the pipeline took that name.

import tempfile
from pathlib import Path

from pyramid.paster import get_app, get_appsettings
from webtest import TestApp

work = Path(tempfile.mkdtemp(prefix="shelves-ini-"))
base_ini = work / "base.ini"
base_ini.write_text(
    """\
[DEFAULT]
site_name = Bookshelf Shelves

[app:bookshelf]
paste.app_factory = shelves_app:main
pyramid.reload_templates = false
bookshelf.page_size = 20
bookshelf.public_shelves = true
bookshelf.admin_emails =
    ada@example.org
    grace@example.org
config_dir = %(here)s

[filter:timer]
paste.filter_app_factory = shelves_app:request_timer
header = X-Request-Time

[pipeline:main]
pipeline = timer bookshelf
"""
)

# The settings of one section: what main(global_config, **settings) receives.
raw = get_appsettings(f"{base_ini}#bookshelf")
print(dict(raw))

# The application of the pipeline named main: filters wrapped around the factory result.
app = TestApp(get_app(str(base_ini)))
response = app.get("/settings")
print(response.json)
print(response.headers["X-Request-Time"][-2:])

The first print shows that every value is still a string, that the multi-line admin_emails value arrived as one string with embedded newlines and that %(here)s was replaced by the directory of the file. The second shows the converted values read back from request.registry.settings, and the middleware’s header proves that the pipeline was applied.

Interpolation, Global Variables and Command-Line Variables

Inside the file, %(name)s is replaced with the value of name from the same section or from [DEFAULT]. PasteDeploy always provides two variables, %(here)s (the directory that contains the file) and %(__file__)s (the path of the file). Two consequences:

  • [DEFAULT] is global configuration: it is passed as global_config and is not part of the **settings of an application, unless the same key is also repeated in the section.

  • A literal percent sign in a value must be written %%, or interpolation fails when the file is read.

Variables can also come from the command line. pserve development.ini http_port=8080 makes %(http_port)s available, and get_appsettings(uri, options={"http_port": "8080"}) does the same from Python. A variable that is used but not defined is an error when the file is read, which is a useful fail-fast behaviour:

from pyramid.paster import get_appsettings

port_ini = work / "port.ini"
port_ini.write_text(
    """\
[app:main]
paste.app_factory = shelves_app:main
bookshelf.public_url = http://localhost:%(http_port)s

[server:main]
use = egg:waitress#main
listen = *:%(http_port)s
"""
)

print(dict(get_appsettings(str(port_ini), options={"http_port": "8080"})))

try:
    get_appsettings(str(port_ini))
except Exception as exc:
    print(type(exc).__name__)

Variables given on the command line are visible in the process list of the machine (ps), so they are for ports and paths, never for passwords. See Secrets from the Environment.

One File per Environment, or One Base File

The cookiecutter starts with three complete files and that is fine for small projects, but the three copies drift apart. PasteDeploy can inherit: a section can say use = config:base.ini#bookshelf to start from another section and then override keys. There is one trap, and it is the thing old tutorials and Stack Overflow answers get wrong. In PasteDeploy an override that starts with the word set (set pyramid.reload_templates = true) changes the global configuration, not the application’s local settings. Pyramid receives local settings in **settings, so a set line is silently lost from the settings dictionary. Write the key without set to override a local value:

dev_ini = work / "dev.ini"
dev_ini.write_text(
    """\
[app:main]
use = config:base.ini#bookshelf
pyramid.reload_templates = true
bookshelf.page_size = 5

[app:wrong]
use = config:base.ini#bookshelf
set pyramid.reload_templates = true
"""
)

good = get_appsettings(str(dev_ini), "main")
bad = get_appsettings(str(dev_ini), "wrong")
print(good["pyramid.reload_templates"], good["bookshelf.page_size"])
print(bad.get("pyramid.reload_templates"))

The first line prints the overridden values (true 5). The second prints None: the set line moved the key out of the section’s local settings and into global_config, where the Configurator never looks, so even the inherited false is gone and Pyramid falls back to its default.

Pipelines and Middleware

pipeline:main is the PasteDeploy way of adding WSGI middleware without touching Python code: the sample above puts a timing filter in front of the application, and the same mechanism is used for gzip, URL prefixes (PasteDeploy ships an egg:PasteDeploy#prefix filter) or tracing middleware. When the file has a pipeline, the application section can no longer be called main, because pserve loads main and expects the whole stack. Pyramid’s own extension points (tweens, events, view derivers) are usually the better tool when the behaviour needs access to the request or to the view, because a WSGI filter only sees the environment and the response iterable. Tweens are the subject of the events and tweens page of this section.

Built-in Settings

Pyramid reserves the pyramid. prefix in the settings dictionary for its own switches. The Settings object that the Configurator builds from your dictionary does three things for each of them:

  • It accepts the key with or without the prefix: pyramid.reload_templates and reload_templates are the same setting, and both keys are present in registry.settings afterwards, with the converted value.

  • It converts the value: booleans through asbool, so only t, true, y, yes, on and 1 (any case, with surrounding spaces ignored) are true, and anything else, including enable, 2 and the empty string, is silently false.

  • It lets an environment variable override the value from the file. The chapter of the manual states this rule and the installed source confirms it: the environment is read after the dictionary, so when both are present the environment wins.

Setting (prefix optional) Environment variable Default Effect

pyramid.reload_templates

PYRAMID_RELOAD_TEMPLATES

false

Reload a template automatically when its file changes. Meaningful to Chameleon, Mako and most third-party renderers; pyramid_jinja2 uses it for its auto_reload option unless jinja2.reload_templates says otherwise.

pyramid.reload_assets

PYRAMID_RELOAD_ASSETS

false

Do not cache asset file data. Also accepted as pyramid.reload_resources and PYRAMID_RELOAD_RESOURCES, the older names. Very slow with templates: development only.

pyramid.reload_all

PYRAMID_RELOAD_ALL

false

Turns on every pyramid.reload_* setting at once.

pyramid.debug_authorization

PYRAMID_DEBUG_AUTHORIZATION

false

Log why each view permission check passed or failed.

pyramid.debug_notfound

PYRAMID_DEBUG_NOTFOUND

false

Log the reason of a view-related not-found error.

pyramid.debug_routematch

PYRAMID_DEBUG_ROUTEMATCH

false

Log which route matched (or that none did), with the matchdict and the predicates.

pyramid.debug_all

PYRAMID_DEBUG_ALL

false

Turns on every pyramid.debug_* setting at once. It does not touch the reload settings.

pyramid.prevent_http_cache

PYRAMID_PREVENT_HTTP_CACHE

false

The http_cache view argument no longer sets any caching header, process-wide.

pyramid.prevent_cachebust

PYRAMID_PREVENT_CACHEBUST

false

The cachebust argument of static views is ignored, so no cache buster is configured (added in 1.6).

pyramid.default_locale_name

PYRAMID_DEFAULT_LOCALE_NAME

en

The locale used when no locale negotiator is registered.

pyramid.csrf_trusted_origins

PYRAMID_CSRF_TRUSTED_ORIGINS

empty list

Extra origins (host names, one per line or separated by spaces) accepted by the Origin and Referer check of CSRF protection on HTTPS requests. It is read through aslist. It is documented with the CSRF options in the security chapter and implemented in the same Settings code as the others; it is missing from the environment chapter.

pyramid.includes

none

empty

Packages to include, equivalent to calling config.include(). See Including Packages and Ordering Tweens.

pyramid.tweens

none

empty

Explicit tween ordering. See Including Packages and Ordering Tweens.

The installed package also accepts one more switch in the same family, pyramid.debug_templates / PYRAMID_DEBUG_TEMPLATES, turned on by debug_all. The environment chapter does not document it; pyramid_jinja2 reads it, so treat it as an add-on contract rather than a core one. A separate table of other pyramid_* add-on settings (jinja2.*, tm.*, retry.*, debugtoolbar.*) is not repeated here: each belongs to the page of its add-on.

How the two bridges differ, as the manual explains: reload_templates asks the template engine to check the modification time of the one template file on every request, which costs little but is not free; reload_assets additionally stops Pyramid from caching the resolved file name of the template, so that changes in an overridden asset directory are seen immediately, and it slows each template rendering by an order of magnitude. Never enable it outside a development machine.

Environment Variables Override the File

The next example reproduces the rule with a plain dictionary, without any .ini file: a production-like setting is overridden from the environment, debug_all expands to the individual switches, and both the prefixed and the plain key are present in the registry afterwards. It also shows what main would see if it looked at its own settings argument instead of the registry: the raw string.

import os
from unittest import mock

from pyramid.config import Configurator

raw_settings = {
    "pyramid.reload_templates": "false",
    "pyramid.debug_all": "false",
}

with mock.patch.dict(os.environ, {"PYRAMID_RELOAD_TEMPLATES": "1", "PYRAMID_DEBUG_ALL": "yes"}):
    config = Configurator(settings=dict(raw_settings))

resolved = config.registry.settings
print(raw_settings["pyramid.reload_templates"])          # the file said false
print(resolved["pyramid.reload_templates"])              # the environment said 1: True
print(resolved["reload_templates"])                      # the unprefixed alias
print(resolved["pyramid.debug_routematch"], resolved["pyramid.debug_notfound"])  # from debug_all
print(resolved["pyramid.default_locale_name"])           # default

# An exported but empty variable is still "set": it overrides the file with false.
with mock.patch.dict(os.environ, {"PYRAMID_RELOAD_TEMPLATES": ""}):
    config = Configurator(settings={"pyramid.reload_templates": "true"})
print(config.registry.settings["pyramid.reload_templates"])

The same effect from a shell, without editing any file, is the manual’s own example, and it is the way to switch a debug flag on for one run:

PYRAMID_DEBUG_ROUTEMATCH=1 PYRAMID_RELOAD_TEMPLATES=1 pserve development.ini

The empty-variable case on the last lines of the example is a real pitfall: a container or CI job that declares PYRAMID_RELOAD_TEMPLATES= (to "unset" it) turns the setting off even if the file says true, because only a missing variable is ignored. Also remember that the environment is applied by the Configurator constructor. Code in main that looks at its own settings argument sees the raw file value, not the environment’s, and sees a string rather than a boolean: read the registry (config.registry.settings, or request.registry.settings in a view) when the effective value matters.

What the Debug Switches Print

The manual says the debug switches print to the standard error stream. In the installed 2.1 code they go through a debug logger: a standard logging logger named after the package of the code that created the Configurator (or the name or logger passed as the debug_logger argument). Whether you see anything therefore depends on the logging configuration of the .ini file, which is why the cookiecutter’s development.ini sets the project logger to DEBUG and production.ini sets it to WARN. Flipping pyramid.debug_routematch to true in production.ini changes nothing visible until that logger level is lowered too.

import io
import logging

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

stream = io.StringIO()
logger = logging.getLogger("bookshelf_shelves")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(stream)
logger.addHandler(handler)

config = Configurator(
    settings={"pyramid.debug_routematch": "true"},
    debug_logger="bookshelf_shelves",
)
config.add_route("book", "/books/{id}")
config.add_view(lambda request: Response("a book"), route_name="book")
client = TestApp(config.make_wsgi_app())
client.get("/books/7")
client.get("/nowhere", status=404)

print(stream.getvalue())
logger.removeHandler(handler)

The two lines that come out say that the route book matched /books/7 with the matchdict {'id': '7'}, and that no route matched /nowhere. That is the output that debug_routematch is for, and proutes and the URL dispatch page of this section build on it. The debug_authorization output is shown on the authorization page.

All of these switches are for development. The manual points out that many of the reload and debug settings have performance or disclosure implications (they print internal route tables, ACL decisions and file paths) that make them undesirable in production.

Including Packages and Ordering Tweens

Two settings are lists, not flags, and have no environment variable. pyramid.includes is the declarative form of config.include(...): each dotted name in the list is passed to Configurator.include once the configurator is built, which means before the code in your main() runs. That is how the cookiecutter switches the debug toolbar on in development.ini and leaves it out of production.ini without a line of Python:

[app:main]
use = egg:bookshelf_shelves
pyramid.includes =
    pyramid_tm
    pyramid_debugtoolbar

The list may be separated by spaces, by line breaks or (in a Python dictionary) written as a real list. The same rule applies to pyramid.tweens, which gives an explicit order for the tween chain. The manual recommends not needing it: tweens normally order themselves from hints given by the add-ons that register them, and you reach for the setting when ptweens shows an order that an add-on’s own documentation asks you to change.

import types

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

calls = []


def includeme(config):
    """Called by config.include("shelves_search"): the add-on's entry point."""
    calls.append("include")
    settings = config.get_settings()
    config.add_route("search", "/search")
    config.add_view(
        lambda request: Response(f"search: page size {settings['bookshelf.page_size']}"),
        route_name="search",
    )


def audit_tween_factory(handler, registry):
    """A tween factory: referenced by dotted name in pyramid.tweens."""

    def audit_tween(request):
        response = handler(request)
        response.headers["X-Audited"] = "yes"
        return response

    return audit_tween


search = types.ModuleType("shelves_search")
search.includeme = includeme
search.audit_tween_factory = audit_tween_factory
sys.modules["shelves_search"] = search

settings = {
    "bookshelf.page_size": "20",
    "pyramid.includes": "shelves_search",
    "pyramid.tweens": "shelves_search.audit_tween_factory\npyramid.tweens.excview_tween_factory",
}
config = Configurator(settings=settings)
calls.append("after constructor")
config.add_route("home", "/")
config.add_view(lambda request: Response("home"), route_name="home")
client = TestApp(config.make_wsgi_app())

print(calls)
print(client.get("/search").text)
print(client.get("/").headers["X-Audited"])

The calls list shows the order: the include ran inside the constructor, before the line after it. Because included code runs first, anything your own main() configures afterwards wins a conflict with it, which the configuration page of this section explains as "the includer wins". A copy-paste hazard: the example of the same name in the manual spells the built-in tween as pyramid.tweebs.excview_tween_factory in one of its code blocks. The real dotted name is pyramid.tweens.excview_tween_factory, and an incorrect name makes the configurator fail at start-up.

Custom Settings

Anything that does not start with pyramid. is yours. The manual’s recipe has four steps, and they are worth following literally because each one prevents a common bug:

  1. Add the key to the [app:main] section. Use a prefix of your own (bookshelf.page_size), because bare names collide with add-ons: jinja2.*, tm.*, retry.* and debugtoolbar.* are all in the same flat dictionary.

  2. In main() convert the string: int(), float(), asbool for booleans, aslist for lists and your own parser for anything else. asbool and aslist live in pyramid.settings.

  3. Write the converted value back into settings before building the Configurator, because the configurator copies the dictionary it is given. Changing the original afterwards does nothing.

  4. Read it at run time from request.registry.settings (in a view), from config.registry.settings or config.get_settings() (in includeme and at configuration time) and, only where no request is available, from pyramid.threadlocal.get_current_registry().settings.

aslist splits on newlines and then, by default, on spaces too; pass flatten=False to keep each line whole, which you want for values that contain spaces. The asbool pitfall from the table above applies to your own flags, so a typo such as ture is false rather than an error.

The scalable version of that recipe is one small class that parses and validates every key once, fails at start-up with a message that names the offending key, and gives the rest of the code typed attributes:

from dataclasses import dataclass

from pyramid.config import Configurator
from pyramid.exceptions import ConfigurationError
from pyramid.settings import asbool, aslist


@dataclass(frozen=True)
class BookshelfSettings:
    page_size: int
    public_shelves: bool
    admin_emails: list[str]

    @classmethod
    def from_settings(cls, settings):
        try:
            page_size = int(settings.get("bookshelf.page_size", "20"))
        except ValueError:
            raise ConfigurationError("bookshelf.page_size must be an integer") from None
        if not 1 <= page_size <= 100:
            raise ConfigurationError("bookshelf.page_size must be between 1 and 100")
        return cls(
            page_size=page_size,
            public_shelves=asbool(settings.get("bookshelf.public_shelves", "true")),
            admin_emails=aslist(settings.get("bookshelf.admin_emails", ""), flatten=True),
        )


def make_app(global_config=None, **settings):
    shelves = BookshelfSettings.from_settings(settings)
    settings["bookshelf"] = shelves  # converted values go in before the Configurator
    with Configurator(settings=settings) as config:
        config.add_route("limits", "/limits")
        config.add_view(
            lambda request: {"page_size": request.registry.settings["bookshelf"].page_size},
            route_name="limits",
            renderer="json",
        )
    return config.make_wsgi_app()


client = TestApp(make_app(**{"bookshelf.page_size": "50"}))
print(client.get("/limits").json)

try:
    make_app(**{"bookshelf.page_size": "many"})
except ConfigurationError as exc:
    print(exc)

Two warnings on the shape of registry.settings. It is an ordinary dictionary after construction, shared by every request and every thread, so treat it as read-only at run time (a request that mutates it changes the application for everyone). And config.add_settings(...) exists for the cases where an include or a test must add keys after the configurator was built: it updates the same dictionary.

Secrets from the Environment

A signing key, a database password or an API token must never be a line of development.ini, production.ini or a default in the code. These files are committed, copied into images and read by everyone who can read the repository, and their keys are, like everything else in the dictionary, plain strings. The 12-factor rule applies to Pyramid unchanged: keep non-secret structure in the file, and let the operator inject the secrets as environment variables or as files from a secret store.

Three rules make this work in practice:

  • Read the secrets in main() from os.environ, and fail at start-up if they are missing in production. A missing key discovered on the first login is a bad way to find out. Provide a throwaway random value only for the development profile, generated with secrets.token_hex(), and never log it.

  • Do not use pserve ... name=value for secrets: variables on the command line show up in the process list. Environment variables are better, and mounted files (/run/secrets/...) better still.

  • Keep secrets out of what the application can print. The settings dictionary is the first thing that a debug page, pshell session or a "show configuration" view dumps. The debug toolbar shows it in development, which is another reason to keep that add-on out of production.ini.

The same pattern also covers containers, where there is often no .ini file to edit: build the settings dictionary from the environment and pass it to the factory. A prefix such as BOOKSHELF_ keeps the mapping explicit:

import os
import secrets
from unittest import mock

from pyramid.config import Configurator
from pyramid.exceptions import ConfigurationError


def settings_from_environ(environ, prefix="BOOKSHELF_"):
    """BOOKSHELF_PAGE_SIZE=30 becomes the setting bookshelf.page_size = '30'."""
    return {
        "bookshelf." + name[len(prefix):].lower(): value
        for name, value in environ.items()
        if name.startswith(prefix) and name != prefix + "SESSION_SECRET"
    }


def load_session_secret(environ, environment_name):
    value = environ.get("BOOKSHELF_SESSION_SECRET")
    if value:
        return value
    if environment_name == "development":
        return secrets.token_hex(32)  # throw-away: sessions die with the process
    raise ConfigurationError("BOOKSHELF_SESSION_SECRET is required outside development")


fake_environ = {
    "BOOKSHELF_PAGE_SIZE": "30",
    "BOOKSHELF_ENVIRONMENT": "production",
    "BOOKSHELF_SESSION_SECRET": secrets.token_hex(16),  # stands for a value injected by the platform
    "HOME": "/home/shelves",
}
settings = settings_from_environ(fake_environ)
print(sorted(settings))  # the secret is deliberately not copied into the dictionary

secret = load_session_secret(fake_environ, settings["bookshelf.environment"])
print(len(secret))

try:
    load_session_secret({}, "production")
except ConfigurationError as exc:
    print(exc)
print(len(load_session_secret({}, "development")))

The example keeps the secret out of the general dictionary on purpose. Hand it to the one component that needs it (the security policy and the session factory read it from a small object created in main(); the security pages of this section show how) rather than leaving it in a mapping that every add-on can read and that tracebacks and toolbars print. Dockerfiles and orchestrator manifests that supply such variables are covered by the Docker and Kubernetes sections of this site (Docker, Kubernetes).

Testing with Different Settings

A test should never depend on the developer’s shell or on development.ini. The cookiecutter’s testing.ini has the same shape as the others, and the conftest.py it generates reads it, but anything can be overridden per test by passing a dictionary to the factory, or by patching the environment for one block. get_appsettings takes the same options as pserve, and the factory accepts extra keyword settings, so a test can start from the real file and change only what the scenario needs:

from pyramid.paster import get_appsettings

test_ini = work / "testing.ini"
test_ini.write_text(
    """\
[app:main]
paste.app_factory = shelves_app:main
pyramid.reload_templates = false
bookshelf.page_size = 20
"""
)

settings = dict(get_appsettings(str(test_ini), "main"))
settings["bookshelf.page_size"] = "3"  # per-test override of one key
app = TestApp(shelves_app.main({}, **settings))
print(app.get("/settings").json["bookshelf.page_size"])

with mock.patch.dict(os.environ, {"PYRAMID_RELOAD_TEMPLATES": "true"}):
    reloaded = TestApp(shelves_app.main({}, **dict(settings)))
print(reloaded.get("/settings").json["pyramid.reload_templates"])

The unit and functional testing page of this section builds a pytest fixture on this idea. The point to take from the example is that the second block prints True even though testing.ini says false: the environment variable won, which is also how a stray variable in a developer’s shell can make a test pass locally and fail in CI. Clear PYRAMID_* variables in the test setup (or run the suite with an environment that does not define them).

Choosing Where a Value Belongs

Kind of value Where Why

Which server, which port, which middleware

[server:main], [pipeline:main] and [filter:*] in the .ini file

They are deployment facts and never reach the application.

Pyramid’s own switches (reload, debug, locale)

pyramid.* keys in the file, with PYRAMID_* for one-off overrides

The environment variable wins, so an operator can change behaviour without editing the file.

Add-on settings (jinja2.*, tm.*, debugtoolbar.*)

The file

They are structure, identical across environments except for the debug toolbar.

Application options (page size, feature flags)

The file, with a prefix of your own, converted in main()

Reviewable, versioned and visible in one place.

Secrets (keys, passwords, tokens)

The environment or a secret file, read in main()

Never committed, never baked into an image.

Per-test values

A dictionary passed to the factory

Isolated from the developer’s machine.

Compared with Other Frameworks

Pyramid is unusual in separating deployment configuration (the .ini file, shared with the server and logging) from the settings dictionary that the application reads. Flask keeps one config object filled by code (Flask: Configuration), FastAPI usually relies on a typed settings class (FastAPI: Settings, Lifespan & Background Tasks), Tornado registers command-line options (Tornado: Options, Configuration & Logging) and Django uses a Python settings module (Django: Settings & Configuration). The Pyramid equivalent of a typed settings class is the BookshelfSettings pattern above; the equivalent of environment-specific modules is one .ini file per environment, or one base file that the others inherit.

Common Pitfalls

  • set key = value in an inheriting section changes global configuration, not the application’s settings. Override the key without set.

  • Treating main(global_config, **settings) as the final settings. The values are raw strings, they have not been through asbool, and the environment variables have not been applied yet.

  • Boolean typos. pyramid.reload_templates = flase and = enabled are both false. The same is true for your own flags if you use asbool.

  • An empty PYRAMID_* variable overrides the file with false.

  • Shipping the development file. pyramid.reload_templates = true, pyramid.debug_*, pyramid.reload_assets and pyramid.includes = pyramid_debugtoolbar belong in development.ini only. The toolbar in particular exposes settings, request data and an interactive console to every host listed in debugtoolbar.hosts.

  • Mutating registry.settings at run time. It is shared by all requests and all threads.

  • Secrets in files or arguments. See Secrets from the Environment.

  • The use = egg:... line failing with "no entry point". The project is not installed in the active environment, or its paste.app_factory entry point is missing from pyproject.toml. Reinstall with pip install -e ".[testing]".

  • Printing nothing from a debug switch. The debug logger’s level in the logging section of the file decides.

Recent Changes and Outdated Material

Version Change that affects this page

2.0

Python 2 support was dropped, so every example is Python 3 only. The pickle-based session serializer was replaced by JSON, which matters for the signing key that this page tells you to keep out of the repository.

2.1

Python 3.10 to 3.14 are supported and 3.6 to 3.9 were dropped. The cookiecutter 2.1-branch generates pyproject.toml and no setup.py, and the entry point is declared as [project.entry-points."paste.app_factory"]. Some chapters of the narrative documentation (narr/paste.html still prints a setup.py listing) lag behind the 2.1 tutorials; the cookiecutter output is the reference.

Older tutorials may still tell you to run pcreate -s starter, which was removed in favour of cookiecutters, to edit setup.py and use python setup.py develop, which has been replaced by pip install -e ".[testing]", or to find the .ini template inside the scaffold package. The .ini format itself did not change.

Summary

  • The .ini file is PasteDeploy syntax loaded through plaster: [app:*] sections provide the settings, [server:*] the server, [pipeline:*] and [filter:*] the WSGI middleware and the logging sections the logging.

  • main(global_config, **settings) receives raw strings. The registry holds the converted values.

  • The built-in pyramid.* switches accept an optional prefix, are converted with asbool and are overridden by the matching PYRAMID_* environment variable, even when that variable is empty.

  • pyramid.includes and pyramid.tweens are lists with no environment variable; includes run in the Configurator constructor, before your own configuration.

  • Prefix your own settings, convert and validate them once in main(), store the converted values before the configurator is built and read them from request.registry.settings.

  • Secrets come from the environment or a secret file, never from the repository, from pserve arguments or from the general settings dictionary.