Don't abuse Django's DEBUG setting

It's tempting to treat settings.DEBUG as a feature flag. Many devs see it as analogous to is the app not running in prod. However, using settings.DEBUG to change the flow of code adds complexities to testing and maintaining the codebase.

As a concrete example, let's say we want to activate Django admin only in local development to reduce the surface area for hackers to attack in prod. We could do this:

from django.conf import settings
from django.contrib import admin

urlpatterns = [...]

if settings.DEBUG:
    # not exposing admin to prod so it can't be hacked
    urlpatterns.append(path('admin/', admin.site.urls))

This will work, but the code will be harder to test in isolation because the value of settings.DEBUG changes other Django behaviours including error handling (should we see detailed crash report in browser? Should the error be emailed to admins?). Therefore instead of using settings.DEBUG in this way consider using a feature flag:

from django.conf import settings
from django.contrib import admin

urlpatterns = [...]

if settings.IS_ADMIN_ENABLED:
    # not exposing admin to prod so it can't be hacked
    urlpatterns.append(path('admin/', admin.site.urls))

The reason for this complexity is the fact settings.DEBUG is for switching whether Django should run in debug mode. Indeed it's for switching behaviour of the framework and framework libraries, not application code. Using settings.DEBUG for also controlling the behaviour of the application code built on top of the framework makes the codebase harder to maintain and test. Mixing the layers and responsibilities like this adds complexity.

For example, DEBUG=True shows detailed error pages for use during local development, while DEBUG=False can result in the error instead being emailed to settings.ADMINS. Do you want to cause either of those behavioural changes while testing your code? Probably not, as this is inconvenient and also goes against the principle of least surprise.

Aside from the complexities in testing, Twelve factor App suggests aiming for dev/prod parity. Using settings.DEBUG to control which block of code the application runs prevents dev/prod parity because most developers will not test or run their local environment with DEBUG=False. To solve this instead consider adding a feature flag specifically for this one check, which you can switch easily during unit tests and with none of the other side effects inherent with settings.DEBUG.

If we spot this issue in your GitHub pull request we give this advice:

Feature flag implimentation

# settings.py
from ast import iteral_eval
from os import getenv

IS_ADMIN_ENABLED = literal_eval(getenv('IS_ADMIN_ENABLED', 'False'))

Does your Django codebase have tech debt like this?

23