Two Scoops of Django 3x - Compress 2
Two Scoops of Django 3x - Compress 2
This issue compounds itself with the difficulty and complexity involved in migrat-
ing data out of SQLite3 and into something designed for concurrency (e.g., Post-
greSQL) when problems eventually arise.
While we’re aware that there are plenty of articles advocating the use of SQLite3 in
production, the fact that a tiny group of SQLite3 power users can get away with it
for particular edge cases is not justification for using it in production Django.
Pip is a tool that fetches Python packages from the Python Package Index and its mirrors.
It is used to manage and install Python packages and it comes with most non-Linux Python
installations.
Virtualenv is a tool for creating isolated Python environments for maintaining package
dependencies. It’s great for situations where you’re working on more than one project at a
time, and where there are clashes between the version numbers of different libraries that
your projects use.
For example, imagine that you’re working on one project that requires Django 3.0 and an-
other that requires Django 3.2.
ä Without virtualenv (or an alternative tool to manage dependencies), you have to re-
install Django every time you switch projects.
ä If that sounds tedious, keep in mind that most real Django projects have at least a
dozen dependencies to maintain.
Pip is already included in Python 3.4 and higher. Further reading and installation instruc-
tions can be found at:
ä pip: pip.pypa.io
ä virtualenv: virtualenv.pypa.io
2.2.1 virtualenvwrapper
Personally, we think virtualenv without virtualenvwrapper can be a pain to use, because every
time you want to activate a virtual environment, you have to type something long like:
$ source ~/.virtualenvs/twoscoops/bin/activate
$ workon twoscoops
Virtualenvwrapper is a popular companion tool to pip and virtualenv and makes our lives
easier, but it’s not an absolute necessity.
Figure 2.1: Pip, virtualenv, and virtualenvwrapper in ice cream bar form.
To summarize how this works: a requirements file is like a grocery list of Python packages
that you want to install. It contains the name and optionally suitable version range of each
package. You use pip to install packages from this list into your virtual environment.
We cover the setup of and installation from requirements files in Chapter 5: Settings and
Requirements Files.
You can also set your virtualenv’s PYTHONPATH to include the current directory with
the latest version of pip. Running “pip install -e .” from your project’s root
directory will do the trick, installing the current directory as a package that can be
edited in place.
If you don’t know how to set this or it seems complicated, don’t worry about it and
stick with manage.py.
Additional reading:
ä hope.simons-rock.edu/~pshields/cs/python/pythonpath.html
ä docs.djangoproject.com/en/3.2/ref/django-admin/
Git is the industry standard for developers of all languages and tools. Git makes it easy to
create branches and merge changes. When using a version control system, it’s important
to not only have a local copy of your code repository but also to use a code hosting service
for backups. Fortunately, there are a number of services and tools who host repositories. Of
them, we recommend using GitHub (github.com) or GitLab (gitlab.com).
That said, if the production infrastructure consists of 10,000 servers, it’s completely unrealis-
tic to have another 10,000 local servers for development purposes. So when we say identical,
we mean “as identical as realistically possible.”
The most common way to set up identical development environments is with Docker.
2.5.1 Docker
At the time of this writing, Docker is the industry standard for containerization of environ-
ments. It has excellent support across all operating systems, including Microsoft Windows.
Working with Docker is sort of like developing inside of a VM, except more lightweight.
Docker containers share the host OS but have their own isolated process and memory space.
Furthermore, since Docker uses a union-capablefilesystem, containers can be built
quickly off of a snapshot plus deltas rather than building from scratch.
For the purposes of local development, its main benefit is that it makes setting up environ-
ments that closely match development and production much easier.
For example, if our development laptops run Mac (or Windows, or Centos, etc) but a
project’s configuration is Ubuntu-specific, we can use Docker via Docker Compose to
quickly get a virtual Ubuntu development environment set up locally, complete with all
the packages and setup configurations needed for the project. We can:
ä Set up identical local development environments for everyone on our project’s dev
team.
ä Configure these local development environments in a way similar to our staging, test,
and production servers.
ä Extra complexity that is not needed in many situations. For simpler projects where
we’re not too worried about OS-level differences, it’s easier to skip this.
ä On older development machines, running even lightweight containers can slow per-
formance to a crawl. Even on newer machines, small but noticeable overhead is added.
ä cookiecutter-django.readthedocs.io/en/latest/
developing-locally-docker.html
ä http://bit.ly/1dWnzVW Real Python article on Django and Docker Compose
ä dockerbook.com
2.6 Summary
This chapter covered using the same database in development as in production, pip, vir-
tualenv, venv, conda, poetry, pipenv, version control, and Docker. These are good to have
in your tool chest, since they are commonly used not just in Django, but in the majority of
Python software development.
Project layout is one of those areas where core Django developers have differing opinions
about what they consider best practice. In this chapter, we present our approach, which is
one of the most commonly used ones.
mysite/
├── manage.py
├── my_app
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── mysite
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
There are a number of problems with Django’s default project layout. While useful for the
tutorial, it’s not quite as useful once you are trying to put together a real project. The rest of
this chapter will explain why.
<repository_root>/
├── <configuration_root>/
├── <django_project_root>/
like the README.md, docs/ directory, manage.py, .gitignore, requirements.txt files, and
other high-level files that are required for deployment and running the project.
If using django-admin startproject, you would run the command from within the
repository root. The Django project that it generates would then be the project root.
The files in the configuration root are part of what is generated by the
django-admin startproject command.
icecreamratings_project
├── config/
│ ├── settings/
│ ├── __init__.py
│ ├── asgi.py
│ ├── urls.py
│ └── wsgi.py
├── docs/
├── icecreamratings/
│ ├── media/ # Development only!
│ ├── products/
│ ├── profiles/
│ ├── ratings/
│ ├── static/
│ └── templates/
├── .gitignore
├── Makefile
├── README.md
├── manage.py
└── requirements.txt
Let’s do an in-depth review of this layout. As you can see, in the icecreamratings_project/
directory, which is the <repository_root> , we have the following files and directories. We
describe them in the table below:
When anyone visits this project, they are provided with a high-level view of the project.
We’ve found that this allows us to work easily with other developers and even non-
developers. For example, it’s not uncommon for designer-focused directories to be created
in the root directory.
If you find this confusing, there’s no harm in calling it assets/ or site_assets/ instead.
Just remember to update your STATICFILES_DIRS setting appropriately.
A good place to create the virtualenv for this project would be a separate directory where
you keep all of your virtualenvs for all of your Python projects. We like to put all our envi-
ronments in one directory and all our projects in another.
Figure 3.3: An isolated environment, allowing your ice cream to swim freely.
~/projects/icecreamratings_project/
~/.envs/icecreamratings/
On Windows:
c:\projects\icecreamratings_project\
c:\envs\icecreamratings\
~/.virtualenvs/icecreamratings/
Also, remember, there’s no need to keep the contents of your virtualenv in version control
since it already has all the dependencies captured in requirements.txt, and since you won’t be
editing any of the source code files in your virtualenv directly. Just remember that require-
ments.txt does need to remain in version control!
$ pip freeze
With Mac or Linux, you can pipe this into a requirements.txt file:
There are now thousands of Cookiecutter templates for Python, Django, FastAPI,
C, C++, JavaScript, Ruby, LaTeX/XeTeX, Berkshelf-Vagrant, HTML, Scala, 6502
Assembly, and more. Numerous companies use Cookiecutter internally to power a
variety of tools.
Cookiecutter isn’t just a command-line tool, it’s a library used by a host of orga-
nizations. You can also find it integrated into IDEs such as PyCharm and Visual
Studio.
1 First, it asks you to enter a series of values (e.g. the value for project_name).
2 Then it generates all your boilerplate project files based on the values you entered.
First, install Cookiecutter as per the instructions in the official Cookiecutter documentation.
cookiecutter https://github.com/pydanny/cookiecutter-django
You've downloaded /home/quique/.cookiecutters/cookiecutter-django
,→ before. Is it okay to delete and re-download it? [yes]: no
Do you want to re-use the existing version? [yes]: yes
project_name [My Awesome Project]: icecreamratings
project_slug [icecreamratings]:
description [Behold My Awesome Project!]: Support your Ice Cream
,→ Flavour!
author_name [Daniel Feldroy]:
After filling out all the values, in the directory where you ran Cookiecutter, it will create
a directory for your project. In this case with the values entered above, the name of this
directory will be icecreamratings.
The resulting project files will be roughly similar to the layout example we provided. The
project will include settings, requirements, starter documentation, a starter test suite, and
more.
It’s a lot fancier than the default startproject template provided by Django.
We’d rather have you see our actual, real-life template that we use for our projects
than a stripped-down, beginner-oriented template that we don’t use.
You are welcome to fork Cookiecutter Django and customize it to fit your own Django
project needs.
It’s okay if a project differs from our layout, just so long as things are either done in a hier-
archical fashion or the locations of elements of the project (docs, templates, apps, settings,
etc) are documented in the root README.md.
We encourage you to explore the forks of Cookiecutter Django, and to search for other
Cookiecutter-powered Django project templates online. You’ll learn all kinds of interesting
tricks by studying other people’s project templates.
TIP: cookiecutter-django-vue-graphql-aws
Another good open source Cookiecutter-powered template is Grant Mc-
Connaughey’s cookiecutter-django-vue-graphql-aws. It knits together
technology we enjoy (Django, GraphQL, Vue, AWS Lambda + Zappa,
Figure 3.4: Project layout differences of opinion can cause ice cream fights.
3.7 Summary
In this chapter, we covered our approach to basic Django project layout. We provided a
detailed example to give you as much insight as possible into our practice. Cookiecutter and
two templates are introduced.
Project layout is one of those areas of Django where practices differ widely from developer
to developer and group to group. What works for a small team may not work for a large
team with distributed resources. Whatever layout is chosen it should be documented clearly.
It’s not uncommon for new Django developers to become understandably confused by
Django’s usage of the word “app”. So before we get into Django app design, it’s very impor-
tant that we go over some definitions.
Figure 4.1: It’ll make more sense when you see the next figure.
“The art of creating and maintaining a good Django app is that it should
follow the truncated Unix philosophy according to Douglas McIlroy: ‘Write
programs that do one thing and do it well.”’
In essence, each app should be tightly focused on its task. If an app can’t be explained in
a single sentence of moderate length, or you need to say ‘and’ more than once, it probably
means the app is too big and should be broken up.
We’d call the Django project for our shop’s website twoscoops_project. The apps within our
Django project might be something like:
ä A flavors app to track all of our ice cream flavors and list them on our website.
ä A blog app for the official Two Scoops blog.
ä An events app to display listings of our shop’s events on our website: events such as
Strawberry Sundae Sundays and Fudgy First Fridays.
Each one of these apps does one particular thing. Yes, the apps relate to each other, and you
could imagine events or blog posts that are centered around certain ice cream flavors, but it’s
much better to have three specialized apps than one app that does everything.
Notice how events are kept separate from ticket sales. Rather than expanding the events app
to sell tickets, we create a separate tickets app because most events don’t require tickets, and
because event calendars and ticket sales have the potential to contain complex logic as the
site grows.
Eventually, we hope to use the tickets app to sell tickets to Icecreamlandia, the ice cream
theme park filled with thrill rides that we’ve always wanted to open.
Did we say that this was a fictional example? Ahem...well, here’s an early concept map of
what we envision for Icecreamlandia:
When possible keep to single word names like flavors, animals, blog, polls, dreams, estimates,
and finances. A good, easy-to-remember app name makes the project easier to maintain.
As a general rule, the app’s name should be a plural version of the app’s main model, but
there are many good exceptions to this rule, blog being one of the most common ones.
Don’t just consider the app’s main model, though. You should also consider how you want
your URLs to appear when choosing a name. If you want your site’s blog to appear at
http://www.example.com/weblog/, then consider naming your app weblog rather than
blog, posts, or blogposts, even if the main model is Post, to make it easier for you to see
which app corresponds with which part of the site.
Use valid, PEP 8-compliant, importable Python package names: short, all-lowercase names
without numbers, dashes, periods, spaces, or special characters. If needed for readability, you
can use underscores to separate words, although the use of underscores is discouraged.
Try and keep your apps small. Remember, it’s better to have many small apps than to have
a few giant apps.
Figure 4.4: Two small, single-flavor pints are better than a giant, 100-flavor container.
# Common modules
scoops/
├── __init__.py
├── admin.py
├── forms.py
├── management/
├── migrations/
├── models.py
├── templatetags/
├── tests/
├── urls.py
├── views.py
Over time a convention of module names has emerged for building Django apps. By fol-
lowing this convention across the building of apps we set behaviors for ourselves and others,
making examining each other’s code easier. While Python and Django are flexible enough
that most of these don’t need to be named according to this convention, not doing so will
cause problems. Probably not from an immediate technical perspective, but when you or
others look at nonstandard module names later, it will prove to be a frustrating experience.
# uncommon modules
scoops/
├── api/
├── behaviors.py
├── constants.py
├── context_processors.py
├── decorators.py
├── db/
├── exceptions.py
├── fields.py
├── factories.py
├── helpers.py
├── managers.py
├── middleware.py
├── schema.py
├── signals.py
├── utils.py
├── viewmixins.py
What is the purpose of each module? Most of these have clear English names, but we’ll go
over a few that might not be so clear.
api/ This is the package we create for isolating the various modules needed when creating
an api. See Section 17.3.1: Use Consistent API Module Naming.
behaviors.py An option for locating model mixins per Section 6.7.1: Model Behaviors
a.k.a Mixins.
constants.py A good name for placement of app-level settings. If there are enough of them
involved in an app, breaking them out into their own module can add clarity to a
project.
decorators.py Where we like to locate our decorators. For more information on decorators,
see Section 9.3: Decorators Are Sweet.
db/ A package used in many projects for any custom model fields or components.
fields.py is commonly used for form fields but is sometimes used for model fields when
there isn’t enough field code to justify creating a db/ package.
factories.py Where we like to place our test data factories. Described in brief in Sec-
tion 24.3.5: Don’t Rely on Fixtures
helpers.py What we call helper functions. These are where we put code extracted from
views (Section 8.5: Try to Keep Business Logic Out of Views) and models (Sec-
tion 6.7: Understanding Fat Models) to make them lighter. Synonymous with utils.py
managers.py When models.py grows too large, a common remedy is to move any custom
model managers to this module.
schema.py This is where code behind GraphQL APIs is typically placed.
signals.py While we argue against providing custom signals (see ??: ??), this can be a useful
place to put them.
utils.py Synonymous with helpers.py
viewmixins.py View modules and packages can be thinned by moving any view mixins to
this module. See Section 10.2: Using Mixins With CBVs.
For all of the modules listed in this section, their focus should be at the ‘app-level’, not
global tools. Global-level modules are described in Section 31.1: Create a Core App for
Your Utilities.
park:
The typical placement for these actions is spread across methods on the managers assigned
to the User and Ticket. Others prefer to use class methods instead. So we might see:
class UserManager(BaseUserManager):
"""In users/managers.py"""
def create_user(self, email=None, password=None,
,→ avatar_url=None):
user = self.model(
email=email,
is_active=True,
last_login=timezone.now(),
registered_at=timezone.now(),
avatar_url=avatar_url
)
resize_avatar(avatar_url)
Ticket.objects.create_ticket(user)
return user
class TicketManager(models.manager):
"""In tasks/managers.py"""
def create_ticket(self, user: User):
ticket = self.model(user=user)
send_ticket_to_guest_checkin(ticket)
return ticket
While this works, the service layer approach argues that there should be separation of con-
cerns between the user object and tickets. Specifically, embedding logic in the User code
to call the Ticket code tightly couples the two domains. Thus, a new layer is added to keep
concerns separate, and this is called the service layer. Code for this architecture is placed in
service.py and selectors.py.
Under the service layer design, the above code would be executed thus:
# In users/services.py
from .models import User
from tickets.models import Ticket, send_ticket_to_guest_checkin
ticket = Ticket(user=user)
send_ticket_to_guest_checkin(ticket)
return user
ä Adding type annotations of returned objects is easier with this approach than typical
Django
ä Very rare for projects to replace entire modules without entire rewrites
ä For small projects often adds unnecessary complexity
ä For complex projects the service layer typically grows to thousands of lines of code,
forcing the development of new architecture to support it
ä Use of selectors.py everywhere forces an extra step any time a simple ORM query
could be made
ä Django persistence is based on importable business logic placed on the model itself.
Service layers remove that capability, removing some advanced techniques
ä b-list.org/weblog/2020/mar/16/no-service/
ä b-list.org/weblog/2020/mar/23/still-no-service/
ä dabapps.com/blog/django-models-and-encapsulation/
For the authors of Two Scoops of Django the use of service layers isn’t new - we’ve seen
it for years. As with any technique that diverges from core Django practices, our anecdotal
experience is we’ve seen projects fail a bit more frequently using service layers. That isn’t to
say that the approach doesn’t have merit, but rather than the additional abstraction may not
be worth the effort.
There are positives to this approach. Specifically, migrations are easier and table names fol-
low a simplified pattern.
It has to be said that other frameworks such as Rails embrace this technique and are quite
successful. However, while Rails and other tools are designed to follow this pattern, Django
isn’t optimized for this design. Using this technique with Django requires experience and
expertise with patterns rarely described in documentation on Django.
Typically, everything is lumped together in giant models.py, views.py, tests.py, and urls.py
modules. Eventually either the project collapses under its own growing complexity or the
files are broken up into sub-modules based on the domains covered by models. Using our
icecreamlandia project, anything about “Users” is placed into models/users.py and “Tickets”
is placed into models/tickets.py.
Please note this can work if done with careful foresight. Indeed, Audrey has done it with
a project to great success, it did involve a bit more juggling of code than expected. It’s a
technique that should only be explored after a Djangonaut has completed several successful
Django projects.
4.6 Summary
This chapter covered the art of Django app design. Specifically, each Django app should
be tightly-focused on its own task and possess a simple, easy-to-remember name. If an app
seems too complex, it should be broken up into smaller apps. Getting app design right takes
practice and effort, but it’s well worth the effort.
We also covered alternative approaches to architecting apps and how logic is called within
them.
Django 3 has over 150 settings that can be controlled in the settings module, most of which
come with default values. Settings are loaded when your server starts up, and experienced
Django developers stay away from trying to change settings in production since they require
a server restart.
Figure 5.1: As your project grows, your Django settings can get pretty complex.
As developers, we have our own necessary settings for development, such as settings for
debugging tools that should be disabled (and often not installed to) staging or production
servers.
Furthermore, there are often good reasons to keep specific settings out of public or private
code repositories. The SECRET_KEY setting is the first thing that comes to mind, but API
key settings to services like Amazon, Stripe, and other password-type variables need to be
protected.
The same warning for SECRET_KEY also applies to production database passwords,
AWS keys, OAuth tokens, or any other sensitive data that your project needs in
order to operate.
Later in this chapter, we’ll show how to handle the SECRET_KEY issue in the “Keep
Secret Keys Out With Environment Settings” section.
A common solution is to create local_settings.py modules that are created locally per server
or development machine, and are purposefully kept out of version control. Developers now
make development-specific settings changes, including the incorporation of business logic
without the code being tracked in version control. Staging and deployment servers can have
location-specific settings and logic without them being tracked in version control.
Ahem...
ä Everyone copy/pastes the same local_settings.py module everywhere. Isn’t this a vio-
lation of Don’t Repeat Yourself but on a larger scale?
Let’s take a different approach. Let’s break up development, staging, test, and production
settings into separate components that inherit from a common base object in a settings file
tracked by version control. Plus, we’ll make sure we do it in such a way that server secrets
will remain secret.
Instead of having one settings.py file, with this setup you have a settings/ directory containing
your settings files. This directory will typically contain something like the following:
settings/
├── __init__.py
├── base.py
├── local.py
├── staging.py
├── test.py
├── production.py
Let’s take a look at how to use the shell and runserver management commands with this
setup. You’ll have to use the --settings command line option, so you’ll be entering the
following at the command-line.
To start the Python interactive interpreter with Django, using your settings/local.py settings
file:
To run the local development server with your settings/local.py settings file:
For the settings setup that we just described, here are the values to use with the --settings
command line option or the DJANGO_SETTINGS_MODULE environment variable:
DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'twoscoops',
'HOST': 'localhost',
}
}
INSTALLED_APPS += ['debug_toolbar', ]
Yet there’s another advantage: No more ‘if DEBUG’ or ‘if not DEBUG’ logic to copy/paste
around between projects. Settings just got a whole lot simpler!
At this point we want to take a moment to note that Django settings files are the single,
solitary place we advocate using import *. The reason is that for the singular case of Django
setting modules we want to override all the namespace.
Well, it’s still better tracking these settings in version control than relying on everyone cus-
tomizing the same local.py module to their own tastes. A nice way to do this is with multiple
dev settings files, e.g. local_audrey.py and local_pydanny.py:
# settings/local_pydanny.py
from .local import *
Why? It’s not only good to keep all your own settings files in version control, but it’s also
good to be able to see your teammates’ dev settings files. That way, you can tell if someone’s
missing a vital or helpful setting in their local development setup, and you can make sure
that everyone’s local settings files are synchronized. Here is what our projects frequently use
for settings layout:
settings/
__init__.py
base.py
local_audreyr.py
local_pydanny.py
local.py
staging.py
test.py
production.py
To resolve this, our answer is to use environment variables in a pattern we like to call, well,
The Environment Variables Pattern.
Every operating system supported by Django (and Python) provides the easy capability to
create environment variables.
Here are the benefits of using environment variables for secret keys:
ä Keeping secrets out of settings allows you to store every settings file in version control
without hesitation. All of your Python code really should be stored in version control,
including your settings.
ä Instead of each developer maintaining an easily-outdated, copy-and-pasted version of
the local_settings.py.example file for their own development purposes, everyone shares
the same version-controlled settings/local.py .
ä System administrators can rapidly deploy the project without having to modify files
containing Python code.
ä Most platforms-as-a-service recommend the use of environment variables for config-
uration and have built-in features for setting and managing them.
export SOME_SECRET_KEY=1c3-cr3am-15-yummy
export AUDREY_FREEZER_KEY=y34h-r1ght-d0nt-t0uch-my-1c3-cr34m
On Windows systems, it’s a bit trickier. You can set them one-by-one at the command line
(cmd.exe) in a persistent way with the setx command, but you’ll have to close and reopen
your command prompt for them to go into effect. A better way is to place these commands
at the end of the virtualenv’s bin/postactivate.bat script so they are available upon activation:
PowerShell is much more powerful than the default Windows shell and comes with Win-
dows Vista and above. Setting environment variables while using PowerShell:
[Environment]::SetEnvironmentVariable('SOME_SECRET_KEY',
'1c3-cr3am-15-yummy', 'User')
[Environment]::SetEnvironmentVariable('AUDREY_FREEZER_KEY',
'y34h-r1ght-d0nt-t0uch-my-1c3-cr34m', 'User')
Machine-wide:
[Environment]::SetEnvironmentVariable('SOME_SECRET_KEY',
'1c3-cr3am-15-yummy', 'Machine')
[Environment]::SetEnvironmentVariable('AUDREY_FREEZER_KEY',
'y34h-r1ght-d0nt-t0uch-my-1c3-cr34m', 'Machine')
unset SOME_SECRET_KEY
unset AUDREY_FREEZER_KEY
[Environment]::UnsetEnvironmentVariable('SOME_SECRET_KEY', 'User')
[Environment]::UnsetEnvironmentVariable('AUDREY_FREEZER_KEY',
,→ 'User')
If you are using virtualenvwrapper and want to unset environment variables whenever a
virtualenv is deactivated, place these commands in the postdeactivate script.
can set the environment variables manually. But if you’re using scripts or tools for auto-
mated server provisioning and deployment, your approach may be more complex. Check
the documentation for your deployment tools for more information.
To see how you access environment variables from the Python side, open up a new Python
prompt and type:
>>> import os
>>> os.environ['SOME_SECRET_KEY']
'1c3-cr3am-15-yummy'
To access environment variables from one of your settings files, you can do something like
this:
# Top of settings/production.py
import os
SOME_SECRET_KEY = os.environ['SOME_SECRET_KEY']
This snippet simply gets the value of the SOME_SECRET_KEY environment variable from
the operating system and saves it to a Python variable called SOME_SECRET_KEY.
Following this pattern means all code can remain in version control, and all secrets remain
safe.
Here’s a useful code snippet that makes it easier to troubleshoot those missing environment
variables. If you’re using our recommended environment variable secrets approach, you’ll
want to add this to your settings/base.py file:
# settings/base.py
import os
def get_env_variable(var_name):
"""Get the environment variable or return exception."""
try:
return os.environ[var_name]
except KeyError:
error_msg = 'Set the {} environment
,→ variable'.format(var_name)
raise ImproperlyConfigured(error_msg)
Then, in any of your settings files, you can load secret keys from environment variables as
follows:
SOME_SECRET_KEY = get_env_variable('SOME_SECRET_KEY')
Now, if you don’t have SOME_SECRET_KEY set as an environment variable, you get
a traceback that ends with a useful error message like this:
That being said, if you’re struggling with getting django-admin to work, it’s perfectly
okay to develop and launch your site running it with manage.py.
1 Create a secrets file using the configuration format of choice, be it JSON, .env, Config,
YAML, or even XML.
{
"FILENAME": "secrets.json",
"SECRET_KEY": "I've got a secret!",
"DATABASES_HOST": "127.0.0.1",
"PORT": "5432"
}
To use the secrets.json file, add the following code to your base settings module.
# settings/base.py
import json
SECRET_KEY = get_secret('SECRET_KEY')
Now we are loading secrets from non-executable JSON files instead of from unversioned
executable code. Hooray!
To follow this pattern, recommended to us by Jeff Triplett, first create a requirements/ di-
rectory in the <repository_root>. Then create ‘.txt’ files that match the contents of your
settings directory. The results should look something like:
requirements/
├── base.txt
├── local.txt
├── staging.txt
├── production.txt
In the base.txt file, place the dependencies used in all environments. For example, you might
have something like the following in there:
Django==3.2.0
psycopg2-binary==2.8.8
djangorestframework==3.11.0
Your local.txt file should have dependencies used for local development, such as:
coverage==5.1
django-debug-toolbar==2.2
The needs of a continuous integration server might prompt the following for a ci.txt file:
coverage==5.1
For production:
We humbly beseech the reader to never hardcode file paths in Django settings files. This is
really bad:
# settings/base.py
# Configuring MEDIA_ROOT
# DON’T DO THIS! Hardcoded to just one user's preferences
MEDIA_ROOT = '/Users/pydanny/twoscoops_project/media'
# Configuring STATIC_ROOT
# DON’T DO THIS! Hardcoded to just one user's preferences
STATIC_ROOT = '/Users/pydanny/twoscoops_project/collected_static'
# Configuring STATICFILES_DIRS
# DON’T DO THIS! Hardcoded to just one user's preferences
STATICFILES_DIRS = ['/Users/pydanny/twoscoops_project/static']
# Configuring TEMPLATES
# DON’T DO THIS! Hardcoded to just one user's preferences
TEMPLATES = [
{
'BACKEND':
,→ 'django.template.backends.django.DjangoTemplates',
DIRS: ['/Users/pydanny/twoscoops_project/templates',]
},
]
The above code represents a common pitfall called hardcoding. The above code, called a
fixed path, is bad because as far as you know, pydanny (Daniel Feldroy) is the only person
who has set up their computer to match this path structure. Anyone else trying to use this
example will see their project break, forcing them to either change their directory structure
(unlikely) or change the settings module to match their preference (causing problems for
everyone else including pydanny).
To fix the path issue, we dynamically set a project root variable intuitively named BASE_DIR
at the top of the base settings module. Since BASE_DIR is determined in relation to the
location of base.py, your project can be run from any location on any development computer
or server.
We find the cleanest way to set a BASE_DIR-like setting is with Pathlib, part of Python
since 3.4 that does elegant, clean path calculations:
BASE_DIR = Path(__file__).resolve().parent.parent.parent
MEDIA_ROOT = BASE_DIR / 'media'
STATIC_ROOT = BASE_DIR / 'static_root'
If you really want to set your BASE_DIR with the Python standard library’s os.path library,
though, this is one way to do it in a way that will account for paths:
def root(*dirs):
base_dir = join(dirname(__file__), '..', '..')
return abspath(join(base_dir, *dirs))
BASE_DIR = root()
MEDIA_ROOT = root('media')
STATIC_ROOT = root('static_root')
STATICFILES_DIRS = [root('static')]
TEMPLATES = [
{
'BACKEND':
,→ 'django.template.backends.django.DjangoTemplates',
'DIRS': [root('templates')],
},
]
With your various path settings dependent on BASE_DIR, your file path settings should
work, which means your templates and media should be loading without error.
5.7 Summary
Remember, everything except for passwords and API keys ought to be tracked in version
control.
Any project that’s destined for a real live production server is bound to need multiple set-
tings and requirements files. Even beginners to Django need this kind of settings/require-
ments file setup once their projects are ready to leave the original development machine.
We provide our solution, as well as an Apache-friendly solution since it works well for both
beginning and advanced developers.
Also, if you prefer a different shell than the ones provided, environment variables still work.
You’ll just need to know the syntax for defining them.
The same thing applies to requirements files. Working with untracked dependency differ-
ences increases risk as much as untracked settings.