Feature flags let you deploy code to production that’s not yet visible to users, allowing you to test new features in a live environment without impacting the user experience.

Let’s see this in action with a simple Flask app.

from flask import Flask, render_template_string
import os

app = Flask(__name__)

# In a real app, this would come from a database, config file, or external service
# For simplicity, we'll use an environment variable.
# Set to 'true' to enable the new feature, anything else to disable.
FEATURE_FLAG_NEW_NAV = os.environ.get('FEATURE_FLAG_NEW_NAV', 'false').lower()

@app.route('/')
def index():
    if FEATURE_FLAG_NEW_NAV == 'true':
        return render_template_string("""
            <h1>Welcome!</h1>
            <nav>
                <a href="#">Home</a> |
                <a href="#">About Us</a> |
                <a href="#">Contact</a>
            </nav>
            <p>This is the new navigation!</p>
        """)
    else:
        return render_template_string("""
            <h1>Welcome!</h1>
            <nav>
                <a href="#">Home</a> |
                <a href="#">Old Link 1</a> |
                <a href="#">Old Link 2</a>
            </nav>
            <p>This is the old navigation.</p>
        """)

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

To run this:

  1. Save the code as app.py.
  2. To see the old navigation:
    python app.py
    
    Open your browser to http://127.0.0.1:5000/. You’ll see "This is the old navigation."
  3. To see the new navigation:
    FEATURE_FLAG_NEW_NAV=true python app.py
    
    Open your browser to http://127.0.0.1:5000/. You’ll see "This is the new navigation!"

The core problem feature flags solve is the "deploy/don’t deploy" binary choice. Instead of needing to deploy only when a feature is ready and fully tested, you can deploy the code for a new feature behind a flag. This decouples deployment from release. You can deploy code that’s still being iterated on, or even rolled back, without affecting users. This allows for continuous deployment, where code is merged and deployed to production frequently, but only "released" to users when the flag is toggled on.

Internally, a feature flag is just a conditional statement. In its simplest form, it’s an if/else block in your code. The condition checks the state of the flag, which is typically stored externally. This external storage can be a simple environment variable (as shown), a JSON file, a key-value store like Redis, or a dedicated feature flagging service (e.g., LaunchDarkly, Optimizely Feature Experimentation, Unleash). The application reads the flag’s state and executes one code path or another.

The real power comes from managing these flags dynamically. Instead of redeploying your application every time you want to toggle a feature, you can update the flag’s state in its external store. The application, if designed to poll or subscribe to changes, can then react to the new state without a restart. This allows for instantaneous toggling of features, rolling them out to specific user segments (e.g., 10% of users, users in a specific region, beta testers), or quickly disabling a problematic feature.

When you’re using a more sophisticated feature flagging system, you often get advanced targeting capabilities. For instance, you can configure a flag to be true for users with user_id in [101, 205, 310] or for users whose country attribute is CA. This is often implemented by passing user attributes to the feature flagging SDK, which then evaluates the flag’s rules against those attributes to determine the outcome for that specific request. The conditional logic in your Flask app then becomes: if feature_flag_sdk.is_enabled('new_checkout_flow', user_attributes): ....

The most surprising thing about feature flags is how many different strategies you can employ beyond a simple on/off switch. You can use them for A/B testing by splitting traffic between two or more variations of a feature, for gradual rollouts where you increase the percentage of users seeing a feature over time, or even for kill switches to quickly disable a feature experiencing critical errors without a full rollback. The underlying mechanism remains a conditional, but the conditions themselves become incredibly sophisticated.

The next step is often implementing percentage-based rollouts for a new feature.

Want structured learning?

Take the full Flask course →