Stop Using Multiple Django Settings Files

Stop Using Multiple Django Settings Files

Multiple settings files is a pattern preached by many Django experts and I’ve come across a few projects that use this abomination of a pattern lately.

In this post, I’ll tell you why multiple settings files in Django are dumb and what to do instead.

Python is a full fledged programming language

Python:

  1. Is not a flat file like JSON or YAML and must never be used as such.
  2. Has if statements, for loops, and dicts, which give us tremendous power.

Because of its interpreted nature, people are tempted to use .py files as a flat data store. Projects then end up with crazy settings architecture like base.py, dev.py, and production.py.

There are even projects that keep a different file for each deployment site. For example, site-[id].py where id identifies the site. That’s right, they keep 1000 files if 1000 different sites are available!

I would like to recommend a thorough read through the 12factor architectural pattern for these crazy situations.

It’s confusing and causes repetition

When using multiple settings for different environments you have to be extra careful where you’re importing modules and defining constants.

Define a constant that should only be in development and you could open up your app for security leaks or other serious bugs.

When you have dev.py and production.py, a lot of constants will have to be defined in both files. This causes repetition, and mistakes are more easily made. Every time you have to update a constant, you have to do it in 2 places. Sometimes you forget and only change it in one of the files. Now you have a bug.

It’s harder for your IDE to parse

PyCharm is my IDE of choice and it acts like a partner during development. If you use multiple settings files, the IDE will have trouble parsing your project settings properly and you will lose some intelligent and static analysis features.

Environment variables to the rescue

The industry standard for configuring environments in UNIX is through environment variables. The word environment should immediately give you a clue about what it’s a great use case for.

You can use this technique using the getenv function from the os module. But for Django apps, there’s a better alternative called django-environ .

In addition to loading constants from a file, this package will convert variables into their corresponding Python compatible data types.

To get started, create a .env file in your project root. Dump all constants you need in this file. Example:

DJANGO_DEBUG=0

    DJANGO_TEST=0

    DJANGO_SECRET_KEY=secret

    DJANGO_DB_URL=psql://superman:p@ssw0rd999@127.0.0.1/db_name
    DJANGO_DB_CONN_MAX_AGE=60

In your settings file, instantiate an env object:

import environ

    env = environ.Env()
    env.read_env(os.path.join(BASE_DIR, ".env"))

    DEBUG = env.bool("DJANGO_DEBUG")

env.bool will automatically convert the DJANGO_DEBUG constant into a boolean. You can then have things like DJANGO_DEBUG=0 in your .env file and it will work just fine.

What if you need some settings only in development but not in production? Here’s a simple example:

if DEBUG:
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]

        def show_toolbar(request):
            return True

        DEBUG_TOOLBAR_CONFIG = {
            "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"],
            "SHOW_TEMPLATE_CONTEXT": True,
            "SHOW_TOOLBAR_CALLBACK": show_toolbar,
        }

        INTERNAL_IPS = [
            "127.0.0.1",
        ]

What if you need different settings for development, test, and production environments? So easy, a caveman could do it:

if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = env.str("DJANGO_SMTP_HOST", default="localhost")
EMAIL_PORT = env.int("DJANGO_SMTP_PORT", default=1025)
elif TEST:
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
else:
EMAIL_BACKEND = "anymail.backends.amazon_ses.EmailBackend"
ANYMAIL = {
"AMAZON_SES_CLIENT_PARAMS": {
"aws_access_key_id": env.str("DJANGO_AWS_ACCESS_KEY_ID"),
"aws_secret_access_key": env.str("DJANGO_AWS_SECRET_ACCESS_KEY"),
"region_name": "us-east-1"
}
}

As you can see, by simply using conditional statements, we can remove the need for multiple settings files.

Having all your variables in a single place makes it much less likely to make mistakes.

By using environment variables, your app can be easily deployed to different platforms like Heroku or Kubernetes because they support environment variables by default.

Break down into modules for lengthy settings files

If your settings is getting way too long, you can simply create modules containing small functions and import them. Let’s say I wanted to break down my security settings into a separate module:

First create security.py and put the following inside:

def get_security(debug, test, env):
if not debug and not test:
return {
'SESSION_COOKIE_HTTPONLY': True,
'CSRF_COOKIE_HTTPONLY': False,
'SECURE_BROWSER_XSS_FILTER': True,
'X_FRAME_OPTIONS': "DENY",
'SECURE_PROXY_SSL_HEADER': ("HTTP_X_FORWARDED_PROTO", "https"),
'SESSION_COOKIE_SECURE': True,
'CSRF_COOKIE_SECURE': True,
'SECURE_HSTS_SECONDS': 518400,
'SECURE_HSTS_INCLUDE_SUBDOMAINS': env.bool(
"DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True
),
'SECURE_HSTS_PRELOAD': env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True),
'SECURE_CONTENT_TYPE_NOSNIFF': env.bool(
"DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True
),
}
return {}

Notice how the parameters consist of every variable the function will need from my settings file making it a simple and isolated function.

Now, in your settings.py:

    from . import security

    for k, v in security.get_security(DEBUG, TEST, env).items():
        exec(f"{k}={v}")

You now have all the constants you need and you broke down your settings.py file in a way that won’t confuse future developers or your IDE.

How is this different than multiple settings files then?

Here, you’re still defining constants in a single place (settings.py). Then, in your main settings.py, you’re simply importing the module and making the constants available in scope. With multiple settings files, you’ll be defining security settings in multiple places.

For example, in development, you will disable SESSION_COOKIE_SECURE and enable it in production. You will be defining this variable in both dev.py and production.py. With this method, you won’t have to touch the variable ever again because the system will autoconfigure itself based on what DEBUG is set to.

Python is not a configuration language

As you can see, Python is a programming language and because Django uses it to configure itself, you need to take full advantage of what it has to offer.

Many other frameworks use json or yaml files for configuration and Django developers tend to these copy patterns over even though Python isn’t json. It’s like driving your car with the handbrakes on. You can do it but it’s not efficient.