Configure Django Settings With django-environ

Use django-environ to read Django settings from environment variables and .env files: typed values for DEBUG and ALLOWED_HOSTS, DATABASE_URL and CACHE_URL parsing, required variables, safe .env handling, and when to skip .env in production.

Problem statement

Environment variables are always strings. os.environ["DEBUG"] returns "False", which is truthy in Python, and many production incidents have started with exactly that. Lists such as ALLOWED_HOSTS, database URLs, and cache URLs need parsing too. django-environ reads environment variables with types and defaults, parses DATABASE_URL-style strings into Django's settings dictionaries, and can load a .env file for local development.

From env vars to settings.env or real envstrings onlyenviron.Envtypes and defaultsenv.bool, env.listDEBUG, hostsenv.db, env.cacheURL parsingdjango-environ turns string variables into typed settings and fails on missing required values.
  1. pip install django-environ.
  2. In settings: env = environ.Env(DEBUG=(bool, False)).
  3. Read typed values: DEBUG = env("DEBUG"), ALLOWED_HOSTS = env.list("ALLOWED_HOSTS"), SECRET_KEY = env("SECRET_KEY") (no default, so it is required).
  4. Parse URLs: DATABASES = {"default": env.db("DATABASE_URL")}, CACHES = {"default": env.cache("REDIS_URL")}.
  5. Load .env only in development: environ.Env.read_env(BASE_DIR / ".env") guarded by a check, and never commit .env.
  6. In production, set real environment variables through systemd, Docker, Kubernetes, or the platform.

Step-by-step solution

1. Basic setup

# config/settings/base.py
from pathlib import Path
import environ

BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env(
    DEBUG=(bool, False),
    ALLOWED_HOSTS=(list, []),
)

ENV_FILE = BASE_DIR / ".env"
if ENV_FILE.exists():
    environ.Env.read_env(ENV_FILE)

DEBUG = env("DEBUG")
SECRET_KEY = env("SECRET_KEY")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")

read_env does not override variables that are already set in the real environment, so production values always win over a stray .env file.

2. Typed values

DEBUG = env.bool("DEBUG", default=False)            # "true", "1", "yes", "on" -> True
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")           # "a.com,b.com" -> ["a.com", "b.com"]
CONN_MAX_AGE = env.int("CONN_MAX_AGE", default=60)
SENTRY_SAMPLE = env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.1)
ADMINS = [("Ops", e) for e in env.list("ADMIN_EMAILS", default=[])]

Calling env("SECRET_KEY") without a default raises ImproperlyConfigured when the variable is missing, with a message naming it. Give defaults only to values that are safe in production.

string in, typed setting outos.environdjango-environDEBUG="False"✕"False" (truthy!)✓FalseALLOWED_HOSTS="a.com,b.com"one string✓["a.com", "b.com"]DATABASE_URL="postgres://..."one string✓DATABASES dictSECRET_KEY missingKeyError or None✓ImproperlyConfiguredTyped reads remove the classic DEBUG="False" bugdjango-deployment.com
Use a typed reader for every non-string setting.

3. Parse service URLs

DATABASES = {"default": env.db("DATABASE_URL")}
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)

CACHES = {"default": env.cache("REDIS_URL", default="locmemcache://")}
EMAIL_CONFIG = env.email("EMAIL_URL", default="consolemail://")
vars().update(EMAIL_CONFIG)

env.db understands postgres://, mysql://, sqlite:///, and others, including query parameters such as ?sslmode=require. Passwords with special characters must be URL-encoded in the URL.

4. Handle .env files safely

  • Add .env to .gitignore; commit a .env.example with variable names and safe placeholder values.
  • In production, prefer real environment variables. If you do use a file (for example a systemd EnvironmentFile), keep it outside the project directory with chmod 600.
  • Docker: do not COPY .env into images; add it to .dockerignore.

5. Document required variables

Keep a single list of variables the app needs, with types and examples, in .env.example:

SECRET_KEY=change-me
DEBUG=false
ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgres://app:password@db:5432/app
REDIS_URL=redis://redis:6379/0
environment settings without surprisesTyped reads for bools, ints, listsenv.bool, env.int, env.listNo default for secretsmissing = startup error.env only for local developmentreal env vars in production.env in .gitignore and .dockerignore.env.example committedURL-encoded passwords in URLsspecial characters break parsingThe library helps only if secrets stay out of files that get copied arounddjango-deployment.com
Review .env.example in code review whenever a new setting appears.

Verification check: in production, python manage.py shell -c "from django.conf import settings; print(settings.DEBUG, settings.ALLOWED_HOSTS)" prints False and the real hosts; unsetting SECRET_KEY and starting the app fails with an ImproperlyConfigured message naming the variable.

Explanation

The twelve-factor approach stores configuration in the environment, but environments only hold strings. django-environ provides a thin, Django-aware parsing layer: typed accessors, URL schemes for databases, caches, and email, and optional loading of a .env file that never overrides real variables. That keeps settings files short and declarative while making type mistakes and missing values fail early. Similar libraries such as python-decouple and pydantic-settings follow the same principles.

Edge cases / notes

  • Booleans. Only recognised true values ("true", "on", "yes", "1") become True; anything else is False, so typos fail safe.
  • Lists with spaces. "a.com, b.com" keeps the spaces in some versions; avoid spaces or strip values.
  • Docker Compose also reads a .env file for variable substitution in the compose file itself, which is separate from the app's .env.
  • Secrets managers can populate environment variables at runtime; django-environ then reads them like any other variable.

Next steps

FAQ

Why is DEBUG still True when I set DEBUG=False?

os.environ returns the string "False", which is truthy. Read it with env.bool.

Should I use a .env file in production?

Prefer real environment variables. If you use a file, keep it outside the project with strict permissions and never in images or Git.

How do I parse DATABASE_URL in Django?

With env.db("DATABASE_URL"), which returns a Django DATABASES entry.

django-environ or python-decouple?

Both work. django-environ adds URL parsers for databases, caches, and email; decouple is smaller and framework-agnostic.

Keep going