When starting a new Django project, you get a settings.py
file
with some initial basic settings in it.
This is rarely complex enough for real world projects.
Lets take a look at how to improve the settings management in Django.
There is no claim that the examples here will fit perfectly for your specific project. However, this provides a framework for dealing with Django settings, and it is based on several years of real world experience in large Django projects.
With this method, there is no need to ever set DJANGO_SETTINGS_MODULE
,
it stays exactly the same as when the project was created
with django-admin startproject
.
Yet, it allows for dynamic control over any Django setting
via other environment variables.
Convert settings into a package
An import statement in Python can point to
either a module or a package interchangeably.
This means that the settings module can be converted into a package,
without having to change anything else in the framework.
As long as there is an __init__.py
file in the package
that does whatever the module would have done,
the code that is doing the import (Django) will not care.
Lets go from this:
.
├── manage.py
└── rainbows
├── __init__.py
├── urls.py
├── wsgi.py
└── settings.py
To this:
.
├── manage.py
└── rainbows
├── __init__.py
├── urls.py
├── wsgi.py
└── settings
├── __init__.py
├── base.py
├── allowed_hosts.py
└── envs
├── __init__.py
├── dev.py
├── stage.py
└── prod.py
settings/__init__.py
Avoid putting any actual settings in this file, __init__.py
files are reserved for package level imports only.
Lets set up a system that does some dynamic imports,
based on an environment variable.
import os
import importlib
env = os.environ.get('DJANGO_SETTINGS_ENV')
module_paths = []
module_paths.append('rainbows.settings.base')
if env:
module_paths.append('rainbows.settings.envs.{env}'.format(env=env))
module_paths.append('rainbows.settings.allowed_hosts')
for module_path in module_paths:
module = importlib.import_module(module_path)
globals().update(module.__dict__)
A list called module_paths
is created,
and populated with multiple strings
that contain paths to python modules that are going to be imported.
One of the module path strings is constructed dynamically,
based on the value of the environment variable DJANGO_SETTINGS_ENV
stored in the env
variable.
The actual imports happen on the last two lines.
The calls to importlib.update_module()
and globals().update()
produces
a result that is nearly identical to a from somewhere import *
statement.
(Assuming that module_path
contains the string 'somewhere'
).
This makes all module level variables in the various imported modules
(such as INSTALLED_APPS
in base.py
) appear at
the module level of the __init__
module.
And any variable in the __init__
module will automatically
be available at the settings
package level,
which satisfies Django when it imports rainbows.settings
.
I say nearly identical, because when doing import *
with
the regular import statement, it actually ignores names that start
with an underscore. (See the section on public names).
But since Django settings files should never contain any variables
that start with an underscore, this will not be an issue.
settings/base.py
This file contains regular Django settings such as INSTALLED_APPS
,
that should apply regardless of the current value of DJANGO_SETTINGS_ENV
.
settings/envs/__init__.py
This file is empty.
settings/envs/dev.py
This file contains regular Django settings, such as DEBUG
,
that should apply only for the dev environment.
from ..base import * # noqa
DEBUG = True
SECRET_KEY = '...'
INSTALLED_APPS += [
'debug_toolbar'
]
The from ..base import *
statement is actually
not necessary in order for the code to run.
However, it helps with autocompletion when coding in an IDE.
settings/envs/stage.py
This file contains regular Django settings, that should apply only for the stage environment.
from ..base import * # noqa
DEBUG = False
SECRET_KEY = '...'
settings/envs/prod.py
This file contains regular Django settings, that should apply only for the prod environment.
from ..base import * # noqa
DEBUG = False
SECRET_KEY = '...'
settings/allowed_hosts.py
Allowed hosts deserves its own module, because it contains some dynamic code to determine the correct domains based on the current environment.
import os
env = os.environ.get('DJANGO_SETTINGS_ENV')
hosts = {
'dev': [
'*',
],
'stage': [
'staging.rainbows.tld',
],
'prod': [
'rainbows.tld',
],
}
if env:
ALLOWED_HOSTS = hosts[env]
Final words
To avoid confusion,
make sure that a setting only occurs at one level in the hierarchy,
and that it occurs in all modules at that level.
For example; the DEBUG
setting in the above code is being set
at the environment level.
Lets make sure that it does not also occur in base.py
And lets make sure that it does occur in
all three env modules: dev.py
, stage.py
, and prod.py
.
Use only environment variables as input for the settings package. It is a standard way of interfacing between an OS and a program. That means it will be portable across any platform you decide to deploy on.
With this method, it is simple to add more dimensions.
In this example there is only one input, DJANGO_SETTINGS_ENV
.
But you could add more environment variables and sub packages
for anything you come up with.
Perhaps you want to spin up multiple instances of the same project,
under different domains with different url patterns for each domain.
You could add an environment variable DJANGO_SETTINGS_SITE
,
and a settings/sites
package,
and have different values for ROOT_URLCONF
in the various site modules,
and adapt allowed_hosts.py
to accomodate for the different sites.
Or perhaps you want to deploy multiple instances
of the project in different zones,
and want to point each instance to a different API gateway.
You could add an environment variable DJANGO_SETTINGS_ZONE
,
and a settings/zones
package,
and have a API_GATEWAY
setting that varies for each zone module.