Professional Feature Flags For Django


Motivation

Any non-trivial project I’ve ever worked on sooner or later had a need for feature flags (also called feature toggles). They can be used for awesome stuff but they can also be used terrible things. It’s really up to you and your team.

The goal

At the end of this tutorial you will know

  • how to implement feature flags in django
  • how to use a third-party feature-flag-provider to manage your feature-flags
  • have an idea how this can be used to roll-out new features to a subset of your users (e.g. only staff-members)

Getting the project source-code

The example-project can be found on github. Here are the steps I used to create the initial commit. I assume you have pipenv installed (and, of course, Python 3):

Here is what you need to get the project files:

# Follow these instructions to install the project in ./django_feature_flags_example

$ git clone https://github.com/steuke/django_feature_flags_example.git
Cloning into 'django_feature_flags_example'...
remote: Enumerating objects: 41, done.
remote: Counting objects: 100% (41/41), done.
remote: Compressing objects: 100% (30/30), done.
remote: Total 41 (delta 9), reused 41 (delta 9), pack-reused 0
Unpacking objects: 100% (41/41), done.

Let’s create a virtual environment, activate it, and install the dependencies:

(Are you using pipenv? Then you can simply run pipenv install in the git-folder that contains the Pipfile and skip this part.)

$ cd django_feature_flags_example
$ python3 -m venv ./venv
$ source ./venv/bin/activate
$ pip install -r requirements.txt

Verify that it all worked:

$ feature_flags_project/manage.py
Traceback (most recent call last):
  File "feature_flags_project/manage.py", line 21, in <module>
  ...
ValueError: You must supply a valid Optimizely SDK-key. Did you remember to set settings.OPTIMIZELY_SDK_KEY?

If you see the above error, then you are all set up to follow this tutorial. :)

Important: The example-project is NOT PRODUCTION-READY. It is meant as a convenient way to follow this tutorial. More information can be found in the Django deployment check-list.

Project Layout

feature_flags_project/
├── feature_flags_project       # django project folder
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py             # <--- you need to add the Optimizely key here (later on)
│   ├── urls.py
│   └── wsgi.py
├── feature_flags               # feature flag module (you could copy this to your own project)
│   ├── __init__.py
│   ├── features.py
│   ├── optimizely_provider.py  # sample implementation of a feature-flag provider
│   └── providers.py
├── manage.py
└── my_app                      # sample django app we use to demonstrate feature flags
    ├── __init__.py
    ├── apps.py
    ├── my_features.py          # contains your feature flag definitions
    ├── templates
    │   └── index.html          # shows how to use feature flags to show/hide html elements
    ├── templatetags
    │   ├── __init__.py
    │   └── my_feature_flags.py # custom template tag for your feature flags
    └── views.py

💡 To use this for an existing project, you can simply copy the feature_flags folder to your project-root.


Create an Optimizely Rollouts account

Sign-up for a free Optimizely Rollouts account.

Why Optimizely? Because it’s free, and I’ve used it before. (There are others: LaunchDarkly, ConfigCat, Cloudbees Rollout etc.) The benefits of using a third-party service is that you will get a robust implementation to manage your feature-flags, including a nice GUI. (If you want to use a different feature-flag-provider (or create your own), you need to subclass feature_flags.providers.FeatureFlagProvider and provide an instance of it to settings.FEATURE_FLAG_PROVIDER.)

Find the OPTIMIZELY_SDK_KEY

  • Log into Optimizely and go to “Settings”. You should find two default environments that Optimizely has created for you. We will be using the development-environment.

  • Copy the SDK-Key for the development-environment to the settings.py.

Replace this:

OPTIMIZELY_SDK_KEY = None

with this (using your own Optimizely SDK-Key):

OPTIMIZELY_SDK_KEY = "w348t7cznw8t7UZBUB"

💡 Depending on how you manage different stages you could put this into an environment-variable. To keep the example simple, I hard-coded it.


Re-create the sample feature in your Optimizely account

The sample django-project comes with a feature named enable_awesome_text_feature. In order to actually be able to turn the feature on and off via Optimizely, you need to create this feature in your Optimizely account (using their UI). This is what it should look like. Make sure to use the exact same name:

/img/posts/optimizely-feature-example.png

(Note: If you use a different name, the sample code will not work. I’ll show you how to create your own feature below.)

Take it for a spin

Open a shell and cd into the project django-project directory (the one containing the manage.pyfile) and run the local development server:

$ ./manage.py runserver
...
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Now open http://127.0.0.1:8000/ in your favorite browser, and you should see this text:

/img/posts/page-with-feature-disabled.png

Did it work? We cannot be sure until we change the feature from disabled to enabled in Optimizely. Let’s do that now.

Switching the feature ON and checking if it worked

  • Go to the Optimizely dashboard and edit the feature settings by clicking the feature’s name.
  • Enable the feature for the correct stage (or all stages, if you are not sure).
  • Be sure to click “Save” at the end!
/img/posts/enable-feature.png

Verify that it worked

For the change to take effect, you have to wait 20 seconds. (This is configured using the settings.OPTIMIZELY_UPDATE_INTERVAL_SECONDS. In a real-life application you would probably increase this time to something like 5 or 10 minutes.) Then reload the page http://127.0.0.1:8000/. It should now show this awesome text (is promised):

/img/posts/page-with-feature-disabled.png

In addition, the console log will show the following line:

INFO Feature 'enable_awesome_text_feature' is enabled=True

🧐 If the awesome text doesn’t show and you see enabled=False instead, then you should double-check the exact spelling you used in Optimizely when you set up the feature. It must match the string of the feature that can be found in my_app/my_features.py.


Creating your own feature

Let’s say you want to create your own, crazy feature. Here are the steps todo just that:

  • create the feature in Optimizely (we’ll name it enable_crazy_feature)
  • add the feature to my_app/my_features.py
  • (optional) create a custom template-tag, so that you can use the feature-flag in django-templates

We will go through this step-by-step.

Adding your new feature to Optimizely

This is the same as before, only with a different name. Click “Create New Feature” and use the name you chose. We will use enable_crazy_feature as the name.


💡 I prefer to give feature-flags very explicit names. No-one should need to guess what a feature does when it is enabled. Adding a description in the Optimizely UI helps as well. That being said, I couldn’t come up with a better name for this tutorial-feature, so we’ll stick to the terrible name enable_crazy_feature.


Adding your feature to my_app/my_features.py

Add the following line at the end of the file:

ENABLE_CRAZY_FEATURE = Feature(name="enable_crazy_feature")

The above code will register the new feature-flag with our feature-flags-module.

To use your new feature-flag you simply import it and call it’s is_enabled() method:

from my_app.my_features import ENABLE_CRAZY_FEATURE

if ENABLE_CRAZY_FEATURE.is_enabled():
    print("We are in crazy mode!")
else:
    print("Nothing crazy is going on.")

Adding a custom template-tag for your feature (optional)

If you want to use the feature-flag in a template, you can create a simple custom tag. Add the following lines to my_app/templatetags/my_feature_flags.py:

from my_app.my_features import ENABLE_CRAZY_FEATURE

@register.simple_tag
def enable_crazy_feature() -> bool:
    return ENABLE_CRAZY_FEATURE.is_enabled()

⚠️ You must restart the development-server for this change to take effect as noted in the Django documentation.


Using the custom template-tag in your templates to show some text (optional)

Using this new template-tag is a little awkward, because you have to define a variable inside your template to use in your if-statement. Here is how it’s done:

{% enable_crazy_feature as crazy_feature_enabled %}
{% if crazy_feature_enabled %}
    <h1>And this is just crazy! 🥳</h1>
{% else %}
    <p>Not crazy at all.</p>
{% endif %}

Using Optimizely-Audiences to roll-out features to a subset of your users

While this is beyond the scope of this introduction, I wanted to give you an idea how feature-flags can be even more customized. The feature-flags you have seen until now are global feature flags, i.e. they are either enabled or disabled for all your users.

What if you want to enable a certain feature only for a subset of users, maybe only the staff-members (based on Django’s built-in is_staff user-property?

This can be done by using Optimizely’s audiences and attributes:

  • within Optimizely, create an attribute (e.g. “is_staff”)
  • within Optimizely, create an audience “Staff-Members” with a condition “is_staff equals true”
  • within Optimizely, edit your feature and set its audience to “Staff-Members” (instead of “Everyone”)
  • in your code, set the attribute “is_staff” to “true” for all staff-members.

The latter has been already implemented in the sample project: Have a look at the _attributes_from_request()-method (located in feature_flags/providers.py).

Things you’ll want to add

There a many things that are missing from the sample project. In a production project you would at least need the following:

  • automated tests
  • a way to handle different environments/stages (development, testing, staging, production)
  • a mock-provider for testing

In addition, you’ll need to think about how you will handle database-migrations and roll-backs, but that’s a whole different topic for another day.


See also