Django Settings Patterns to Avoid

Settings are the engine of your project.

This post is an adapted extract from my book Boost Your Django DX, available now.

Here are some potential mistakes made with Django settings that you can avoid.

Don’t Read Settings at Import Time

Python doesn’t make a distinction between import time and run time. As such, it’s possible to read settings at import time, but this can lead to subtle bugs. Reading a setting at import time will use or copy its initial value, and will not account for any later changes. Settings don’t often change, but when they do, you definitely want to use the new value.

For example, take this views file:

from django.conf import settings

...

page_size = settings.PAGE_SIZE


def book_list(request):
    paginator = Paginator(Book.objects.all(), page_size)
    ...

page_size is set to the value of the setting once, at import time. It won’t account for any changes made in tests, such as with override_settings:

from django.test import TestCase, override_settings


class BookListTests(TestCase):
    def test_pagination(self):
        with override_settings(PAGE_SIZE=2):
            response = self.client.get(...)

The previous view code would not use the changed PAGE_SIZE setting, since it won’t affect the module-level page_size variable. You can fix this problem by changing the view to read the setting when it runs:

from django.conf import settings

...


def book_list(request):
    paginator = Paginator(Book.objects.all(), settings.PAGE_SIZE)
    ...

Straightforward when you know how.

This problem can also manifest itself with classes, as class-level attributes are also set once, at import time. For example, in this class-based view:

from django.conf import settings


class BookListView(ListView):
    ...
    paginate_by = settings.PAGE_SIZE

In this case, you can avoid the problem by reading the setting in the relevant view method:

from django.conf import settings


class BookListView(ListView):
    def get_paginate_by(self, queryset):
        return settings.PAGE_SIZE

Two thumbs up!

Avoid Direct Setting Changes

Thanks to Python’s flexibility, it’s possible to change a setting at runtime by directly setting it. But you should avoid doing so, since this does not trigger the django.test.signals.setting_changed signal, and any signal receivers will not be notified. Django’s documentation warns against this pattern.

For example, tests should not directly change settings:

from django.conf import settings
from django.test import TestCase


class SomeTests(TestCase):
    def test_view(self):
        settings.PAGE_SIZE = 2
        ...

Instead, you should always use override_settings, and related tools:

from django.conf import settings
from django.test import TestCase
from django.test import override_settings


class SomeTests(TestCase):
    @override_settings(PAGE_SIZE=2)
    def test_view(self):
        ...

Coolio.

(Note, making direct changes to pytest-django’s settings fixture is okay. It’s a special object that uses override_settings under the hood.)

Don’t Import Your Project Settings Module

It’s possible to directly import your settings module and read from it directly, bypassing Django’s settings machinery:

from example import settings


def index(request):
    if settings.DEBUG:
        ...

You should avoid doing so though. If settings are loaded from a different place, or overridden with override_settings etc., they won’t be reflected in the original module.

You should always import and use Django’s settings object:

from django.conf import settings


def index(request):
    if settings.DEBUG:
        ...

Alrighty.

It can occasionally be legitimate to import your settings modules directly, such as when testing them (covered later in the book). If you don’t have such usage, you can guard against this pattern with the flake8-tidy-imports package’s banned-modules option. This allows you to specify a list of module names for which it will report errors wherever they are imported.

Avoid Creating Custom Settings Where Module Constants Would Do

When you define a custom setting, ensure that it is something that can change. Otherwise, it’s not really a setting but a constant, and it would be better to define it in the module that uses it. This can keep your settings file smaller and cleaner.

For example, take this “setting”:

EXAMPLE_API_BASE_URL = "https://api.example.com"

(When using a single settings file.)

It only defines a code uses a constant base URL, so it’s not configurable. Therefore there’s no need for this variable to be a setting. It can instead live in the Example API module, keeping the settings file trim.

Avoid Creating Dynamic Module Constants Instead of Settings

This is kind of the opposite to the above. In order to avoid cluttering the settings file, you might be tempted to define some constants that read from environment variables in other modules. For example, at the top of an API client file:

EXAMPLE_API_BASE_URL = os.environ.get(
    "EXAMPLE_API_BASE_URL",
    "https://api.example.com",
)

As a separated module-level constant, it loses several of the advantages of being a setting:

  1. Since it’s not centralized in the settings file, it’s not discoverable. This makes it hard to determine the full set of environment variables that your project reads.
  2. When the environment variable is read is a bit less determined. The settings file is imported by Django at a predictable time in its startup process. Other modules can be imported earlier or later, and this can change as your project evolves. This non-determinism makes it harder to ensure that environment variable sources like a .env file have been read.
  3. override_settings cannot replace the constant. You can use unittest.mock instead, but this gets tricky if the constant is imported elsewhere.

Such a constant deserves to be a setting instead. Make it so!

Name Your Custom Settings Well

As settings live in a single flat namespace, settings names are important, even more than regular variable names. When you create a custom setting, use a highly specific name to help guide future readers. Avoid abbreviations, and use a prefix where applicable. When reading from an environment variable, match the environment variable name to the setting name.

For example, here’s a poorly named custom setting:

EA_TO = float(os.environ.get("EA_TIMEOUT", 5.0))

It begs some questions:

Here’s a better definition of that setting:

EXAMPLE_API_TIMEOUT_SECONDS = float(os.environ.get("EXAMPLE_API_TIMEOUT_SECONDS", 10))

A fine, informative name.

Override Complex Settings Correctly

Some settings take complex values, such as nested dictionary structures. When overriding them in tests, you need to be careful to only affect the part of the setting that you care about. If you don’t, you may erase vital values from the setting.

For example, imagine a package configured with a dictionary setting:

EXAMPLE = {
    "VERSIONING": "example.SimpleVersioning",
    "PAGE_SIZE": 20,
    # ...
}

In a test, you might want to reduce the value of PAGE_SIZE to improve test speed. You might try this:

from django.test import TestCase, override_settings


class SomeTests(TestCase):
    def test_something(self):
        with override_settings(EXAMPLE={"PAGE_SIZE": 2}):
            # Do test
            ...

…unfortunately, this would erase the value of the VERSIONING key, and any others. This might affect the test behaviour now, or in the future when the setting is changed.

Instead, you should create a copy of the existing setting value with the appropriate modification. One way to do this, on Python 3.9+, is with the dictionary merge operator:

from django.conf import settings
from django.test import TestCase, override_settings


class SomeTests(TestCase):
    def test_something(self):
        with override_settings(EXAMPLE=settings.EXAMPLE | {"PAGE_SIZE": 2}):
            # Do test
            ...

On older Python versions, you can use dict(base, key=value, ...) to copy a dictionary and replace the given keys:

from django.conf import settings
from django.test import TestCase, override_settings


class SomeTests(TestCase):
    def test_something(self):
        with override_settings(EXAMPLE=dict(settings.EXAMPLE, PAGE_SIZE=2)):
            # Do test
            ...

w00t.

Fin

May your settings be tidy and clear,

—Adam


Learn how to make your tests run quickly in my book Speed Up Your Django Tests.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: