Every Django project has a settings.py
file. When you are just starting it seems harmless. You put your SECRET_KEY
there. You configure your database. You set DEBUG = True
. Then you commit it and move on.
This is a mistake. It is a small mistake at first but it grows into a big one. Your settings file becomes a messy drawer of secrets configuration and environment specific logic. You might have a block of code like this.
# settings.py
# DANGER: Do not do this
import socket
if socket.gethostname() == 'prod-server-01':
DEBUG = False
DATABASES = { ... } # production db
else:
DEBUG = True
DATABASES = { ... } # local sqlite db
This is fragile. What happens when you add a second production server? Or a staging environment? This path leads to complexity and bugs. Even worse is committing your secret keys and database passwords directly into your version control history. It is a security risk waiting to happen.
There is a simpler and more robust way.
The principle is simple. Your code should be the same everywhere. The only thing that should change between your laptop and your production server is the configuration. This configuration includes things like database URLs API keys and the debug setting.
This idea is not new. It is one of the pillars of the Twelve-Factor App methodology. The solution is to store configuration in environment variables.
Environment variables are simple key value pairs that live outside your application’s code. The operating system provides them to your running process. This cleanly separates your code from its configuration. Your settings.py
file will no longer contain any secrets. It will only contain the logic to read those secrets from the environment.
Let’s refactor a typical settings file to use this pattern. We will not use any external packages. Django and Python’s standard library are all we need.
First import the os
module at the top of your settings.py
.
# settings.py
import os
Now let’s change the SECRET_KEY
. Instead of a hardcoded string we will read it from an environment variable called SECRET_KEY
.
# settings.py
# Old way
# SECRET_KEY = 'django-insecure-....'
# New way
SECRET_KEY = os.environ.get('SECRET_KEY')
The os.environ.get()
method attempts to read the environment variable. If it is not found it will return None
. In production you want your application to crash if the secret key is missing. It is a critical piece of configuration. A missing key is a fatal error.
What about settings like DEBUG
? In development you want it to be True
. In production it must be False
.
You cannot just read it from the environment because environment variables are always strings. os.environ.get('DEBUG')
will return the string “True”
or “False”
not the boolean. You need to do a comparison.
Here is a safe way to handle the DEBUG
setting.
# settings.py
# New way for DEBUG
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
Here we use the second argument of .get()
to provide a default value. If the DEBUG
environment variable is not set it defaults to the string 'False'
. The comparison 'False' == 'True'
results in the boolean False
. This is a safe default for production. Your app will be secure by default.
Your database configuration likely contains a password which is a secret. We should move the entire database connection string into the environment. Many services expect a single database URL. It is a good pattern to adopt.
A library like dj-database-url
can make this easy but we can also do it without any dependencies if the URL format is predictable. For simplicity let’s just read the components from the environment.
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
}
}
Again we provide sensible defaults for things like HOST
and PORT
which are often the same in development. Critical values like DB_NAME
and DB_PASSWORD
have no defaults. If they are missing we want to know about it.
Now your settings.py
is clean. It contains no secrets. But how do you run your app locally? You do not want to manually set a dozen environment variables every time you open a terminal.
The common solution is to use a .env
file. This is a simple text file in your project’s root directory that holds your local configuration.
Create a file named .env
in the same directory as your manage.py
.
# .env file
SECRET_KEY=your-local-secret-key-here
DEBUG=True
DB_NAME=myapp_dev
DB_USER=myuser
DB_PASSWORD=mypassword
Crucially you must add .env
to your .gitignore
file. This file should never be committed to your repository.
# .gitignore
.env
To load these variables into your environment you need a little bit of code. A popular way is to use a library like python-dotenv
. But if you want to avoid dependencies you can use a simple script or a tool like direnv
. A simpler way for Django is to just read the file if DEBUG
is on.
Let’s modify our manage.py
and wsgi.py
files to load this file for us. Or even better put a little snippet at the top of settings.py
that only runs in development.
A simple way to do this is to add a small loader at the top of your settings.py
itself.
# settings.py
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# A simple .env file loader for local development
# Make sure .env is in your .gitignore
try:
with open(os.path.join(BASE_DIR, '.env')) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
except FileNotFoundError:
pass # No .env file found, which is fine for production
# Now the rest of your settings file can use os.environ
SECRET_KEY = os.environ.get('SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
# ... and so on
This snippet is a bit of magic but it is small and understandable. It looks for a .env
file in the project root. If it finds one it reads it and sets the environment variables but only if they are not already set. This is important. In production your server’s real environment variables will take precedence.
Your settings.py
is now just a template. It describes what configuration your app needs. The actual values live in the environment.
This is a huge improvement.
It is more secure. You are not committing secrets to Git. It is more flexible. You can deploy the same code to staging and production with different configurations. It is simpler. There are no more
if
statements checking hostnames.
Your new workflow is simple. For local development you create a .env
file. For production you set the environment variables using your host’s control panel or your deployment scripts. The code does not change.
This is one of those small changes that has a large positive impact on the life of a project. It makes your application cleaner safer and easier to manage.
Take a look at your own project’s configuration. Now I encourage you to think about what you could improve.
— Rishi Banerjee
September 2025