Back to Django Environ

Tips

docs/tips.rst

0.13.012.2 KB
Original Source

==== Tips

Handling Inline Comments in .env Files

django-environ provides an optional feature to parse inline comments in .env files. This is controlled by the parse_comments parameter in the read_env method.

Modes

  • Enabled (parse_comments=True): Inline comments starting with # will be ignored.
  • Disabled (parse_comments=False): The entire line, including comments, will be read as the value.
  • Default: The behavior is the same as when parse_comments=False.

Side Effects

While this feature can be useful for adding context to your .env files, it can introduce unexpected behavior. For example, if your value includes a # symbol, it will be truncated when parse_comments=True.

Why Disabled by Default?

In line with the project's philosophy of being explicit and avoiding unexpected behavior, this feature is disabled by default. If you understand the implications and find the feature useful, you can enable it explicitly.

Example

Here is an example demonstrating the different modes of handling inline comments.

.env file contents:

.. code-block:: shell

.env file contents

BOOL_TRUE_WITH_COMMENT=True # This is a comment STR_WITH_HASH=foo#bar # This is also a comment

Python code:

.. code-block:: python

import environ

Using parse_comments=True

env = environ.Env() env.read_env(parse_comments=True) print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True print(env('STR_WITH_HASH')) # Output: foo

Using parse_comments=False

env = environ.Env() env.read_env(parse_comments=False) print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True # This is a comment print(env('STR_WITH_HASH')) # Output: foo#bar # This is also a comment

Using default behavior

env = environ.Env() env.read_env() print(env('BOOL_TRUE_WITH_COMMENT')) # Output: True # This is a comment print(env('STR_WITH_HASH')) # Output: foo#bar # This is also a comment

Docker-style file based variables

Docker (swarm) and Kubernetes are two widely used platforms that store their secrets in tmpfs inside containers as individual files, providing a secure way to be able to share configuration data between containers.

Use :class:.environ.FileAwareEnv rather than :class:.environ.Env to first look for environment variables with _FILE appended. If found, their contents will be read from the file system and used instead.

For example, given an app with the following in its settings module:

.. code-block:: python

import environ

env = environ.FileAwareEnv() SECRET_KEY = env("SECRET_KEY")

the example docker-compose.yml for would contain:

.. code-block:: yaml

secrets: secret_key: external: true

services: app: secrets: - secret_key environment: - SECRET_KEY_FILE=/run/secrets/secret_key

Using unsafe characters in URLs

In order to use unsafe characters you have to encode with :py:func:urllib.parse.quote before you set into .env file. Encode only the value (i.e. the password) not the whole url.

.. code-block:: shell

DATABASE_URL=mysql://user:%[email protected]:3306/dbname

See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for reference.

Smart Casting

django-environ has a "Smart-casting" enabled by default, if you don't provide a cast type, it will be detected from default type. This could raise side effects (see #192 <https://github.com/joke2k/django-environ/issues/192>_). To disable it use env.smart_cast = False.

.. note::

The next major release will disable it by default.

Callable defaults (lazy evaluation)

The default argument accepts any callable (a function, lambda, or class that implements __call__). When the environment variable is absent the callable is invoked with no arguments and its return value is used as the default. The callable is not invoked when the variable is present in the environment.

This is useful when generating a default value has side-effects (e.g. creating a temporary directory) and you only want that to happen when the variable is truly missing.

.. code-block:: python

import tempfile import environ

env = environ.Env()

tempfile.mkdtemp() is called only when MEDIA_ROOT is not set

MEDIA_ROOT = env('MEDIA_ROOT', default=tempfile.mkdtemp)

.. note::

When smart_cast is enabled (the default), the cast type is not inferred from a callable default. Provide an explicit cast or a type in the scheme tuple if you need type coercion.

Warn when defaults are used

If you want visibility when a missing environment variable falls back to a default value, enable warnings on the Env instance:

.. code-block:: python

import environ

env = environ.Env() env.warn_on_default = True value = env("MISSING_VAR", default="fallback")

When enabled, django-environ emits DefaultValueWarning for missing variables that return an explicit default.

Multiple redis cache locations

For redis cache, multiple master/slave or shard locations can be configured as follows:

.. code-block:: shell

CACHE_URL='rediscache://master:6379,slave1:6379,slave2:6379/1'

Email settings

In order to set email configuration for Django you can use this code:

.. code-block:: python

The email() method is an alias for email_url().

EMAIL_CONFIG = env.email( 'EMAIL_URL', default='smtp://user:password@localhost:25' )

vars().update(EMAIL_CONFIG)

SQLite urls

SQLite connects to file based databases. The same URL format is used, omitting the hostname, and using the "file" portion as the filename of the database. This has the effect of four slashes being present for an absolute

file path: sqlite:////full/path/to/your/database/file.sqlite.

Nested lists

Some settings such as Django's ADMINS make use of nested lists. You can use something like this to handle similar cases.

.. code-block:: python

DJANGO_ADMINS=Blake:[email protected],Alice:[email protected]

ADMINS = [x.split(':') for x in env.list('DJANGO_ADMINS')]

or use more specific function

from email.utils import getaddresses

DJANGO_ADMINS=Alice Judge [email protected],[email protected]

ADMINS = getaddresses([env('DJANGO_ADMINS')])

another option is to use parseaddr from email.utils

DJANGO_ADMINS="Blake [email protected], Alice Judge [email protected]"

from email.utils import parseaddr

ADMINS = tuple(parseaddr(email) for email in env.list('DJANGO_ADMINS'))

.. _complex_dict_format:

Complex dict format

Sometimes we need to get a bit more complex dict type than usual. For example, consider Djangosaml2's SAML_ATTRIBUTE_MAPPING:

.. code-block:: python

SAML_ATTRIBUTE_MAPPING = { 'uid': ('username', ), 'mail': ('email', ), 'cn': ('first_name', ), 'sn': ('last_name', ), }

A dict of this format can be obtained as shown below:

.env file:

.. code-block:: shell

.env file contents

SAML_ATTRIBUTE_MAPPING="uid=username;mail=email;cn=first_name;sn=last_name;"

settings.py file:

.. code-block:: python

settings.py file contents

import environ

env = environ.Env()

{'uid': ('username',), 'mail': ('email',), 'cn': ('first_name',), 'sn': ('last_name',)}

SAML_ATTRIBUTE_MAPPING = env.dict( 'SAML_ATTRIBUTE_MAPPING', cast={'value': tuple}, default={} )

Multiline value

To get multiline value pass multiline=True to str().

.. note::

You shouldn't escape newline/tab characters yourself if you want to preserve the formatting.

The following example demonstrates the above:

.env file:

.. code-block:: shell

.env file contents

UNQUOTED_CERT=---BEGIN---\r\n---END--- QUOTED_CERT="---BEGIN---\r\n---END---" ESCAPED_CERT=---BEGIN---\n---END---

settings.py file:

.. code-block:: python

settings.py file contents

import environ

env = environ.Env()

print(env.str('UNQUOTED_CERT', multiline=True))

---BEGIN---

---END---

print(env.str('UNQUOTED_CERT', multiline=False))

---BEGIN---\r\n---END---

print(env.str('QUOTED_CERT', multiline=True))

---BEGIN---

---END---

print(env.str('QUOTED_CERT', multiline=False))

---BEGIN---\r\n---END---

print(env.str('ESCAPED_CERT', multiline=True))

---BEGIN---\

---END---

print(env.str('ESCAPED_CERT', multiline=False))

---BEGIN---\n---END---

Restrict string values with choices

You can restrict env.str() to an allowed list of values using choices. If the value is not in the provided list, django.core.exceptions.ImproperlyConfigured is raised.

.. code-block:: python

import environ from django.core.exceptions import ImproperlyConfigured

env = environ.Env()

APP_ENV=prod

env.str("APP_ENV", choices=("dev", "prod", "staging")) # "prod"

APP_ENV=unknown

try: env.str("APP_ENV", choices=("dev", "prod", "staging")) except ImproperlyConfigured: ...

Proxy value

Values that begin with a $ may be interpolated. Pass interpolate=True to environ.Env() to enable this feature:

.. code-block:: python

import environ

env = environ.Env(interpolate=True)

BAR=FOO

PROXY=$BAR

print(env.str('PROXY')) FOO

Escape Proxy

If you're having trouble with values starting with dollar sign ($) without the intention of proxying the value to another, You should enable the escape_proxy and prepend a backslash to it.

.. code-block:: python

import environ

env = environ.Env()
env.escape_proxy = True

# ESCAPED_VAR=\$baz
env.str('ESCAPED_VAR')  # $baz

Reading env files

.. _multiple-env-files-label:

Multiple env files

There is an ability point to the .env file location using an environment variable. This feature may be convenient in a production systems with a different .env file location.

The following example demonstrates the above:

.. code-block:: shell

/etc/environment file contents

DEBUG=False

.. code-block:: shell

.env file contents

DEBUG=True

.. code-block:: python

env = environ.Env() env.read_env(env.str('ENV_PATH', '.env'))

Now ENV_PATH=/etc/environment ./manage.py runserver uses /etc/environment while ./manage.py runserver uses .env.

Using Path objects when reading env

It is possible to use of :py:class:pathlib.Path objects when reading environment file from the filesystem:

.. code-block:: python

import os import pathlib

import environ

Build paths inside the project like this: BASE_DIR('subdir').

BASE_DIR = environ.Path(file) - 3

env = environ.Env()

The four lines below do the same:

env.read_env(BASE_DIR('.env')) env.read_env(os.path.join(BASE_DIR, '.env')) env.read_env(pathlib.Path(str(BASE_DIR)).joinpath('.env')) env.read_env(pathlib.Path(str(BASE_DIR)) / '.env')

.. _overwriting-existing-env:

Overwriting existing environment values from env files

If you want variables set within your env files to take higher precedence than an existing set environment variable, use the overwrite=True argument of :meth:.environ.Env.read_env. For example:

.. code-block:: python

env = environ.Env() env.read_env(BASE_DIR('.env'), overwrite=True)

Handling prefixes

Sometimes it is desirable to be able to prefix all environment variables. For example, if you are using Django, you may want to prefix all environment variables with DJANGO_. This can be done by setting the prefix to desired prefix. For example:

.env file:

.. code-block:: shell

.env file contents

DJANGO_TEST="foo"

settings.py file:

.. code-block:: python

settings.py file contents

import environ

env = environ.Env() env.prefix = 'DJANGO_'

env.str('TEST') # foo