Managing environment-specific Python settings

How do you deal with settings changes that manipulate Python for different environments? That’s a question that came up as I was doing my Django Twitch stream (shameless plug :smile:) tonight.

In my current project configuration, I have multiple settings files for different environments like settings/development.py, settings/production.py, and so on which inherit settings from a settings/base.py file. I’m trying to switch to a scheme that only uses a single settings file and reads stuff externally. I happened to select goodconf, but you can totally do something similar with django-environ.

My trouble with this switch is that I don’t know what to do when one of my other settings files manipulated Python. For instance, here is settings/development.py:

from settings.base import *  # noqa

INTERNAL_IPS = ("127.0.0.1",)
INSTALLED_APPS += ("debug_toolbar",)  # noqa
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa

The only substantial change that this settings file is doing is configuring django-debug-toolbar. To do that, I’m monkeying with INSTALLED_APPS and MIDDLEWARE at the Python level (i.e., manipulating lists).

If you use a tool like django-environ, how do you like to deal with these environment-specific customizations? It looks like django-configurations might support this with setup methods. I’m interested to see other techniques that people use.

I only use two settings files in my projects.: settings.py and test_settings.py. My main settings.py file uses django-environ and has good, local defaults which are overrideable with environment variables.

While I don’t use debug_toolbar, I would do something like:

DEBUG = env.bool("DJANGO_DEBUG", default=False)

if DEBUG:
    INTERNAL_IPS = ("127.0.0.1",)
    INSTALLED_APPS.append("debug_toolbar")  # noqa
    MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa

My test_settings.py is disabled migrations, using a faster password hasher, disables logging, and basically a bunch of other tricks to make my testsuite run as fast as possible.

Your production settings shouldn’t be in git so set them via environment variables and/or SECRETS with your hosting provider.

2 Likes

I strongly agree with Jeff’s approach. Using environment variables like this is much easier, to me, to reason about.

1 Like

Why not have only one settings file like this:

if DEBUG:
    INTERNAL_IPS = ("127.0.0.1",)
    INSTALLED_APPS += ("debug_toolbar",)  # noqa
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")

Or even:

try:
    import debug_toolbar
except ImportError:
    pass
else:
    INTERNAL_IPS = ("127.0.0.1",)
    INSTALLED_APPS += ("debug_toolbar",)  # noqa
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")

Then you don’t need development.py

Yep, I’ve opted to use two files for my next project like what @jeff suggested.

It’s a personal preference. I have worked on projects with dozens of settings per project and 1000+ line settings files that try to account for particular environments, and two feels like the right mix for me.

Splitting out your testing config makes CI/testing a bit easier because you can point pytest at the test_settings.py and avoid having to configure extra environment variables. I try to avoid having to set any outside of DATABASE_URL.

I think it’s more readable too. Here’s an example of the pattern that I use in my personal and client projects: https://github.com/jefftriplett/django-jobs/blob/master/config/test_settings.py

I thought that would be a good reason to program as much configuration unfolding as possible. Nonetheless, anything works as long as you practice CI/CD, by whatever means necessary :+1:

I think it’s more readable too

That’s debatable of course but my example code has a very low complexity and few caracters so for many people I think it’s still readable.

Honestly, I don’t really read configuration files much anymore, I use the djcli setting command to print out setting values after interpretation. Basically it’s the same as echo 'from django.conf import settings; print(settings.SOME_SETTING)' | manage.py shell which I used prior to having djcli.

Anyway, splitting it in modules is fine - it worked for me prior to adhering to the 12factor practice, but 1000 lines would look ok for me given the low complexity of a programed configuration file. I recon there’s definitely a learning curve for developers who aren’t familiar with environment variables, so, that would be the only inconvenient to report in my exerience.