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.
pip install django-environ.- In settings:
env = environ.Env(DEBUG=(bool, False)). - Read typed values:
DEBUG = env("DEBUG"),ALLOWED_HOSTS = env.list("ALLOWED_HOSTS"),SECRET_KEY = env("SECRET_KEY")(no default, so it is required). - Parse URLs:
DATABASES = {"default": env.db("DATABASE_URL")},CACHES = {"default": env.cache("REDIS_URL")}. - Load
.envonly in development:environ.Env.read_env(BASE_DIR / ".env")guarded by a check, and never commit.env. - 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.
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
.envto.gitignore; commit a.env.examplewith 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 withchmod 600. - Docker: do not
COPY .envinto 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
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
.envfile 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
- Organise environments with Split Django Settings for Development and Production.
- Keep secrets out of files entirely with Manage Django Secrets in Production.
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.