env.dev

Django Environment Variables: settings.py Patterns

Configure Django with os.environ and django-environ: DEBUG, SECRET_KEY, and DATABASE_URL in settings.py, .env files in dev, real env vars in production.

By env.dev Updated

Django has no built-in .env loader — settings.py is plain Python, and the framework expects you to pull configuration from os.environ yourself. That design has not changed from Django 1.0 through Django 6.0 (December 3, 2025), which is why three competing libraries — django-environ, python-dotenv, and python-decouple — all exist to fill the same gap. Get the wiring wrong and the failure modes are not subtle: a string-truthiness bug that leaves DEBUG=True in production, or a rotated SECRET_KEY that silently logs out every user.

TL;DR

  • Django reads nothing automatically: configuration enters through os.environ in settings.py, or a library that populates it.
  • os.getenv('DEBUG') returns a string, and every non-empty string — including "False" — is truthy in Python. Cast explicitly or use a library that does.
  • django-environ 0.13 (tested against Django 2.2–6.0) is the Django-native pick: typed casts plus DATABASE_URL parsing in one call.
  • Use a .env file in development; in production set real environment variables via systemd, Docker, or your platform — don't ship the file.
  • Rotate SECRET_KEY with SECRET_KEY_FALLBACKS (Django 4.1+) to avoid invalidating every session at once.

How does Django read environment variables?

Through the standard library, like any Python program. settings.py executes once at startup, so anything you read there is fixed for the process lifetime:

python
# settings.py
import os

# Raises KeyError at startup if missing — good for required values
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# Returns None (or the default) if missing — good for optional values
EMAIL_HOST = os.getenv("EMAIL_HOST", "localhost")

# The classic bug: bool("False") is True. Compare strings explicitly.
DEBUG = os.getenv("DJANGO_DEBUG", "false").lower() in ("true", "1", "yes")

ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",")

Django itself consumes one environment variable you have already used: DJANGO_SETTINGS_MODULE, which manage.py sets to point at your settings file. Prefer failing loudly — os.environ["KEY"] for anything the app cannot run without — over a default that papers over a missing production value. The Python env variables guide covers os.environ mechanics beyond Django.

django-environ vs python-dotenv vs python-decouple: which should you use?

All three load a .env file; they differ in what happens next.

LibraryStatus (June 2026)Type castingDATABASE_URL parsing
django-environ0.13.0, tested on Django 2.2–6.0Yes (bool, int, list, dict…)Yes — env.db(), env.cache()
python-dotenv1.2.2 (March 2026), most activeNo — everything stays a stringNo
python-decouple3.8, unchanged since 2023Yes — cast=bool, Csv()No (pair with dj-database-url)

Opinionated take: for a Django project, pick django-environ — it is the only one that understands Django's config shapes, turning a single DATABASE_URL into the nested DATABASES dict and a REDIS_URL into CACHES. python-dotenv is the better generic tool (and the most actively maintained — its 1.2.2 release fixed CVE-2026-28684, a symlink-following issue in set_key()), but in Django it leaves all casting and URL parsing to you. python-decouple's typed config() API is pleasant and the library is essentially finished software, but it has not shipped a release since 2023 and needs dj-database-url alongside it.

What does a django-environ settings.py look like?

python
# settings.py
from pathlib import Path
import environ

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env(
    # declare types and defaults in one place
    DEBUG=(bool, False),
    ALLOWED_HOSTS=(list, []),
)

# Read .env in development; harmless no-op if the file is absent
environ.Env.read_env(BASE_DIR / ".env")

SECRET_KEY = env("SECRET_KEY")          # raises ImproperlyConfigured if unset
DEBUG = env("DEBUG")                    # real bool, cast from "true"/"false"
ALLOWED_HOSTS = env("ALLOWED_HOSTS")    # "a.com,b.com" -> ["a.com", "b.com"]

# postgres://user:pass@host:5432/dbname -> full DATABASES["default"] dict
DATABASES = {"default": env.db("DATABASE_URL")}

# redis://host:6379/0 -> CACHES["default"]
CACHES = {"default": env.cache("REDIS_URL", default="locmemcache://")}
ini
# .env — development values, git-ignored
SECRET_KEY=dev-only-key-change-me
DEBUG=true
ALLOWED_HOSTS=localhost,127.0.0.1
DATABASE_URL=postgres://django:django@localhost:5432/myapp

Note the precedence: read_env() does not overwrite variables that already exist in the process environment, so production values injected by your platform always beat the file. If a .env edit seems ignored, that rule — or a wrong working directory — is almost always why; the dotenv-not-loading debugging guide walks the checklist.

How should DEBUG, SECRET_KEY, and ALLOWED_HOSTS be configured?

  • DEBUG — default it to False and require an explicit opt-in per environment. Debug pages leak settings, SQL, and stack traces; Django's own deployment checklist (manage.py check --deploy) flags a truthy DEBUG first.
  • SECRET_KEY — signs sessions, password-reset tokens, and messages-framework cookies. Generate per environment with get_random_secret_key(), never reuse dev keys in prod, and rotate via SECRET_KEY_FALLBACKS (Django 4.1+): new key in SECRET_KEY, old key appended to the fallbacks list, remove it after sessions age out. Rotating without fallbacks invalidates every session and unexpired password-reset link at once.
  • ALLOWED_HOSTS — must be set when DEBUG=False or Django answers every request with a 400. A comma-separated env var split into a list is the standard pattern.

Should production use .env files or real environment variables?

Real environment variables. The .env file is a development convenience; in production it is one more plaintext secrets file to protect, back up, and accidentally bake into a Docker layer. Inject values through the process manager instead:

ini
# /etc/systemd/system/gunicorn.service
[Service]
# Reference a root-owned file outside the project tree…
EnvironmentFile=/etc/myapp/env
ExecStart=/srv/myapp/.venv/bin/gunicorn myapp.wsgi:application
yaml
# docker-compose.yml — same idea, container edition
services:
  web:
    image: myapp:latest
    environment:
      DJANGO_DEBUG: "false"
      DATABASE_URL: postgres://django:${POSTGRES_PASSWORD}@db:5432/myapp

Platform dashboards (Heroku, Fly.io, Railway, ECS task definitions, Kubernetes Secrets) all amount to the same thing: the variable exists in the process environment before Python starts, and settings.py reads it with zero file I/O. For the broader rules — naming, startup validation, rotation — see the environment variable best practices guide.

When should you not use environment variables in Django?

  • Per-environment Python logic — if staging needs different installed apps or middleware, a settings module per environment (settings/prod.py importing from settings/base.py) is clearer than a thicket of if os.getenv(...) branches. Use env vars for values, settings modules for structure.
  • High-value secrets at scale — environment variables are visible in /proc/PID/environ, error-tracker payloads, and child processes. Past a certain blast radius, fetch from a secrets manager at startup instead; the security guide covers when that line is crossed.
  • Feature flags that change at runtime settings.py is evaluated once per process. Anything you want to flip without a restart belongs in the database or a flag service, not the environment.

Frequently Asked Questions

How do I use a .env file with Django?

Django has no built-in support, so use a library: django-environ (environ.Env.read_env(BASE_DIR / ".env") in settings.py), python-dotenv (load_dotenv() in manage.py and wsgi.py), or python-decouple (config() reads .env automatically). Keep the file git-ignored and only rely on it in development.

Why is DEBUG still True even though my .env says DEBUG=False?

Environment variables are strings, and bool("False") is True in Python — any naive bool() cast or truthiness check keeps debug on. Cast explicitly (os.getenv("DJANGO_DEBUG", "false").lower() in ("true","1")) or declare DEBUG=(bool, False) with django-environ. Also check whether a real environment variable is overriding your file: loaders do not overwrite existing values.

Which is best for Django: django-environ, python-dotenv, or python-decouple?

django-environ for most Django projects — it casts types and parses DATABASE_URL/REDIS_URL into the DATABASES and CACHES structures Django expects, and version 0.13 is tested against Django 6.0. python-dotenv is the most actively maintained but does loading only (no casting). python-decouple has a clean typed API but has not released since 2023.

How do I rotate SECRET_KEY without logging everyone out?

Use SECRET_KEY_FALLBACKS, added in Django 4.1: set the new key as SECRET_KEY and append the old key to the SECRET_KEY_FALLBACKS list. Django signs new values with the new key while still validating old sessions and tokens against fallbacks. Remove the old key once existing sessions have expired.

Where do environment variables go in production Django?

Into the real process environment, not a .env file: EnvironmentFile= in a systemd unit, environment/env_file in Docker Compose, Kubernetes Secrets, or your PaaS dashboard. settings.py then reads os.environ exactly as it does in development — only the injection mechanism changes.

Does DATABASE_URL work natively in Django?

No. Django expects the nested DATABASES dict. The URL convention comes from Heroku, and a parser converts it: django-environ (env.db()) or the standalone dj-database-url package. One URL string is easier to inject and rotate than five separate host/port/name/user/password variables.

References

Check your .env for syntax issues with the env validator, or see the Python env variables guide for os.environ, Pydantic Settings, and Flask patterns.

Was this helpful?

Frequently Asked Questions

How do I use a .env file with Django?

Django has no built-in support, so use a library: django-environ (environ.Env.read_env(BASE_DIR / ".env") in settings.py), python-dotenv (load_dotenv() in manage.py and wsgi.py), or python-decouple (config() reads .env automatically). Keep the file git-ignored and only rely on it in development.

Why is DEBUG still True even though my .env says DEBUG=False?

Environment variables are strings, and bool("False") is True in Python — any naive bool() cast or truthiness check keeps debug on. Cast explicitly (os.getenv("DJANGO_DEBUG", "false").lower() in ("true","1")) or declare DEBUG=(bool, False) with django-environ. Also check whether a real environment variable is overriding your file: loaders do not overwrite existing values.

Which is best for Django: django-environ, python-dotenv, or python-decouple?

django-environ for most Django projects — it casts types and parses DATABASE_URL/REDIS_URL into the DATABASES and CACHES structures Django expects, and version 0.13 is tested against Django 6.0. python-dotenv is the most actively maintained but does loading only (no casting). python-decouple has a clean typed API but has not released since 2023.

How do I rotate SECRET_KEY without logging everyone out?

Use SECRET_KEY_FALLBACKS, added in Django 4.1: set the new key as SECRET_KEY and append the old key to the SECRET_KEY_FALLBACKS list. Django signs new values with the new key while still validating old sessions and tokens against fallbacks. Remove the old key once existing sessions have expired.

Where do environment variables go in production Django?

Into the real process environment, not a .env file: EnvironmentFile= in a systemd unit, environment/env_file in Docker Compose, Kubernetes Secrets, or your PaaS dashboard. settings.py then reads os.environ exactly as it does in development — only the injection mechanism changes.

Does DATABASE_URL work natively in Django?

No. Django expects the nested DATABASES dict. The URL convention comes from Heroku, and a parser converts it: django-environ (env.db()) or the standalone dj-database-url package. One URL string is easier to inject and rotate than five separate host/port/name/user/password variables.

Stay up to date

Get notified about new guides, tools, and cheatsheets.