Django Settings Patterns to Avoid

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:
- 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.
- 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. override_settings
cannot replace the constant. You can useunittest.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:
- What does “EA” stand for?
- What units is the timeout in?
- What does
TO
stand for?
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.
Learn how to make your tests run quickly in my book Speed Up Your Django Tests.
One summary email a week, no spam, I pinky promise.
Related posts:
- Common Issues Using Celery (And Other Task Queues)
- How to implement a “dry run mode” for data imports in Django
- django-upgrade Mega Release 1.11.0
Tags: django