A Simple Pattern for Django Settings

The Problem with settings.py

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.

Configuration Should Live in the Environment

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.

A Practical Pattern

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.

Handling Booleans and Defaults

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.

Configuring the Database

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.

Local Development with a .env File

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.

The Final Result

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