RizTech Academy logo
RizTech Academy
Writing Django Worth ReadingLesson 2 of 525 min

Settings, environments and secrets, done right

Settings are where a project meets the real world — the database it connects to, the secret key it signs with, whether debugging is on. The same code runs on your laptop, on staging, and in production, but its settings must differ between them, and its secrets must never be committed. Getting settings organisation right is a small amount of work that prevents a class of embarrassing (and dangerous) mistakes. This lesson is how to structure settings across environments and handle secrets safely.

The core rule: config from the environment

The foundational principle (from the widely-followed "twelve-factor app" guidelines): anything that differs between environments, or is secret, comes from the environment — not hard-coded in settings. The database URL, the secret key, the debug flag, the allowed hosts, API keys — these vary between your machine and production, or must stay secret, so they are read from environment variables (or a .env file locally), never written into the committed settings.py.

import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]           # required — fail loudly if missing
DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")

Note os.environ["..."] (bracket access) for a required secret — it raises immediately if unset, which is what you want: a missing secret key should stop the app, not silently use a default. Use os.environ.get(..., default) only for genuinely optional settings with a safe default.

Reading .env in development

Typing environment variables into your shell every time is tedious, so in development you keep them in a .env file (which the setup lesson's .gitignore already excludes) and load it:

# .env  — NEVER committed
DJANGO_SECRET_KEY=dev-only-key-abc123
DJANGO_DEBUG=True
DATABASE_URL=postgres://localhost/nidaan

A small library reads it. Two common, sound choices:

  • django-environ — reads .env and parses typed values, including a DATABASE_URL into Django's DATABASES dict:
import environ
env = environ.Env()
environ.Env.read_env()                                 # load .env

SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env.bool("DJANGO_DEBUG", default=False)
DATABASES = {"default": env.db()}                       # parses DATABASE_URL
  • python-dotenv — a lighter option that just loads .env into os.environ.

Either way, the pattern is: secrets and per-environment config live in .env locally (untracked) and in the real environment variables in production (set by your host or deployment tool), and settings.py reads them. The code is identical everywhere; only the values differ.

Organising settings across environments

There are two respectable ways to handle "dev versus production differ":

One settings file, driven by the environment (simpler). A single settings.py that branches on environment variables (DEBUG, DATABASES from env, etc.). This is clean when the differences are just values, and it is the modern lean approach — the file is the same, the environment supplies the differences.

Split settings (for larger differences). A settings/ package with base.py (shared), development.py and production.py (each from .base import * then overriding), selected by DJANGO_SETTINGS_MODULE:

config/settings/
  base.py          # everything shared
  development.py   # DEBUG=True, dev tools, console email
  production.py    # DEBUG=False, security settings on, real email

Split settings suit projects where environments differ structurally (different installed apps, different middleware, different email backends) rather than just in values. For many projects the single-file, env-driven approach is enough; reach for split settings when the environments genuinely diverge. Either way, secrets still come from the environment — splitting files is about structure, not about putting secrets in a production.py (never do that).

What must never be committed — and what to do if it was

The non-negotiables:

  • SECRET_KEY, database passwords, API keys, tokens — never in the repository, ever. A secret in git is compromised the moment it is pushed, and it lives in history forever (deleting it in a later commit does not remove it from history).
  • .env — always in .gitignore; commit a .env.example (with dummy values and the names of the variables) instead, so a new developer knows what to set without seeing real secrets.

If a real secret is accidentally committed, the only safe response is to rotate it — generate a new secret key, change the database password, revoke and reissue the API key. Scrubbing it from git history is secondary and often impractical (forks, clones, caches already have it); assume anything pushed is compromised and replace it. This is why the .gitignore goes in before the first commit — prevention is the only reliable cure.

Check your work

The core rule. Anything that differs between environments or is secret comes from the environment, not hard-coded — database, SECRET_KEY, DEBUG, ALLOWED_HOSTS, API keys.

Required versus optional. os.environ["X"] for required secrets (fails loudly if missing); os.environ.get("X", default) only for optional settings with a safe default.

.env in development. Keep secrets/config in an untracked .env, loaded by django-environ or python-dotenv; production uses real environment variables. Same code, different values.

Organising settings. Single env-driven settings.py when environments differ only in values; a split settings/ package (base/development/production) when they differ structurally — secrets still from the environment either way.

Never commit secrets. SECRET_KEY, passwords, keys, and .env stay out of git (commit .env.example); a committed secret lives in history forever and must be rotated, not just deleted.

Practice

  1. Move SECRET_KEY, DEBUG and ALLOWED_HOSTS to environment variables read in settings.py; run the app with them set via a .env.
  2. Use os.environ["DJANGO_SECRET_KEY"] and unset the variable; confirm the app fails loudly (and reason about why that is better than a default).
  3. Install django-environ, load a DATABASE_URL from .env with env.db(), and confirm the connection.
  4. Create a .env.example with variable names and dummy values; confirm .env is gitignored but .env.example is committed.
  5. Sketch when you would split settings into base/development/production versus keep one env-driven file, for Nidaan.
  6. Reason through the correct response to accidentally committing a real SECRET_KEY — and why rotation, not deletion, is the fix.

Official documentation

Next: query discipline — performance you can see.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship