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.
The .ini File Format
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 |
|---|---|
|
A WSGI application. The |
|
A WSGI server. The default |
|
WSGI middleware: a callable that wraps an application. Selected with |
|
A list of filter names followed by one application name, applied left to right. Replacing |
|
A shorthand for one filter wrapping one application, written as |
|
Values shared by every section. They are passed to the application factory as |
|
Python |
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:])
Source: Pyramid: pyramid.paster
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 asglobal_configand is not part of the**settingsof 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"))
Source: PasteDeploy documentation
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_templatesandreload_templatesare the same setting, and both keys are present inregistry.settingsafterwards, with the converted value. -
It converts the value: booleans through
asbool, so onlyt,true,y,yes,onand1(any case, with surrounding spaces ignored) are true, and anything else, includingenable,2and 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 |
|---|---|---|---|
|
|
false |
Reload a template automatically when its file changes. Meaningful to Chameleon, Mako and most third-party renderers;
pyramid_jinja2 uses it for its |
|
|
false |
Do not cache asset file data. Also accepted as |
|
|
false |
Turns on every |
|
|
false |
Log why each view permission check passed or failed. |
|
|
false |
Log the reason of a view-related not-found error. |
|
|
false |
Log which route matched (or that none did), with the matchdict and the predicates. |
|
|
false |
Turns on every |
|
|
false |
The |
|
|
false |
The |
|
|
|
The locale used when no locale negotiator is registered. |
|
|
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 |
|
none |
empty |
Packages to |
|
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:
-
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.*anddebugtoolbar.*are all in the same flat dictionary. -
In
main()convert the string:int(),float(),asboolfor booleans,aslistfor lists and your own parser for anything else.asboolandaslistlive inpyramid.settings. -
Write the converted value back into
settingsbefore building theConfigurator, because the configurator copies the dictionary it is given. Changing the original afterwards does nothing. -
Read it at run time from
request.registry.settings(in a view), fromconfig.registry.settingsorconfig.get_settings()(inincludemeand at configuration time) and, only where no request is available, frompyramid.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()fromos.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 withsecrets.token_hex(), and never log it. -
Do not use
pserve ... name=valuefor 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,
pshellsession or a "show configuration" view dumps. The debug toolbar shows it in development, which is another reason to keep that add-on out ofproduction.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")))
Source: The Twelve-Factor App: Config
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 |
|
They are deployment facts and never reach the application. |
Pyramid’s own switches (reload, debug, locale) |
|
The environment variable wins, so an operator can change behaviour without editing the file. |
Add-on settings ( |
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 |
Reviewable, versioned and visible in one place. |
Secrets (keys, passwords, tokens) |
The environment or a secret file, read in |
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 = valuein an inheriting section changes global configuration, not the application’s settings. Override the key withoutset. -
Treating
main(global_config, **settings)as the final settings. The values are raw strings, they have not been throughasbool, and the environment variables have not been applied yet. -
Boolean typos.
pyramid.reload_templates = flaseand= enabledare both false. The same is true for your own flags if you useasbool. -
An empty
PYRAMID_*variable overrides the file with false. -
Shipping the development file.
pyramid.reload_templates = true,pyramid.debug_*,pyramid.reload_assetsandpyramid.includes = pyramid_debugtoolbarbelong indevelopment.inionly. The toolbar in particular exposes settings, request data and an interactive console to every host listed indebugtoolbar.hosts. -
Mutating
registry.settingsat 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 itspaste.app_factoryentry point is missing frompyproject.toml. Reinstall withpip 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 |
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
.inifile 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 withasbooland are overridden by the matchingPYRAMID_*environment variable, even when that variable is empty. -
pyramid.includesandpyramid.tweensare lists with no environment variable; includes run in theConfiguratorconstructor, 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 fromrequest.registry.settings. -
Secrets come from the environment or a secret file, never from the repository, from
pservearguments or from the general settings dictionary.