Settings & Configuration
|
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 Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python
3.12-3.14 and PostgreSQL 15 or newer, with django-environ 0.14.0, all checked on 2026-10-03; where the 5.2 LTS (5.2.17)
behaves differently the text says so. Every example was run on that stack in a scratch project on SQLite, because no
PostgreSQL or Redis server was available: the PostgreSQL and Redis snippets are marked as such and were validated with
manage.py check and by parsing, not by connecting. A Django project is, in practice, one Python module of
upper-case variables plus the rules for choosing which module is loaded. Almost every other page of this section reads
from it: the database, the installed apps, the middleware chain, the storage backends, the security switches. In the
Bookshelf application that grows across this section, settings decide which database the catalogue lives in, where
uploaded covers go, which hosts may serve the site and whether the process is allowed to show a traceback to the world.
This page covers the settings module and how Django loads it, two proven layouts (a split package and one
environment-driven module), secrets and key rotation, the three settings that most often bite (DEFAULT_AUTO_FIELD,
STORAGES, DATABASES), a tour of the reference, the deployment checks, and how to test code that depends on settings.
In a hurry: what goes where
| Concern | Rule of thumb | Section |
|---|---|---|
Which settings file runs |
One module, named by |
|
Values that differ per environment |
Read them from environment variables (django-environ or |
Two layouts: split settings or environment-driven settings, One module driven by the environment (django-environ) |
|
Long, random, unique per environment, never committed; rotate by moving the old value to |
|
Primary key type |
Leave |
|
File and static storage |
|
|
Database |
|
|
Production readiness |
|
|
Tests |
|
The settings module
A settings file is an ordinary Python module whose module-level, upper-case names are the configuration. Because it is
Python you can compute values, read the environment, import from other settings modules and use pathlib; because it is
Python it also fails on a syntax error and runs arbitrary code at import time, so keep it free of side effects
(no network calls, no database access). The settings that django-admin startproject writes are a good first reading:
BASE_DIR (a pathlib.Path), SECRET_KEY, DEBUG, ALLOWED_HOSTS, INSTALLED_APPS, MIDDLEWARE, ROOT_URLCONF,
TEMPLATES, WSGI_APPLICATION, DATABASES, AUTH_PASSWORD_VALIDATORS, the internationalisation block, STATIC_URL
and, since 6.1, a MAILERS dictionary that points at the console email backend.
Source: Django: Settings
Choosing the module: DJANGO_SETTINGS_MODULE
Django must be told which module to import, through the DJANGO_SETTINGS_MODULE environment variable, whose value is a
dotted Python path (config.settings.prod) that must be importable from sys.path. The three entry points set a default
with os.environ.setdefault(…), so a variable already present in the environment wins:
# manage.py (excerpt) -- wsgi.py and asgi.py contain the same line
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev")
Source: Django: Designating the settings
For one command you can instead pass --settings, which wins over the variable:
export DJANGO_SETTINGS_MODULE=config.settings.dev
python manage.py check
python manage.py check --deploy --settings=config.settings.prod # overrides the variable
Source: Django: The django-admin utility
The convention used on this page is a config package (project settings, URLs, ASGI/WSGI entry points) next to the apps,
as described in Project Structure & Apps. A pitfall worth knowing: if
DJANGO_SETTINGS_MODULE names a package whose __init__.py is empty (here config.settings after turning
settings.py into a package), Django does not complain; it loads an empty settings module. In the scratch project
diffsettings printed only SETTINGS_MODULE = 'config.settings' # and check reported no issues. Point the variable
at a concrete module (config.settings.dev), and keep __init__.py empty or import nothing from it.
How the settings are loaded
The algorithm is short: Django starts from django/conf/global_settings.py (every setting has a default there), then
loads your module and overrides the defaults. Your module should not import global_settings. Application code reads
settings through the single object django.conf.settings, which is an instance of LazySettings, not a module:
or --settings"] --> lazy["django.conf.settings
(LazySettings)"] gs["global_settings.py
(defaults)"] --> lazy mod["your settings module
(overrides)"] --> lazy lazy -->|"first attribute access"| read["settings.DEBUG
settings.DATABASES ..."] read --> setup["django.setup()
app registry, logging"] setup --> handler["WSGI/ASGI handler
middleware, URLconf"]
from django.conf import settings
if settings.DEBUG:
... # fine: read through the settings object
# from django.conf.settings import DEBUG # ImportError: settings is an object, not a module
# from config.settings.dev import DEBUG # wrong: bypasses the defaults and the override mechanism
# settings.DEBUG = True # don't mutate settings at runtime (tests excepted)
Three practical consequences follow:
-
Laziness. The module is imported on the first access, not when
django.confis imported. Since 3.2 theSECRET_KEYcheck is also deferred to the first access of that setting, so management commands that never touch it can run without a key. -
Read at call time. Code that copies
settings.Xinto a module-level constant freezes the value at import time and defeatsoverride_settings(see Testing code that depends on settings). Readsettings.Xinside the function that needs it. -
Names. Custom settings must be upper case; do not reinvent an existing setting; sequences are lists by convention.
manage.py diffsettings prints what your project changes relative to the defaults: lines marked # are settings that
have no default, --all includes unchanged ones and --output unified renders a diff -u style output. It is the fastest
way to answer "which value is this process really using?":
$ python manage.py diffsettings --settings=config.settings.dev
BASE_DIR = PosixPath('/srv/bookshelf') ###
DEBUG = True
MAILERS = {'default': {'BACKEND': 'django.core.mail.backends.console.EmailBackend'}} ###
ROOT_URLCONF = 'config.urls' ###
SECRET_KEY = 'dev-only-not-a-secret'
SETTINGS_MODULE = 'config.settings.dev' ###
...
Source: Django: diffsettings
(diffsettings prints every setting including SECRET_KEY and database passwords: do not paste its output into a ticket
from a production environment.)
Standalone use: settings.configure() and django.setup()
Outside a project (a script, a notebook, one component such as the template engine used inside a larger program) you can
call settings.configure() instead of setting the variable. Use exactly one of the two, never both and never neither;
calling configure() after a setting has been read, or twice, is an error. A standalone script must also call
django.setup() once, to populate the application registry:
import django
from django.conf import settings
if not settings.configured:
settings.configure(
DEBUG=True,
TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates"}],
)
django.setup() # required for standalone use; manage.py and the WSGI/ASGI entry points do it for you
from django.template import engines # noqa: E402
print(engines["django"].from_string("Hello {{ name }}").render({"name": "Bookshelf"}))
print("configured:", settings.configured, "| DEBUG:", settings.DEBUG)
Hello Bookshelf
configured: True | DEBUG: True
configure() also accepts a default_settings module or class; that replaces Django’s defaults entirely, so you then
own every setting the imported code might read. In this mode Django does not touch the process environment (for
example it will not set TZ, which it otherwise does from TIME_ZONE on Unix).
Where settings sit in the request/response cycle
Settings are not one lookup; different settings are consumed at different moments, and that decides whether a change needs a restart:
-
Process start.
manage.py,get_wsgi_application()andget_asgi_application()import the settings module and calldjango.setup():INSTALLED_APPSpopulates the app registry,LOGGINGis applied, and theDATABASES/CACHES/STORAGES/TASKS/MAILERShandlers are created lazily from their dictionaries. -
Handler start. The handler builds the middleware chain once from
MIDDLEWAREand resolvesROOT_URLCONFlazily on the first request. Editing these needs a restart (runserverrestarts for you when a file changes). -
Every request. Some settings are read per request:
ALLOWED_HOSTSis checked byHttpRequest.get_host(), the CSRF and cookie settings by the matching middleware,CONN_MAX_AGEwhen Django decides whether to reuse or close a connection at the start and end of the request,DEBUGwhen an unhandled exception chooses between the technical error page and the plain 500 page. -
Never at runtime. Application code must not assign to
settings; the supported way to vary a value for a block of code isoverride_settingsin tests (Testing code that depends on settings).
The ALLOWED_HOSTS case is easy to demonstrate with the test client. This is a complete test (it needs no URL beyond the
admin login page):
from django.test import SimpleTestCase, override_settings
class HostValidationTests(SimpleTestCase):
"""ALLOWED_HOSTS is read on every request (HttpRequest.get_host())."""
@override_settings(DEBUG=False, ALLOWED_HOSTS=["bookshelf.example.com"])
def test_unknown_host_is_rejected(self):
response = self.client.get("/admin/login/", headers={"host": "evil.example.org"})
self.assertEqual(response.status_code, 400)
@override_settings(DEBUG=False, ALLOWED_HOSTS=["bookshelf.example.com"])
def test_known_host_is_served(self):
response = self.client.get(
"/admin/login/", headers={"host": "bookshelf.example.com"}
)
self.assertEqual(response.status_code, 200)
Source: Django: ALLOWED_HOSTS
For the full path a request takes after the settings are loaded, see Request/Response Cycle and Middleware.
Sync and async notes
The settings object is a plain in-memory attribute lookup, so reading it is safe from synchronous and asynchronous code
alike; nothing in django.conf performs I/O after the module has loaded. The settings that interact with concurrency are
the database ones: each thread has its own connection, persistent connections (CONN_MAX_AGE) are meant for WSGI and
should be disabled under ASGI, where a backend connection pool is the supported way to avoid reconnecting (see
DATABASES). Compute expensive or blocking values (a secret fetched from a vault, for example) before the settings
module finishes importing, once, rather than lazily from inside a coroutine.
Two layouts: split settings or environment-driven settings
Every real project needs different values per environment. There are two mainstream answers, and they combine well.
| Split settings package | One module driven by the environment | |
|---|---|---|
Shape |
|
One |
Strengths |
Explicit, greppable, diffable; per-environment code (apps, middleware, debug tools) is easy to express. |
Matches twelve-factor and container deployment; the same artefact runs everywhere; secrets never touch Git. |
Weaknesses |
|
Behaviour hides behind variables; a missing or mistyped variable shows up at start-up (good) or at runtime (bad). |
Best for |
Small teams, a few well-known environments, tooling differences (Debug Toolbar in |
Containers, Kubernetes, PaaS, many deployments of the same image. |
The hybrid that most teams end up with, and the one used for Bookshelf: a base.py for everything that is the same
everywhere, thin dev/test modules for local differences, and a production module that reads every secret and every
host-specific value from the environment (either with os.environ or django-environ, shown in One module driven by the environment (django-environ)).
A split settings package
bookshelf/
manage.py
config/
__init__.py
settings/
__init__.py # empty on purpose
base.py # shared by every environment
dev.py # DEBUG, console mailer, local SQLite
test.py # fast hasher, in-memory database
prod.py # secrets from the environment, HTTPS hardening
urls.py
asgi.py
wsgi.py
shop/ # the Bookshelf app
# config/settings/base.py -- shared by every environment
from pathlib import Path
# config/settings/base.py -> parents[0]=settings, [1]=config, [2]=project root
BASE_DIR = Path(__file__).resolve().parents[2]
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"shop",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
# config/settings/dev.py -- development: readable, insecure, throw-away
from .base import * # noqa: F403
DEBUG = True
SECRET_KEY = "dev-only-not-a-secret" # a throw-away key that is only valid on a developer machine
ALLOWED_HOSTS = [] # with DEBUG=True Django allows localhost, 127.0.0.1 and [::1]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3", # noqa: F405
}
}
MAILERS = {
"default": {"BACKEND": "django.core.mail.backends.console.EmailBackend"},
}
# config/settings/test.py -- tests: fast and isolated
from .dev import * # noqa: F403
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] # speed only, never outside tests
DATABASES["default"]["NAME"] = ":memory:" # noqa: F405
MAILERS = {"default": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"}}
# config/settings/prod.py -- production: everything sensitive comes from the environment
import os
from .base import * # noqa: F403
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # KeyError at start-up if it is missing: good
SECRET_KEY_FALLBACKS = [
key for key in os.environ.get("DJANGO_SECRET_KEY_FALLBACKS", "").split(",") if key
]
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = [
o for o in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if o
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3", # PostgreSQL in the real deployment, see the databases section
"NAME": os.environ.get("DJANGO_DB_PATH", BASE_DIR / "db.sqlite3"), # noqa: F405
}
}
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 3600 # raise gradually once HTTPS is proven; HSTS is hard to undo
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_REFERRER_POLICY = "same-origin"
X_FRAME_OPTIONS = "DENY"
STATIC_ROOT = BASE_DIR / "staticfiles" # noqa: F405
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {
"host": os.environ.get("DJANGO_EMAIL_HOST", "localhost"),
"port": int(os.environ.get("DJANGO_EMAIL_PORT", "587")),
"username": os.environ.get("DJANGO_EMAIL_USER", ""),
"password": os.environ.get("DJANGO_EMAIL_PASSWORD", ""),
"use_tls": True,
},
},
}
The module for manage.py, wsgi.py and asgi.py defaults to config.settings.dev; production sets
DJANGO_SETTINGS_MODULE=config.settings.prod in the process environment, and the test run uses
--settings=config.settings.test. Running it:
$ python manage.py check
System check identified no issues (0 silenced).
$ DJANGO_SETTINGS_MODULE=config.settings.prod python manage.py check --deploy # no variables set
...
KeyError: 'DJANGO_SECRET_KEY'
$ export DJANGO_SECRET_KEY="$(python -c 'from django.core.management.utils import get_random_secret_key as g; print(g())')"
$ export DJANGO_ALLOWED_HOSTS=bookshelf.example.com
$ python manage.py check --deploy --settings=config.settings.prod
System check identified no issues (0 silenced).
$ python manage.py test --settings=config.settings.test
Found 9 test(s).
...
OK
Source: Django: Deployment checklist
Habits that keep a split package honest:
-
Put the common values in
base.pyand only the differences in the others; a value repeated in two environment files will diverge. -
from .base import *is acceptable here because the file is in your control; add# noqa: F403andF405for linters. When you extend a list or dictionary frombase, build a new one (MIDDLEWARE = [*MIDDLEWARE, "…"]) instead of mutating the imported object in place, which is shared with every module that imported it. -
Never commit
prod.pyvalues that are secret; the example above commits only the names of the variables. -
A
test.pythat is faster than production (a weak password hasher, an in-memory database) is fine; make sure the tests that matter (migrations, deployment checks) also run once with production-like settings.
One module driven by the environment (django-environ)
django-environ (PyPI 0.14.0, 2026-06-18; its classifiers list Django up to 6.0
but it ran against 6.1.1 here) wraps os.environ with typed readers (bool, int, list, db, cache, …), defaults,
and an optional .env file reader. Install it with pip install django-environ (the import name is environ).
# config/settings/env.py -- one settings module for every environment
from pathlib import Path
import environ
from .base import * # noqa: F403
BASE_DIR = Path(__file__).resolve().parents[2]
env = environ.Env(
# (cast, default) pairs
DJANGO_DEBUG=(bool, False),
DJANGO_ALLOWED_HOSTS=(list, []),
DJANGO_CSRF_TRUSTED_ORIGINS=(list, []),
DJANGO_SECRET_KEY_FALLBACKS=(list, []),
)
# Reads BASE_DIR/.env if it exists; real environment variables always win.
environ.Env.read_env(BASE_DIR / ".env")
DEBUG = env("DJANGO_DEBUG")
SECRET_KEY = env("DJANGO_SECRET_KEY") # no default: ImproperlyConfigured if missing
SECRET_KEY_FALLBACKS = env("DJANGO_SECRET_KEY_FALLBACKS")
ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS")
CSRF_TRUSTED_ORIGINS = env("DJANGO_CSRF_TRUSTED_ORIGINS")
# DATABASE_URL=postgres://user:password@host:5432/dbname (PostgreSQL)
# DATABASE_URL=sqlite:///db.sqlite3 (what this page runs)
DATABASES = {"default": env.db("DATABASE_URL", default=f"sqlite:///{BASE_DIR}/db.sqlite3")}
DATABASES["default"]["CONN_MAX_AGE"] = env.int("DJANGO_CONN_MAX_AGE", default=0)
if not DEBUG:
SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True)
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# .env.example -- committed; copy to .env (git-ignored) and fill in. Values are placeholders.
DJANGO_DEBUG=false
DJANGO_SECRET_KEY=replace-me-with-output-of-get_random_secret_key
DJANGO_ALLOWED_HOSTS=bookshelf.example.com,www.bookshelf.example.com
DJANGO_CSRF_TRUSTED_ORIGINS=https://bookshelf.example.com
DATABASE_URL=sqlite:///db.sqlite3
$ DJANGO_SETTINGS_MODULE=config.settings.env python manage.py check # no .env, no variables
django.core.exceptions.ImproperlyConfigured: Set the DJANGO_SECRET_KEY environment variable
$ cp .env.example .env
$ DJANGO_SETTINGS_MODULE=config.settings.env python manage.py diffsettings | grep -E "^(ALLOWED_HOSTS|CSRF_TRUSTED|SECRET_KEY)"
ALLOWED_HOSTS = ['bookshelf.example.com', 'www.bookshelf.example.com']
CSRF_TRUSTED_ORIGINS = ['https://bookshelf.example.com']
SECRET_KEY = 'replace-me-with-output-of-get_random_secret_key'
$ DJANGO_DEBUG=true DJANGO_SETTINGS_MODULE=config.settings.env python manage.py diffsettings | grep "^DEBUG"
DEBUG = True # the real environment beat the .env file
Source: django-environ documentation
How it behaves, and the traps:
-
Precedence.
Env.read_env()does not overwrite variables that already exist (itsoverwriteparameter defaults toFalse), so the real environment beats the file. That is what you want: the deployment overrides the repository. -
Required values. A variable with no default raises
ImproperlyConfiguredthe first time it is read, which is the fail-fast behaviour you want for secrets. A default for a secret defeats the purpose. -
Casting.
env.boolaccepts the usual true/false spellings;env.listsplits on commas;env.intconverts. Remember that every environment variable is a string:DJANGO_DEBUG=Falseas a plainos.environvalue is truthy. -
Database URLs.
env.db()parsesDATABASE_URL. A relative SQLite path is relative to the working directory, so build an absolute one fromBASE_DIR, as above. Parsing a PostgreSQL URL needs no server (placeholder credentials):
import environ
environ.Env.db_url_config("postgres://bookshelf:change-me@db.internal:5432/bookshelf?sslmode=require")
# {'NAME': 'bookshelf', 'USER': 'bookshelf', 'PASSWORD': 'change-me', 'HOST': 'db.internal', 'PORT': 5432,
# 'OPTIONS': {'sslmode': 'require'}, 'ENGINE': 'django.db.backends.postgresql'}
Source: django-environ: API reference
-
.envin production. Containers and platforms inject real variables; ship no.envfile in the image, and keep.envin.gitignore. Mounted secret files (Docker/Kubernetes secrets) can be read withPath("/run/secrets/db_password").read_text().strip()in the settings module; see Kubernetes Secrets and ConfigMaps. -
Email. django-environ has helpers that produce the deprecated
EMAIL_*settings; with 6.1 build aMAILERSdictionary yourself (see The settings reference: highlights), as the production module above does. -
Other stacks. The same twelve-factor idea (config in the environment, validated at start-up) is covered for FastAPI with pydantic-settings in Settings, Lifespan & Background Tasks and for NestJS in Configuration; the principle is the twelve-factor "Config" rule.
Secrets: SECRET_KEY and SECRET_KEY_FALLBACKS
SECRET_KEY seeds Django’s cryptographic signing. The documentation lists what depends on it: sessions (unless you use
the cache session backend, and always when the default get_session_auth_hash() is used), messages when the cookie or
fallback storage is used, password-reset tokens, and any use of django.core.signing that is not given another key.
It is not used to hash user passwords, so rotating it does not invalidate passwords. A known key lets an attacker forge
all of those, which is why the docs describe the consequences as privilege escalation and, for some configurations,
remote code execution.
Rules that follow from that:
-
Generate it with
get_random_secret_key()(and never copy the one thatstartprojectprinted into a repository: itsdjango-insecure-prefix is exactly what thesecurity.W009deployment check flags). -
Use a different key per environment; the production key exists only in the production secret store.
-
Load it from the environment or a file at start-up. The settings module must not contain it, and neither must the image.
-
Treat other secrets the same way: database and cache passwords, email credentials, third-party API keys.
DEBUG=Truepages hide settings whose names containAPI,KEY,PASS,SECRET,SIGNATUREorTOKEN, but that is partial protection and you must never run production withDEBUG=True.
from django.core.management.utils import get_random_secret_key
print(get_random_secret_key()) # 50 random characters; run once per environment and store in the secret manager
Source: Django: SECRET_KEY
The deployment checklist’s two ways to load the key (an environment variable, or a file) are:
import os
SECRET_KEY = os.environ["SECRET_KEY"] # or:
with open("/etc/secret_key.txt") as f: # a file mounted by your platform, readable only by the app user
SECRET_KEY = f.read().strip()
Rotating the key with SECRET_KEY_FALLBACKS
Since 4.1, SECRET_KEY_FALLBACKS lets you change the key without logging everyone out. Verification accepts signatures made
by the current key or any fallback; new signatures always use SECRET_KEY. The procedure:
-
Generate a new key and store it.
-
Set
SECRET_KEYto the new key and put the previous value at the start ofSECRET_KEY_FALLBACKS; deploy. -
Wait until the longest-lived thing signed with the old key (a session, a password-reset link) has expired or been re-issued.
-
Remove the old key from the list; deploy again.
This script proves the behaviour against django.core.signing (it uses throw-away keys and override_settings, because a
second settings.configure() is an error):
from django.conf import settings
from django.core import signing
from django.core.management.utils import get_random_secret_key
from django.test import override_settings
old_key = get_random_secret_key()
new_key = get_random_secret_key()
# 1. Before the rotation: the old key is SECRET_KEY.
settings.configure(SECRET_KEY=old_key)
token = signing.dumps({"user_id": 42}, salt="bookshelf.demo")
print("verify before rotation:", signing.loads(token, salt="bookshelf.demo"))
# 2. Rotation: new key first, old key moved to SECRET_KEY_FALLBACKS.
with override_settings(SECRET_KEY=new_key, SECRET_KEY_FALLBACKS=[old_key]):
print("verify during rotation:", signing.loads(token, salt="bookshelf.demo"))
fresh = signing.dumps({"user_id": 42}, salt="bookshelf.demo")
print("new tokens are signed with the new key:", fresh != token)
# 3. After removing the fallback: old tokens are rejected.
with override_settings(SECRET_KEY=new_key, SECRET_KEY_FALLBACKS=[]):
try:
signing.loads(token, salt="bookshelf.demo")
except signing.BadSignature as exc:
print("after the fallback is removed:", type(exc).__name__)
verify before rotation: {'user_id': 42}
verify during rotation: {'user_id': 42}
new tokens are signed with the new key: True
after the fallback is removed: BadSignature
Source: Django: SECRET_KEY_FALLBACKS
A few details from the reference worth keeping in mind:
-
Signing is computationally expensive, so every extra fallback adds work to each check that does not match an earlier key. Keep the list short and remove old values "in a timely manner", as the deployment checklist puts it.
-
security.W025applies the weak-key test ofsecurity.W009to every entry ofSECRET_KEY_FALLBACKS. -
Uses of the key must not assume it is text or bytes; go through
force_str()/force_bytes()if you consume it yourself. Preferdjango.core.signing(with a salt) to hand-rolled HMACs; see Sessions, Cookies, Messages & Signing. -
If the key is missing, Django refuses to start (
ImproperlyConfigured: The SECRET_KEY setting must not be empty.); since 3.2 the error appears on first access of the setting, not when the settings load.
DEFAULT_AUTO_FIELD and AppConfig.default_auto_field
When a model declares no field with primary_key=True, Django adds an automatic id column whose type comes from
DEFAULT_AUTO_FIELD (or, for an app, from AppConfig.default_auto_field, which defaults to the project setting). The
setting arrived in 3.2, which kept AutoField (32-bit) as the default and generated new projects and apps with BigAutoField
spelled out; Django 6.0 changed the default to
BigAutoField (64-bit). A new project on 6.1 therefore needs no setting at all, and the one in this page keeps it
explicit only to document it.
# config/settings/base.py
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # already the default since 6.0
# shop/apps.py -- a reusable app pins its own key type
from django.apps import AppConfig
class ShopConfig(AppConfig):
name = "shop"
default_auto_field = "django.db.models.BigAutoField"
Source: Django: DEFAULT_AUTO_FIELD
Why the app-level attribute matters: a reusable app that ships migrations must pin the key type, otherwise
makemigrations in a project that chose a different DEFAULT_AUTO_FIELD generates migrations inside the installed app
that conflict with its future releases. The scratch project shows both behaviours for a one-field Book model:
$ python manage.py makemigrations shop && grep AutoField shop/migrations/0001_initial.py
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# Project says AutoField, the app pins BigAutoField: nothing to do
$ python manage.py makemigrations shop --dry-run --settings=config.settings.legacy_ids
No changes detected in app 'shop'
# Project says AutoField and the app pins nothing: the model follows the project
$ python manage.py makemigrations shop --dry-run -v 3 --settings=config.settings.legacy_ids | grep AutoField
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
Guidance:
-
Existing projects upgrading to 6.0 or later that still use 32-bit keys can keep them by setting
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"(or the app attribute); changing existing tables tobigintis a migration decision, covered in Migrations. -
Auto-created many-to-many tables respect the setting when created, but the migrations framework cannot yet convert the primary key of an existing auto-created through table. If you switch the setting and generate migrations, the related models and the through table’s foreign keys change but the through table’s own key does not; the reference tells you to add a
RunSQLoperation with theALTER TABLEstep. Explicitly definedthroughmodels are handled normally. -
For public identifiers consider a UUID primary key or a separate
uuid/slug column rather than exposing sequential ids; that is a model-design topic for Models & Fields.
STORAGES
STORAGES (4.2) is a dictionary mapping storage aliases to BACKEND and optional OPTIONS. It replaced
DEFAULT_FILE_STORAGE and STATICFILES_STORAGE, which were deprecated in 4.2 and removed in 5.1. Two aliases have
special meaning: default (the storage behind FileField/ImageField and default_storage) and staticfiles (the one
collectstatic and the static template tag use). Any other alias is yours.
The default value is:
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
Source: Django: STORAGES
Defining STORAGES replaces the default; it is not merged with it. If you only override staticfiles and forget
default, file uploads lose their storage. Copy the whole dictionary and edit it. OPTIONS are passed to the backend’s
constructor as keyword arguments, and a ready instance is available from django.core.files.storage.storages:
# config/settings/base.py (excerpt)
# BASE_DIR is the pathlib.Path defined at the top of base.py
MEDIA_ROOT = BASE_DIR / "media"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
# An extra, named storage for generated exports (any alias is allowed).
"exports": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {"location": BASE_DIR / "private_exports"},
},
}
# config/settings/prod.py (excerpt): hashed static file names for far-future caching
STORAGES["staticfiles"] = {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
}
Run against the production settings, collectstatic produces the hashed copies and the manifest:
$ python manage.py collectstatic --noinput --settings=config.settings.prod
130 static files copied to '.../staticfiles', 130 post-processed.
$ ls staticfiles/admin/css | head -4
autocomplete.5a22097eaab1.css
autocomplete.css
base.6398cd16ee3d.css
base.css
$ ls staticfiles/staticfiles.json
staticfiles/staticfiles.json
Using and testing the aliases (this test ran on 6.1.1 together with the others on this page):
import tempfile
from pathlib import Path
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage, storages
from django.test import SimpleTestCase, override_settings
class StoragesTests(SimpleTestCase):
def test_default_aliases_exist(self):
self.assertEqual(sorted(settings.STORAGES), ["default", "exports", "staticfiles"])
def test_exports_alias_is_a_separate_storage(self):
exports = storages["exports"]
self.assertEqual(Path(exports.location).name, "private_exports")
self.assertNotEqual(exports.location, default_storage.location)
def test_override_media_root(self):
with tempfile.TemporaryDirectory() as tmp, override_settings(MEDIA_ROOT=tmp):
name = default_storage.save("hello.txt", ContentFile(b"hi"))
self.assertTrue((Path(tmp) / name).exists())
Source: Django: storages
Notes:
-
Third-party backends plug in the same way: django-storages (S3, Azure, Google Cloud; see the django-storages documentation) and WhiteNoise’s
CompressedManifestStaticFilesStorage(see WhiteNoise: Using WhiteNoise with Django) are configured asBACKENDplusOPTIONS. Their classifiers lag the Django release, so check their changelogs before upgrading. -
Because
exportsabove has nobase_urlof its own, it would resolve URLs fromMEDIA_URL; give private storages no public route at all and serve them through an authenticated view. -
Uploaded files are untrusted data: see File Uploads & Storage for validation and custom storages, and Static Files & Front-End Assets for
collectstatic, WhiteNoise and CDNs. -
Migrating from the removed settings is mechanical:
DEFAULT_FILE_STORAGE = "x"becomesSTORAGES["default"] = {"BACKEND": "x"},STATICFILES_STORAGE = "y"becomesSTORAGES["staticfiles"] = {"BACKEND": "y"}. On 5.1 and later the old names are silently ignored, so a forgotten one is a bug, not an error.
DATABASES
DATABASES maps an alias to a dictionary of connection settings; a default alias is required and any number of others
may follow (see Transactions, Raw SQL & Multiple Databases
for routers). Django 6.1 supports PostgreSQL 15 or newer (14 was dropped), MariaDB 10.11+, MySQL 8.4+, SQLite 3.37+ and
Oracle.
Every inner option
| Key | Default | Meaning |
|---|---|---|
|
|
The backend: |
|
|
Database name; for SQLite the file path (forward slashes on Windows). |
|
|
Connection parameters (not used by SQLite). An empty |
|
|
Extra, backend-specific parameters; for PostgreSQL they are passed to the driver’s connection constructor. |
|
|
Wrap every view in a transaction on this database. |
|
|
Set |
|
|
Connection lifetime in seconds; |
|
|
Check a reused persistent connection once per request before using it; re-connect transparently if the server dropped it. |
|
|
The connection’s time zone; leave unset unless a third-party system forces a local zone. Error when |
|
|
PostgreSQL only: disable server-side cursors for |
|
|
Options for the test database: |
Source: Django: DATABASES
Related top-level settings: DATABASE_ROUTERS (route reads/writes among aliases), DEFAULT_TABLESPACE and
DEFAULT_INDEX_TABLESPACE, and MIGRATION_MODULES.
PostgreSQL
This is the configuration Bookshelf uses in staging and production. It was loaded and passed manage.py check here, but
could not be connected to (no PostgreSQL server in the sandbox), so everything past check is reasoned from the
documentation rather than observed:
# config/settings/postgres.py -- needs a PostgreSQL server and psycopg[pool]
import os
from .dev import * # noqa: F403
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB", "bookshelf"),
"USER": os.environ.get("POSTGRES_USER", "bookshelf"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""), # from the secret store, never committed
"HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"),
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
"CONN_MAX_AGE": 0, # must be 0 when "pool" is enabled
"CONN_HEALTH_CHECKS": True,
"OPTIONS": {
"pool": {"min_size": 2, "max_size": 10, "timeout": 10},
"sslmode": os.environ.get("POSTGRES_SSLMODE", "prefer"),
},
"TEST": {"NAME": "test_bookshelf"},
},
}
$ DJANGO_SETTINGS_MODULE=config.settings.postgres python manage.py check
System check identified no issues (0 silenced).
$ DJANGO_SETTINGS_MODULE=config.settings.postgres python manage.py migrate # psycopg without the pool extra
django.core.exceptions.ImproperlyConfigured: Error loading psycopg_pool module.
Did you install psycopg[pool]?
Source: Django: PostgreSQL notes
The second command is a real pitfall that appeared during the run: "pool" needs the psycopg[pool] extra, not just
psycopg. The surrounding facts, all from the database notes:
-
Persistent connections.
CONN_MAX_AGE > 0reuses a connection per thread across requests. Django closes it at the start or end of a request if it exceeded its age or is unusable. Your database must allow at least as many connections as you have worker threads, the development server defeats persistence (it makes a thread per request), and the parameters Django sets on connect are not re-applied if you change them on a live connection. -
ASGI. The documentation says persistent connections should be disabled under ASGI; use the backend’s connection pool (5.1+, psycopg only,
"pool": Trueor a dict ofConnectionPooloptions) or an external pooler instead. With a pool,CONN_MAX_AGEmust stay0andmax_lifetimecontrols connection age;CONN_HEALTH_CHECKSdecides whether connections are checked on checkout unless you supply acheckcallable. -
Health checks.
CONN_HEALTH_CHECKS = Truehelps after a database restart or a server-side idle timeout: the check runs at most once per request, and only for requests that touch the database. -
Poolers. Behind PgBouncer in transaction-pooling mode set
DISABLE_SERVER_SIDE_CURSORS = True, because server-side cursors do not survive a change of backend connection. -
Other PostgreSQL options documented for
OPTIONS:isolation_level(importIsolationLevelfromdjango.db.backends.postgresql.psycopg_any; applications must then handle serialization failures),assume_role,server_side_binding(psycopg 3.1.8+), aservicename with.pg_service.conf/.pgpass, andsslmodeand the other libpq parameters. Django setsclient_encoding, isolation level andtimezoneon each connection unless they already have the right value, so configuring them per role withALTER ROLEsaves a few queries per connection. -
Driver. psycopg 3 support arrived in 4.2; use
psycopg[binary]for development orpsycopg[c]in production images (psycopg2 is still accepted, but new projects should start on psycopg 3).
More PostgreSQL-specific features (django.contrib.postgres, full-text search) are in
PostgreSQL & Database Backends; the schema side is in
Migrations.
SQLite, and what this page ran
SQLite is the zero-setup backend and the one every example here used. Two options matter once more than one request is writing:
# config/settings/sqlite_tuned.py
from .dev import * # noqa: F403
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3", # noqa: F405
"OPTIONS": {
"timeout": 20, # seconds to wait on a locked database
"transaction_mode": "IMMEDIATE", # take the write lock when a transaction begins
},
},
"analytics": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "analytics.sqlite3", # noqa: F405
},
}
$ DJANGO_SETTINGS_MODULE=config.settings.sqlite_tuned python manage.py shell -c "
from django.db import connections
for alias in connections: print(alias, connections[alias].settings_dict['OPTIONS'], connections[alias].vendor)"
default {'timeout': 20, 'transaction_mode': 'IMMEDIATE'} sqlite
analytics {} sqlite
Source: Django: SQLite notes
timeout only makes SQLite wait longer before "database is locked"; it does not remove contention. transaction_mode
accepts DEFERRED (default), IMMEDIATE and EXCLUSIVE; IMMEDIATE makes transactions wait for the lock instead of
failing late, and the notes advise against ATOMIC_REQUESTS in that case because the lock is then held for the whole view.
Treat SQLite as a development and test database for Bookshelf; it has no SELECT … FOR UPDATE and limited
concurrency.
The settings reference: highlights
ref/settings/ documents several hundred settings; most projects touch perhaps forty. Grouped by what they protect or
control, with the version that introduced or changed the ones that matter in 6.1:
| Group | Settings | Notes |
|---|---|---|
Identity and safety |
|
|
Wiring |
|
Order matters for middleware and for apps that override templates or commands. |
Data |
|
See the sections above. |
Files |
|
|
HTTP and security headers |
|
Most are acted on by |
Content Security Policy |
|
Built in since 6.0 (middleware |
Sessions and cookies |
|
Set the |
Authentication |
|
|
|
|
|
Background work and caching |
|
|
Internationalisation |
|
|
Errors and logging |
|
Review |
Testing |
|
Source: Django: Settings reference
The 6.1 transitional settings deserve a separate note because they are meant to disappear: USE_BLANK_CHOICE_DASH (new in
6.1: the default blank label of select widgets changed, and the setting restores the old dashes; it is already
deprecated and goes away in Django 2028, replace it by overriding django.db.models.fields.BLANK_CHOICE_LABEL in an
app’s ready()), SIGNED_COOKIE_LEGACY_SALT_FALLBACK (its default became False in 6.1 and the setting is deprecated), and the
URLIZE_ASSUME_HTTPS setting from 6.0. Transitional settings are the only settings you should ever set for a release or two and then delete.
A production-grade highlights module
This module extends the production settings with the cache, logging, CSP, proxy and check-silencing settings from the table.
Redis is shown only as a comment, since no Redis server was available; the LocMemCache default was what ran.
# config/settings/highlights.py
import os
from django.utils.csp import CSP
from .prod import * # noqa: F403
CACHES = {
"default": {
# Redis in production: "django.core.cache.backends.redis.RedisCache"
# with "LOCATION": "redis://redis:6379/1" (needs redis-py; not run here).
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"KEY_PREFIX": "bookshelf",
"TIMEOUT": 300,
}
}
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": os.environ.get("DJANGO_LOG_LEVEL", "INFO")},
}
# Content Security Policy (built in since 6.0) -- needs the middleware as well.
MIDDLEWARE = [*MIDDLEWARE, "django.middleware.csp.ContentSecurityPolicyMiddleware"] # noqa: F405
SECURE_CSP = {
"default-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:"],
}
# Only when a proxy that you control sets (and strips) this header.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# The load balancer redirects HTTP to HTTPS, so Django does not have to.
SECURE_SSL_REDIRECT = False
SILENCED_SYSTEM_CHECKS = ["security.W008"] # a documented decision, not an oversight
$ python manage.py check --deploy --settings=config.settings.highlights
System check identified no issues (1 silenced).
Source: Django: SECURE_CSP
Details worth knowing about those settings: SECURE_CSP values may be lists, tuples or sets (sets are sorted for stable
output), Django does not validate directives for you, and a CSP.NONCE source needs the csp context processor
(otherwise the security.W027 check fires). The full treatment is on the Content Security Policy page of this section.
Without SILENCED_SYSTEM_CHECKS, the same module reports security.W008 (SSL redirect not enabled); silencing a check
is a recorded decision, not a way to make a warning go away.
Your own settings
Name them in upper case, give them a default in code (not only in settings.py), document them, and validate them with
a system check so a typo fails at manage.py check rather than in production:
# shop/conf.py
from django.conf import settings
def reviews_per_page() -> int:
"""Read at call time, never at import time, so override_settings works."""
return getattr(settings, "BOOKSHELF_REVIEWS_PER_PAGE", 20)
# shop/checks.py
from django.conf import settings
from django.core.checks import Error, Tags, register
@register(Tags.compatibility)
def check_reviews_per_page(app_configs, **kwargs):
value = getattr(settings, "BOOKSHELF_REVIEWS_PER_PAGE", 20)
if not isinstance(value, int) or value < 1:
return [
Error(
"BOOKSHELF_REVIEWS_PER_PAGE must be a positive integer.",
hint="Set it to e.g. 20 in your settings module.",
id="shop.E001",
)
]
return []
# shop/apps.py -- register the check when the app is ready
from django.apps import AppConfig
class ShopConfig(AppConfig):
name = "shop"
default_auto_field = "django.db.models.BigAutoField"
def ready(self):
from . import checks # noqa: F401 (importing registers the check)
$ python manage.py check
System check identified no issues (0 silenced).
# with BOOKSHELF_REVIEWS_PER_PAGE = 0 in settings:
$ python manage.py check
SystemCheckError: System check identified some issues:
ERRORS:
?: (shop.E001) BOOKSHELF_REVIEWS_PER_PAGE must be a positive integer.
HINT: Set it to e.g. 20 in your settings module.
System check identified 1 issue (0 silenced).
Verifying production settings: check --deploy
manage.py check --deploy runs the system checks plus the ones that only make sense in production: HSTS, SSL redirect,
secure cookies, DEBUG, ALLOWED_HOSTS, the strength of SECRET_KEY and every fallback, the clickjacking and
content-type middleware, the referrer policy, CSP configuration, and, since 6.1, the mailer (mail.E001 flags a
development-only email backend in the default mailer; mail.W001 flags a missing default entry). Point it at the production settings with
--settings or the environment variable, never at the development module (which has none of the production values), and
use --fail-level WARNING in CI to turn warnings into a non-zero exit status (the default level is ERROR).
$ python manage.py check --deploy --settings=config.settings.prod
SystemCheckError: System check identified some issues:
ERRORS:
?: (mail.E001) Your MAILERS setting uses a development-only email backend in the 'default' entry (django.core.mail.backends.locmem.EmailBackend).
HINT: Use a production-ready email backend, such as the SMTP backend, otherwise email will not be sent.
System check identified 1 issue (0 silenced).
Source: Django: check --deploy
That error came from an earlier draft of the production module that used the in-memory email backend; replacing it with the
SMTP mailer shown above made check --deploy --fail-level WARNING exit with status 0. The most common findings and
their fixes:
| Check | Fix |
|---|---|
|
Set |
|
|
|
Replace the weak or |
|
|
|
|
|
Set |
|
Configure a production mailer and a |
Related deployment-checklist items that no check can verify for you: keep runserver out of production, protect database
and cache credentials like the secret key, make backups, treat MEDIA_ROOT as untrusted (a web server must never execute an uploaded
file), turn on persistent connections or a pool, use cached sessions, rely on the cached template loader (enabled
automatically when DEBUG is False), and test error reporting. The complete list is
Deployment; error reporting is in
Logging & Error Reporting.
Testing code that depends on settings
Django’s test framework swaps settings for the duration of a test and restores them afterwards:
-
@override_settings(NAME=value)on a method, a class or aswith self.settings(…); -
@modify_settings(INSTALLED_APPS={"append": "…"})to add or remove items of list settings (applied afteroverride_settingswhen both decorate one class); -
the
setting_changedsignal, sent twice (apply and restore) withsetting,valueandenter, so a component that caches a setting can refresh itself; it is importable fromdjango.core.signalsoutside test code; -
with pytest-django, the
settingsfixture (settings.USE_TZ = Trueinside a test, undone automatically).
from django.test import SimpleTestCase, TestCase, override_settings
from django.test.signals import setting_changed
class CustomSettingTests(SimpleTestCase):
def test_default_and_override(self):
from shop.conf import reviews_per_page
self.assertEqual(reviews_per_page(), 20)
with override_settings(BOOKSHELF_REVIEWS_PER_PAGE=5):
self.assertEqual(reviews_per_page(), 5)
def test_check_reports_bad_value(self):
from shop.checks import check_reviews_per_page
with override_settings(BOOKSHELF_REVIEWS_PER_PAGE=0):
self.assertEqual([e.id for e in check_reviews_per_page(None)], ["shop.E001"])
class OverrideSettingsTests(TestCase):
@override_settings(DEBUG=True, ALLOWED_HOSTS=["testserver"])
def test_decorator(self):
from django.conf import settings
self.assertTrue(settings.DEBUG)
def test_setting_changed_signal(self):
seen = []
def receiver(sender, setting, value, enter, **kwargs):
if setting == "LANGUAGE_CODE":
seen.append((value, enter))
setting_changed.connect(receiver)
try:
with self.settings(LANGUAGE_CODE="es"):
pass
finally:
setting_changed.disconnect(receiver)
self.assertEqual(seen, [("es", True), ("en-us", False)])
$ python manage.py test
Found 9 test(s).
System check identified no issues (0 silenced).
.........
----------------------------------------------------------------------
Ran 9 tests in 0.016s
OK
Source: Django: Overriding settings
Pitfalls: a value copied at import time (PAGE_SIZE = settings.BOOKSHELF_REVIEWS_PER_PAGE at module level) ignores
override_settings; code in AppConfig.ready() runs once, so a setting read there needs a setting_changed receiver to follow an override;
the documentation warns that settings consulted only during Django’s initialisation do not behave as expected under
override_settings, recommends against altering DATABASES, and calls altering CACHES tricky; for those, choose a
settings module (--settings=config.settings.test) instead. For the pytest
setup see the pytest-django page of this section ( pytest, Factories & Browser Tests )
and the pytest-django helpers documentation.
Pitfalls checklist
-
DEBUG = TrueorALLOWED_HOSTS = ["*"]in production; the first leaks source and settings, the second needs your ownHostvalidation. -
A secret (or a "temporary" production password) committed to Git. Rotate it; deleting the commit is not enough.
-
Overriding
STORAGESwith only one alias, or leaving a removedDEFAULT_FILE_STORAGE/STATICFILES_STORAGEbehind (ignored since 5.1). -
CSRF_TRUSTED_ORIGINSwithout a scheme (an error since 4.0) or withhttp://for a site served over HTTPS. -
Trusting
SECURE_PROXY_SSL_HEADERwhen the proxy does not strip the header from client requests. -
Using
SECURE_SSL_REDIRECTbehind a TLS-terminating proxy withoutSECURE_PROXY_SSL_HEADER, which produces an infinite redirect loop. -
Persistent connections on ASGI or against a pooler;
CONN_MAX_AGEabove 0 together with"pool". -
from .base import *followed by in-place mutation of an imported list. -
Reading settings into module-level constants, or mutating
settingsat runtime. -
Pointing
DJANGO_SETTINGS_MODULEat a package instead of a module; a typo in the module name that happens to resolve. -
A default for
SECRET_KEY"so that development just works" that ends up in production. -
Changing
DEFAULT_AUTO_FIELDon a project that already has migrations without planning the data change. -
Treating
SILENCED_SYSTEM_CHECKSas a way to make CI green.
Where the books are outdated
The four reference books for this section target Django 1.11 to 3.x. For settings the differences are large:
| Older practice | Current practice | Changed in |
|---|---|---|
One |
Environment-driven or split settings; secrets outside the repository; |
always wrong |
|
|
removed in 2.0 |
|
|
current template |
No |
|
3.2 / 6.0 |
|
|
4.2, old names removed in 5.1 |
|
|
4.0 / 5.0 |
|
Origins with a scheme ( |
4.0 |
psycopg2 and |
psycopg 3 ( |
4.2 / 3.1 |
Single |
|
4.1 |
|
|
6.1 (old ones deprecated) |
|
|
6.0 |
Celery as the only answer for background work |
The |
6.0 |
Deployment recipes written for Heroku’s free tier |
Environment variables read with |
ecosystem |
Pipenv, |
|
ecosystem |
Source: Django: Deprecation timeline
What changed recently
| Version | Change relevant to settings |
|---|---|
3.2 |
|
4.0 |
|
4.1 |
|
4.2 |
|
5.0 |
|
5.1 |
|
5.2 (LTS) |
Last release without |
6.0 |
|
6.1 |
|
Source: Django 6.1 release notes
When upgrading, run the test suite with python -Wd manage.py test (all warnings shown)
before bumping the version, so deprecated settings surface as failures while the old behaviour
still works; the per-release notes list the deprecations in a "Features deprecated" section, and the deprecation
timeline names the release that will remove each one. 6.2 (the next LTS, planned for April 2027) is the last release
under the old versioning; from 2028 releases are named by year.