Using Environment Variables in Django
An environment variable is a variable whose value is set outside the program, typically through a functionality built into the operating system. An environment variable is made up of a name/value pair.
While working with web applications often we need to store sensitive data for authentication of different modules such as database credentials, password, secret key, debug status, email host, allowed hosts and API keys. These sensitive keys should not be hardcoded in the settings.py
file instead they should be loaded with Environment variables on runtime.
Mostly, In a development environment we runs our application with debug mode on. Also, it’s a good idea to keep the secret key in a safe place (not in your git repository).
In this blog, we will learn about the python decouple library.
Python Decouple
Python Decouple is a great library that helps you strictly separate the settings parameters from your source code. The idea is simple: Parameters related to the project, goes straight to the source code. Parameters related to an instance of the project, goes to an environment file.
Why should use python decouple?
The settings files in web frameworks store many different kinds of parameters:
- Locale and i18n;
- Middlewares and Installed Apps;
- Resource handles to the database, Memcached, and other backing services;
- Credentials to external services such as Amazon S3 or Twitter;
- Per-deploy values such as the canonical hostname for the instance.
The first 2 are project settings and the last 3 are instance settings.
You should be able to change instance settings without redeploying your app.
Why not just use environment variables?
Envvars
works, but since os.environ
only returns strings, it’s tricky.
Let’s say you have an envvar DEBUG=False
. If you run:
if os.environ['DEBUG']:
print(True)
else:
print(False)
It will print True
, because os.environ['DEBUG']
returns the string “False”. Since it’s a non-empty string, it will be evaluated as True.
Decouple provides a solution that doesn’t look like a workaround: config('DEBUG', cast=bool)
.
Let’s start, Django Environment Variables
Installation
$ pip install python-decouple
Or download it from PyPI if you prefer.
Add decouple
app into the INSTALLED_APPS
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Python Decouple app
'decouple'
]
Usage
Let’s consider the following settings.py
file, to explain how to use the library.
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = '3izb^ryglj(bvrjb2_y1fZvcnbky#358_l6-nn#i8fkug4mmz!'
DEBUG = True
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'DB_NAME',
'USER': 'DB_USER',
'PASSWORD': 'DB_PASS',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
Creating Environment Variables
First create a file named .env
in the root of your project where manage.py file resides and add the following key-value pair inside the file.. You can also use a .ini
file, in case the .env
isn’t suitable for your use case. See the documentation for further instructions.
SECRET_KEY=3izb^ryglj(bvrjb2_y1fZvcnbky#358_l6-nn#i8fkug4mmz!
DEBUG=True
DB_NAME=DB_NAME
DB_USER=DB_USER
DB_PASS=DB_PASS
DB_HOST=127.0.0.1
DB_PORT=5432
Note: If you are working with Git, update your
.gitignore
adding the.env
file so you don’t commit any sensitive data to your remote repository. It’s advisable to create a.env.example
with a template of all the variables required for the project.
Access the variables
Import the library
from decouple import config
Retrieve the settings parameters:
import os
from decouple import config
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASS'),
'HOST': config('DB_HOST'),
'PORT': config('DB_HOST'),
}
}
Casting the data
Attention to the cast argument. Django expects DEBUG
to be a boolean. In a similar way it expects EMAIL_PORT
to be an integer.
DEBUG = config('DEBUG', cast=bool)
EMAIL_PORT = config('EMAIL_PORT', cast=int)
Actually, the cast argument can receive any callable, that will transform the string value into something else. In the case of the ALLOWED_HOSTS
, Django expects a list of hostname.
In the .env
file you can put it like this:
ALLOWED_HOSTS=.localhost, skillshats.com, 127.0.0.1
And then in the settings.py
you can retrieve it this way:
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=lambda v: [s.strip() for s in v.split(',')])
It’s looks little bit complicated, right? Actually the library comes with a Csv Helper, so you don’t need to write all this code. The better way to do it would be:
from decouple import config, Csv
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
Choices
Allows for cast and validation based on a list of choices. For example:
.env
file
CONNECTION_TYPE=usb
In setting file
CONNECTION_TYPE = config('CONNECTION_TYPE', cast=Choices(['eth', 'usb', 'bluetooth']))
If you set and random value for the CONNECTION_TYPE, that does not exists in the list, Then it’ll throw an ValueError
CONNECTION_TYPE=serial
config('CONNECTION_TYPE', cast=Choices(['eth', 'usb', 'bluetooth']))
Traceback (most recent call last):
...
ValueError: Value not in list: 'serial'; valid values are ['eth', 'usb', 'bluetooth']
You can also use a Django-like choices tuple:
USB = 'usb'
ETH = 'eth'
BLUETOOTH = 'bluetooth'
CONNECTION_OPTIONS = (
(USB, 'USB'),
(ETH, 'Ethernet'),
(BLUETOOTH, 'Bluetooth'),
)
CONNECTION_TYPE = config('CONNECTION_TYPE', cast=Choices(choices=CONNECTION_OPTIONS))
Default values
You can add an extra argument to the config
function, to define a default value, in case there is an undefined value in the .env
file.
DEBUG = config('DEBUG', default=True, cast=bool)
Meaning you won’t need to define the DEBUG
parameter in the .env
file in the development environment for example.
Overriding config files
In case you want to temporarily change some of the settings parameter, you can override it with environment variables:
DEBUG=False python manage.py
Load .env
file outside the expected paths
Let’s see, how do you use python-decouple
to load .env
file outside the expected paths or custom paths?
- Instead of importing
decouple.config
and doing the usualconfig('KEY_NAME')
- Create a new
decouple.Config
object usingRepositoryEnv('/path/to/env-file')
from decouple import Config, RepositoryEnv
DOTENV_FILE = '/opt/envs/project-name/.env'
env_config = Config(RepositoryEnv(DOTENV_FILE))
# use the Config().get() method as you normally would since
# decouple.config uses that internally.
# i.e. config('SECRET_KEY') = env_config.get('SECRET_KEY')
SECRET_KEY = env_config.get('SECRET_KEY')