skip to content

How do you give a Django project different settings for development, staging and production, and how does Django decide which settings module to load?

level: middleimportance: must knowfreq 60%

answer

  1. one environment variable names it
  2. setdefault in manage.py and wsgi.py
  3. base module plus overrides
  4. environment variables are strings
  5. fail fast on missing secrets

basics

~20 s

Django loads the settings module named by the DJANGO_SETTINGS_MODULE environment variable, or a command's --settings option. Per-environment setups use a shared base module with small dev, staging and prod modules, or one module reading environment variables, with secrets always from the environment.

solid answer

~40 s

Django imports whichever module `DJANGO_SETTINGS_MODULE` names. The generated `manage.py`, `wsgi.py` and `asgi.py` call `os.environ.setdefault("DJANGO_SETTINGS_MODULE", "payroll.settings")`, so a real environment variable wins, and every management command also accepts `--settings`. Two patterns are common: a settings package with `base.py` plus `dev.py`, `staging.py` and `prod.py` that do `from .base import *` and override a few values, or one `settings.py` reading everything that differs from `os.environ`. Either way, secrets such as `SECRET_KEY` and database passwords come from the environment, never the repository. Read them with `os.environ["DJANGO_SECRET_KEY"]` so a missing value fails at startup; parse strings deliberately, because `ALLOWED_HOSTS` must be a list and `bool("False")` is `True`.

code

python · 18 lines
python
# payroll/settings/base.py (excerpt)
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # KeyError at startup if missing
DEBUG = os.environ.get("DJANGO_DEBUG", "") == "1"
ALLOWED_HOSTS = [
    h.strip() for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h.strip()
]
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["PAYROLL_DB_NAME"],
        "USER": os.environ["PAYROLL_DB_USER"],
        "PASSWORD": os.environ["PAYROLL_DB_PASSWORD"],
        "HOST": os.environ.get("PAYROLL_DB_HOST", "localhost"),
        "PORT": int(os.environ.get("PAYROLL_DB_PORT", "5432")),
    }
}

go deeper

for a junior

Know that DJANGO_SETTINGS_MODULE chooses the settings module and that secrets belong in environment variables, not the repository.

for a middle

Explain setdefault in manage.py and wsgi.py, the --settings option, and the base-plus-overrides versus single-module patterns.

for a senior

Parse environment strings strictly, fail fast on missing secrets, and keep staging close enough to production that it catches configuration bugs.

for a principal

Choose a configuration policy for many services: one artifact driven by environment, documented variables, and defaults that can only fail closed.

## How Django picks the settings module Django has exactly one active settings module per process, and it finds it through the **`DJANGO_SETTINGS_MODULE`** environment variable, which holds a dotted Python path such as `payroll.settings.prod`. - `manage.py`, `wsgi.py` and `asgi.py` generated by `startproject` call `os.environ.setdefault("DJANGO_SETTINGS_MODULE", "payroll.settings")`. Because it is `setdefault`, a variable already set in the environment **wins** over the hard-coded default. - Every management command accepts **`--settings payroll.settings.staging`**, which sets the variable for that run. - Scripts and tools that run outside those entry points must set the variable, or call `settings.configure()`. The module is imported lazily the first time a setting is read, and Django overlays its uppercase names on the defaults in `global_settings.py`. ## Pattern 1: a base module plus per-environment modules ```text payroll/settings/ __init__.py base.py # everything shared: INSTALLED_APPS, MIDDLEWARE, TEMPLATES dev.py # from .base import *; DEBUG = True; local database staging.py # from .base import *; staging hosts; real email disabled prod.py # from .base import *; DEBUG = False; production hosts ``` - **Pros:** differences are visible in code review; each file is short. - **Cons:** star imports hide where a name came from, and it is easy for staging and prod to drift. ## Pattern 2: one module driven by environment variables A single `settings.py` reads every environment-specific value from `os.environ`. The same artifact runs everywhere, and only the environment differs. - **Pros:** one code path; staging really tests production settings. - **Cons:** the settings module must parse and validate strings carefully. Many teams combine them: a base module plus a thin production module, with all secrets and hostnames in environment variables either way. ## Reading environment variables correctly Environment variables are always **strings**, and Django validates only a few settings: | Setting | Pitfall | Safer form | |---|---|---| | `SECRET_KEY` | a silent default ships a known key | `os.environ["DJANGO_SECRET_KEY"]` raises `KeyError` at startup if missing | | `DEBUG` | `bool("False")` is `True` | `os.environ.get("DJANGO_DEBUG", "") == "1"` | | `ALLOWED_HOSTS` | a raw string raises `ImproperlyConfigured: The ALLOWED_HOSTS setting must be a list or a tuple.` | split on commas and drop empty items | | database port or timeouts | strings where numbers are expected | convert with `int()` explicitly | Django also refuses an empty key: reading `settings.SECRET_KEY` when it is empty raises `ImproperlyConfigured: The SECRET_KEY setting must not be empty.` Fail-fast reads turn a misconfigured deployment into a crash at startup instead of a subtly insecure service. ## Rules that keep the split sane 1. **Production-safe defaults** — a missing variable should make the service stricter (`DEBUG` off), never looser. 2. **No secrets in the repository**, not even "only for dev"; generate a dev key locally. 3. **Do not import `global_settings`** in a settings file; Django applies the defaults for you, and the docs say a settings file should not import it. 4. **Assign settings only in settings modules**; the docs warn against changing them at runtime. 5. **Keep the list of environment variables documented** next to the settings, so a new environment can be built from it. ## What an interviewer is listening for A middle candidate names `DJANGO_SETTINGS_MODULE`, knows the `setdefault` in `manage.py` and `wsgi.py`, and describes one of the two patterns. A stronger one covers string parsing, fail-fast secrets, and why production-safe defaults matter.

  • Why does manage.py use os.environ.setdefault rather than assigning the variable?
    `setdefault` only fills in the variable when it is not already set, so a deployment can export `DJANGO_SETTINGS_MODULE=payroll.settings.prod` and override the development default baked into `manage.py` and `wsgi.py`. Assigning it unconditionally would silently ignore the environment and load the wrong settings.
  • What goes wrong with os.environ.get('DJANGO_SECRET_KEY', 'dev-key')?
    A missing variable in production silently falls back to a key committed to the repository, so anyone with the code can forge signed values. Reading with `os.environ[...]` in production settings makes the process fail at startup instead, which is the failure you want.

saying these in an interview costs you the question

  • Commits a real SECRET_KEY to the repository for convenience
  • Assigns ALLOWED_HOSTS the raw comma-separated environment string
  • Gives SECRET_KEY a hard-coded fallback in production settings
  • Believes the setdefault in manage.py overrides the real environment
  • Imports global_settings into a settings module to extend defaults