Environment Configuration
settings.py has quietly accumulated some genuinely sensitive values across this track —
the SECRET_KEY from CSRF & Security, a
database password, potentially the credentials the Celery broker or an email backend need to
authenticate. Every one of those has, so far, been written as a plain string directly in the file.
That's fine for following along — it's a real problem the moment that file is ever committed to
version control, because at that point the secret is sitting in your Git history, readable by anyone
with access to the repository, forever.
The Standard Fix: Environment Variables
The common solution is to stop putting secrets directly in code at all. Instead, values live as
environment variables — set in the actual operating-system environment your project
runs in, completely outside your codebase and outside version control. settings.py then
just reads those values at startup, rather than hardcoding them.
Reading Environment Variables Cleanly with django-environ
Python's standard library can already read environment variables through os.environ, but
it hands everything back as plain strings, with no easy way to say "this one's a boolean" or "this one
has a default." The popular third-party package django-environ (and the similar
python-decouple) wraps that with a much nicer interface built for exactly this use case in
Django projects:
# settings.py
import environ
env = environ.Env()
environ.Env.read_env()
SECRET_KEY = env('SECRET_KEY')
env('SECRET_KEY') looks up that name in the environment and returns its value as a
string — but crucially, the value itself never appears anywhere in this file. Only the name
of the variable does.
.env for Local Development, .env.example for Everyone Else
Setting real environment variables by hand every time you open a terminal gets old fast during local
development, so django-environ (and most similar tools) can load them from a
.env file instead — a plain text file of KEY=value lines sitting at the
project root:
# .env — your own machine's real values, never committed
SECRET_KEY=django-insecure-a-real-random-key-goes-here
DEBUG=True
DATABASE_URL=postgres://taskflow_user:realpassword@localhost:5432/taskflow_dev
This file holds actual, working values for your own machine — which is exactly why it must never be
committed. Add it to .gitignore immediately, before it ever holds a real secret:
# .gitignore
.env
The problem that leaves: a teammate cloning the project has no idea which variables TaskFlow actually
needs. That's what .env.example is for — a template listing every required variable name
with a placeholder or obviously-fake value, committed to the repository on purpose:
# .env.example — committed, safe, no real secrets
SECRET_KEY=change-me-to-a-real-random-value
DEBUG=True
DATABASE_URL=postgres://user:password@localhost:5432/dbname
A new contributor copies .env.example to .env, fills in real values for their
own machine, and never touches version control with an actual secret.
A Full Example: Converting Hardcoded Settings
Putting all of this together — SECRET_KEY, DEBUG, and the database
connection, converted from hardcoded values to environment-driven ones:
# Before
SECRET_KEY = 'django-insecure-hardcoded-value-in-source-control'
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'taskflow_dev',
'USER': 'taskflow_user',
'PASSWORD': 'realpassword',
'HOST': 'localhost',
}
}
# After
import environ
env = environ.Env()
environ.Env.read_env()
SECRET_KEY = env('SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)
DATABASES = {
'default': env.db('DATABASE_URL')
}
env.bool() converts the environment's plain-string value into a real Python boolean, with
a sensible default if the variable is missing entirely. env.db() is even more convenient —
it parses a single DATABASE_URL connection string into the full dictionary Django's
DATABASES setting expects, so one environment variable replaces five separate hardcoded
settings.
Different Settings Per Environment
Development and production usually need more than just different secret values — sometimes genuinely different settings altogether (different installed apps for debugging tools, different static file storage). Two common, equally valid approaches:
| Approach | How it works |
|---|---|
| Separate settings files | A settings/base.py with everything shared, then settings/dev.py and settings/prod.py each importing from base and overriding just what differs. |
| One file, branching on an environment variable | A single settings.py that reads something like ENVIRONMENT=production from the environment and adjusts a handful of values with a plain if statement. |
Neither is objectively correct — separate files scale better once the differences get numerous and complex, while a single branching file stays simpler for a smaller project. Either is a reasonable starting point for TaskFlow.
Where Production Values Actually Get Set
None of this explains where the real production SECRET_KEY or database URL
actually gets configured — and the honest answer is that it depends entirely on where the project is
hosted. Most hosting platforms, whether a Heroku-style PaaS or a Docker-based deployment, provide their
own native way to set environment variables for a running app — a dashboard setting, a config command,
or an orchestration file. Whatever the mechanism, the pattern is the same one covered here: your code
reads a named variable, and the hosting environment is responsible for supplying its actual value.
.env file or deployed environment, so nothing here executes on this page.