Launching Darkly with Python and the Star Wars API

In this post, we’re going to take a rapid fire look at bootstrapping a Python project and exploring feature flags and application delivery with LaunchDarkly. We’ll get our first web application up using a popular web framework for Python called Flask, and then dive into how we can use LaunchDarkly to drive changes to the way the application functions by introducing feature flags.

Historically, we’ve ended up in a world where the deploy and release portions of application development are closely attached to each other. Applications are built, and when we deploy the code to a place, and that deployment finishes, our app is released!

That all sounds so smooth, but it’s actually a pretty narrow canyon. The code either deploys successfully or it doesn’t. If it doesn’t, you’ve potentially got an outage on your hands. Wouldn’t it be better if you could target a subset of users to test that code against first while still serving the unchanged version to users? What if you want to back the change our entirely without redeploying the code? Deployment has come to mean so much more than just, “Is the code running?” These days, deployment also means “functionally stable” as well.

LaunchDarkly enables users to separate that deploy and release process into two distinct paths, giving you full control of which users (and when!) are able to hit that new code, all from the same environment. Operators can deploy their workload with the updated code and place it behind a feature flag (effectively keeping that code out of the runtime path) and when ready, enable it for consumption. This flexibility extends to allowing teams to gradually release functionality to user groups (or segments) and builds the foundation of “progressive delivery” and modern application delivery. This is what we mean when we throw the phrase “test in production” around.

Let’s jump in and see how!

Diving into Python and Flask

I love Flask for this example because it's visual and has a fairly low bar of complexity. We can build a simple page, run our Flask application, and watch via a browser as it changes. In order to do that, we need to get a few things installed.

Note: It’s expected you have Python3 installed on your workstation in order to follow along. This will all work locally on your workstation, but we can’t make lemonade without lemons here :).

Launch your terminal and run the following command to ensure you have Flask as well as the requests module installed:

pip3 install flask requests

It would be wise for you to create a directory somewhere to store your project files in. Since this is going to be a very basic page, similarly we’ll keep this very basic and skate over general directory structure. For now, create a main.py file in the directory of your choosing, and populate it with the following text:

from flask import Flask, jsonify
import requests

app = Flask(__name__)

@app.route("/")
def get_feature():  
    text = "The app is up!"
    return text

if __name__ == '__main__':
    app.run(debug=True)

This simple boilerplate brings in both the Flask components and the requests module, and creates a basic version of our application that will respond on the base path in a web browser. Save the file, and from your terminal (in the directory you placed the main.py file - run the following command:

python3 main.py

Note: If you’re using an IDE like Visual Studio Code, you can usually right click the file and tell it to launch in Python.

You should see a return text similar to below:

$ /usr/local/bin/python3 /Users/codydearkland/python-launchdarkly-demo/main.py
 * Serving Flask app 'main' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 597-409-598

You’ll note the line that says Running on http://127.0.0.1:5000/. If you launch your web browser and navigate to that address, you should see the text The app is up! display on your screen.

Assuming the answer is yes… FANTASTIC! Let's get into LaunchDarkly now and start creating our flags!

Stepping into LaunchDarkly

For the purposes of this post, we’ll assume you’ve visited LaunchDarkly and created your demo account. We give you 14 days of splendor to take in all the glory feature flagging has to offer.

When you sign into your account, you should be in your default project. We use projects to separate out the different groupings of flags, experimentations, and configurations that users may create. These projects might be different applications, as an example. Within each project we have environments. Environments are things like “dev” or “production.” Using my LaunchDarkly account, you can see a rough idea of what this looks like in practice:

Quick sideway tour on environments

Each environment has its own SDK key which allows the application to talk back to LaunchDarkly. You’ll need to leverage this key along side your application in order to resolve. Separating the SDK to different environments allows us to get very granular in controlling the flags applied and evaluated within an application.

You’ll see that in my projects I have SDKs configured for both my Test and Production environments. We’ll be using this SDK key within our Python application. Go ahead and copy down your SDK key for the Test environment. We’ll need to use this later!

Creating Out First Feature Flag

In our application, we created a Flask route that would return a simple string back to the user when they hit the URL. We’ll create a boolean (true or false) and use this to change the text thats being displayed.

Select Feature flags from the left hand navigation bar and select the “Create flag” option on the right hand side of the screen… OR… Did you know we added a new navigation option to LaunchDarkly recently? If you’re on a Mac or PC, you can press the CMD+K (or CTRL+K key on windows) to bring up our new command bar navigation menu. This menu acts as a shortcut to many of the common areas of the LaunchDarkly UI.

If we type “Create” you’ll see that the menu options filter down and you can quickly hit the Create new flag option (alternative you could hit G and then C to quickly use this shortcut.)

We’ll name our new flag textReturn which will also populate the “Key” field with the new value. We’ll want to leave the “SDKs using Client-side ID” box checked, and for simplicity we won’t fill in any other boxes as the default flag is a boolean flag.

Select “Save flag” on the bottom and you’ll see that you’ve returned back to the main list of flags in your environment. This flag is ready to consume in our application!

Our flag is created and ready to be used, it’s set to “off” by default, which means that within our application it will currently evaluate as a “false”. Let's jump back over to Python now and bring this in!

Enabling the LaunchDarkly SDK for Python

If you’ve been following along at home, you’ll note that when you go back into Python, the development server is still running. Let’s go ahead and cancel that (CMD+C or CTRL+C depending on Mac vs PC) and run the following command:

pip3 install launchdarkly-server-sdk

You should see a flurry of text indicating a package was installed. You’ll note that we’re referring to a server SDK here, and this would be a good time to take a break and call out the difference between client-side and server-side.

Client-side flags represent flags that render on the “client side” of the connection. This could be a mobile device or a web browser on their workstation. You can find a current list of our client-side SDK’s within our documentation. Client-side SDKs have the following distinctions:

  • Flags are rendered at the user workstation
  • Only flags and evaluation rules for the current user are transmitted

Client-side systems are considered insecure by default so we transmit the minimum amount of data about a flag environment to the end users workstation.

Server-side flags represent flags that are rendered on the “server.” The SDK being used in this example (Python) is a server-side SDK. Server-side SDKs return all flags and rules for all users in a given environment and all flag evaluations happen within the server. You can see all of our server side SDKs within our documentation.

With that handled, let’s jump back in to our code!

Importing the Python SDK and Evaluating Our Flag

From our Python application, we’ll need to add the following import text to to the top of our application to bring in the necessary SDK components.

import ldclient
from ldclient.config import Config

Below the app = Flask(__name__) line, we’ll want to create a static user object (so we can play with targeting rules in a future post :)). Add the following block of code to create our user object (in JSON format):

user = {
    "key": "aa0ceb",
    "firstName": "Mara",
    "lastName": "Jade",
    "email": "[email protected]",
    "custom": {
      "groups": ["Jedi"]
    }
}

Typically we’d have some way of identifying the user via a login or some other unique details, but for simplicity, we’ve chosen to do it statically.

Now move back to the @app.route section of the code. Since we’re going to be updating our application with a new set of code, this seems like a fantastic time to show LaunchDarkly in action by deploying a new capability behind a feature flag.

If you’ll recall, at the start of this post we brought in the requests module. This module is used for making web requests outbound and is commonly used to retrieve results. Let’s add a feature that when enabled calls out to an API. Since our user, Mara Jade, is a “Star Wars” character, we’ll use the Star Wars API to look up one of her friends (using that term very loosely, depending upon which part of the story you’re at), Luke Skywalker.

First, in order for us to evaluate our flag, we need to instantiate the LaunchDarkly client. We need to provide it with our SDK key for our environment, and check our flag. Update the @app.route("/") section to use the following code:

@app.route("/")
def get_feature():  
    ldclient.set_config(Config("sdk-7e5424a3-01a1-43fb-93e1-f478f2bf9327"))
    show_feature = ldclient.get().variation("textReturn", user, "No Data")
    if show_feature == True:
        r = requests.get("https://swapi.dev/api/people/1/").json()
        ldclient.get().close()
        return jsonify(r)
    else:
        text = "The app is up!"
        return text

Directly under the get_feature() function, you can see we configure the our ldclient(LaunchDarkly client) with an SDK key from our project/environment. Below this we create an object that evaluates the flag that we’re targeting, textReturn in this case. We feed that object a user object as well as a default flag value (this is useful for times where your application might become disconnected from LaunchDarkly for some reason.)

This is where things get really interesting. We look at that flag value, and determine if its True or False. If it's True (enabled), we have our application make an API call to the Star Wars API and return the result (and close our connection to the SDK afterwards, because cleaning up after yourself is the right thing to do). If it’s False (disabled), we return the previous code we had used earlier.

Save your code and reload your page. At the end of it all, your code should look like the following:

import requests
import os
import ldclient
from ldclient.config import Config
from flask import Flask, jsonify

app = Flask(__name__)

user = {
    "key": "aa0ceb",
    "firstName": "Mara",
    "lastName": "Jade",
    "email": "[email protected]",
    "custom": {
      "groups": ["Jedi"]
    }
}

@app.route("/")
def get_feature():  
    ldclient.set_config(Config("sdk-7e5424a3-01a1-43fb-93e1-f478f2bf9327"))
    show_feature = ldclient.get().variation("textReturn", user, "No Data")
    if show_feature == True:
        r = requests.get("https://swapi.dev/api/people/1/").json()
        ldclient.get().close()
        return jsonify(r)
    else:
        text = "The app is up!"
        return text

if __name__ == '__main__':
    app.run(debug=True)

Note: Flask supports “hot-reloading” so the code should update as you save, but if something goes wrong, you may need to reload the application by cancelling the existing instance and running python3 main.py once again from the applications directory.

When you reload the page - you should see the previous response still there (The app is up!). We’re almost there!

Enabling Our Shiny New Feature

Flip back into LaunchDarkly, and select “Enable” the textReturn flag we created earlier. Assuming everything went successfully, you should see the following flow occur!

Putting it in Context!

This was a very basic example, but it highlights an extremely common flow of developers adding new functionality to applications. In our sample, we went from a static text return to returning the results of an external API call. When the flag was on, we saw the new functionality present, when it was off, we saw our previous code.

What if we had made a mistake on the way we were returning the data and it caused errors in the application? Disabling the new functionality would’ve been as simple as “disabling” the flag again. This configuration was set to use a single user, but it’s easily adaptable to multiple users which opens the door to targeting based rollouts as well as user specific targeting.

This sample, while functional, isn’t terribly useful (sticking with the “Star Wars” theme, our application is a bit like Tatooine right now; a mostly barren desert planet). We can return data from the external API - but it’s not setup in a “human readable” fashion. The more common approach would be to return parts of this information into a table that we could then interact with. This sure sounds like a new feature!

In our next blog (in a galaxy not so far away) we will explore making our page a bit more visually appealing, and more functional, using LaunchDarkly. Keep an eye out for it!

For now, I hope you enjoyed the first steps on our journey to being a Feature Flag Jedi (we might have just taken the analogy too far…). May the force be with you!

18