Skip to main content

Python

Requires Python 3.6 or above

Python SDK Resources
v1.3.1
growthbook-pythonPyPiGet help on Slack

Installation

pip install growthbook

Quick Usage

from growthbook import GrowthBook

# User attributes for targeting and experimentation
attributes = {
"id": "123",
"customUserAttribute": "foo"
}

def on_experiment_viewed(experiment, result):
# Use whatever event tracking system you want
analytics.track(attributes["id"], "Experiment Viewed", {
'experimentId': experiment.key,
'variationId': result.key
})

# Create a GrowthBook instance
gb = GrowthBook(
attributes = attributes,
on_experiment_viewed = on_experiment_viewed,
api_host = "https://cdn.growthbook.io",
client_key = "sdk-abc123"
)

# Load features from the GrowthBook API with caching
gb.load_features()

# Simple on/off feature gating
if gb.is_on("my-feature"):
print("My feature is on!")

# Get the value of a feature with a fallback
color = gb.get_feature_value("button-color-feature", "blue")

Available starting in version 1.2.0

For improved performance and better resource utilization, especially in async web applications, use the GrowthBookClient class. This approach provides up to 3x better performance by reusing a single client instance across multiple requests instead of creating new instances per request.

Basic Async Usage

from growthbook import GrowthBookClient, Options, UserContext, FeatureRefreshStrategy
import asyncio

async def main():
# Create client options
options = Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# Optional: Enable real-time feature updates
refresh_strategy=FeatureRefreshStrategy.SERVER_SENT_EVENTS
)

# Create and initialize client
client = GrowthBookClient(options)
try:
# Initialize the client before using it
success = await client.initialize()
if not success:
print("Failed to initialize GrowthBook client")
return

# Create user context for targeting
user = UserContext(
attributes={
"id": "123",
"country": "US",
"premium": True
}
)

# Simple feature evaluation
if await client.is_on("new-homepage", user):
print("New homepage is enabled!")

# Get feature value with fallback
color = await client.get_feature_value("button-color", "blue", user)
print(f"Button color is {color}")

# Run an experiment
from growthbook import Experiment
result = await client.run(
Experiment(
key="my-test",
variations=["A", "B"]
),
user
)
print(f"User got variation: {result.value}")
finally:
# Always close the client when done
await client.close()

# Run the async code
asyncio.run(main())

For web framework integration examples, see Integration Examples below.

Real-time Feature Updates

The async client supports real-time feature updates using Server-Sent Events:

from growthbook import GrowthBookClient, Options, FeatureRefreshStrategy

client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# Enable SSE for real-time updates
refresh_strategy=FeatureRefreshStrategy.SERVER_SENT_EVENTS
)
)

Concurrency and Thread Safety

The async client is designed to be thread-safe and handle concurrent requests efficiently. You can safely use a single client instance across multiple coroutines:

from fastapi import FastAPI
from growthbook import GrowthBookClient, Options, UserContext
import asyncio

app = FastAPI()

# Single client instance shared across all requests
gb_client = GrowthBookClient(Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
))

@app.on_event("startup")
async def startup():
await gb_client.initialize()

@app.on_event("shutdown")
async def shutdown():
await gb_client.close()

@app.get("/batch")
async def batch_process(user_ids: list[str]):
# Safely process multiple users concurrently
tasks = []
for user_id in user_ids:
user = UserContext(attributes={"id": user_id})
tasks.append(gb_client.eval_feature("new-feature", user))

results = await asyncio.gather(*tasks)
return {"results": results}

Note: While the client is thread-safe, you should not share a single UserContext instance across different requests. Create a new UserContext for each request to maintain proper isolation.

Performance Benefits

The GrowthBookClient provides significant performance improvements over the traditional per-request GrowthBook approach:

  • 3x faster feature evaluations due to instance reuse
  • Lower memory usage by sharing feature data across requests
  • Built-in caching with configurable refresh strategies
  • Real-time updates without polling overhead
  • Async/await support for non-blocking operations

Loading Features

There are two ways to load feature flags into the GrowthBook SDK. You can either use the built-in fetching/caching logic or implement your own custom solution.

Built-in Fetching and Caching

Both the async client and traditional client support built-in fetching and caching of feature flags.

For the async client, use GrowthBookClient with Options:

import asyncio
from growthbook import GrowthBookClient, Options, FeatureRefreshStrategy

async def main():
# Create client with built-in fetching and caching
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# Optional: Configure caching
cache_ttl=300, # Cache for 5 minutes
# Optional: Enable real-time updates
refresh_strategy=FeatureRefreshStrategy.SERVER_SENT_EVENTS,
# Optional: Encryption support
decryption_key="your-decryption-key"
)
)

try:
# Initialize features (async)
await client.initialize()

# Features are now cached and ready to use
user = UserContext(attributes={"id": "user_123"})
feature_enabled = await client.is_on("my-feature", user)

print(f"Feature enabled: {feature_enabled}")

finally:
await client.close()

asyncio.run(main())

Custom Caching

GrowthBook comes with a custom in-memory cache. If you run Python in a multi-process mode, the different processes cannot share memory, so you likely want to switch to a distributed cache system like Redis instead.

For the async client, you can provide a custom cache implementation:

import asyncio
import json
from redis.asyncio import Redis
from growthbook import GrowthBookClient, Options, AbstractAsyncFeatureCache

class AsyncRedisFeatureCache(AbstractAsyncFeatureCache):
def __init__(self):
self.redis = Redis(host='localhost', port=6379, decode_responses=True)
self.prefix = "gb:async:"

async def get(self, key: str):
data = await self.redis.get(self.prefix + key)
return None if data is None else json.loads(data)

async def set(self, key: str, value: dict, ttl: int) -> None:
await self.redis.set(
self.prefix + key,
json.dumps(value),
ex=ttl
)

async def close(self):
await self.redis.close()

async def main():
# Create custom cache
cache = AsyncRedisFeatureCache()

# Create client with custom cache
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
cache=cache
)
)

try:
await client.initialize()
# Use client...
finally:
await client.close()
await cache.close()

asyncio.run(main())

Custom Implementation

If you prefer to handle the entire fetching/caching logic yourself, you can just pass in a dict of features from the GrowthBook API directly into the constructor:

# From the GrowthBook API
features = {'my-feature':{'defaultValue':False}}

gb = GrowthBook(
features = features
)

Note: When doing this, you do not need to specify your api_host or client_key and you don't need to call gb.load_features().

GrowthBook class

The GrowthBook constructor has the following parameters:

  • enabled (bool) - Flag to globally disable all experiments. Default true.
  • attributes (dict) - Dictionary of user attributes that are used for targeting and to assign variations
  • url (str) - The URL of the current request (if applicable)
  • qa_mode (boolean) - If true, random assignment is disabled and only explicitly forced variations are used.
  • on_experiment_viewed (callable) - A function that takes experiment and result as arguments.
  • api_host (str) - The GrowthBook API host to fetch feature flags from. Defaults to https://cdn.growthbook.io
  • client_key (str) - The client key that will be passed to the API Host to fetch feature flags
  • decryption_key (str) - If the GrowthBook API endpoint has encryption enabled, specify the decryption key here
  • cache_ttl (int) - How long to cache features in-memory from the GrowthBook API (seconds, default 60)
  • features (dict) - Feature definitions from the GrowthBook API (only required if client_key is not specified)
  • forced_variations (dict) - Dictionary of forced experiment variations (used for QA)

There are also getter and setter methods for features and attributes if you need to update them later in the request:

gb.set_features(gb.get_features())
gb.set_attributes(gb.get_attributes())

Attributes

You can specify attributes about the current user and request. These are used for two things:

  1. Feature targeting (e.g. paid users get one value, free users get another)
  2. Assigning persistent variations in A/B tests (e.g. user id "123" always gets variation B)

Attributes can be any JSON data type - boolean, integer, float, string, list, or dict.

For the async client, attributes are passed via UserContext for each evaluation:

import asyncio
from growthbook import GrowthBookClient, Options, UserContext

async def main():
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
)
)

try:
await client.initialize()

# Create user context with attributes
user = UserContext(
attributes={
'id': "123",
'loggedIn': True,
'age': 21.5,
'tags': ["tag1", "tag2"],
'account': {
'age': 90
},
'country': 'US',
'premium': True
}
)

# Use features with user context
feature_enabled = await client.is_on("premium-feature", user)
discount = await client.get_feature_value("discount-percent", 0, user)

# Different user context for another request
guest_user = UserContext(
attributes={
'id': "guest_456",
'loggedIn': False,
'country': 'CA'
}
)

guest_feature = await client.is_on("premium-feature", guest_user)

finally:
await client.close()

asyncio.run(main())

Secure Attributes

When secure attribute hashing is enabled, all targeting conditions in the SDK payload referencing attributes with datatype secureString or secureString[] will be anonymized via SHA-256 hashing. This allows you to safely target users based on sensitive attributes. You must enable this feature in your SDK Connection for it to take effect.

If your SDK Connection has secure attribute hashing enabled, you will need to manually hash any secureString or secureString[] attributes that you pass into the GrowthBook SDK.

To hash an attribute, use the hashlib library with SHA-256 support, and compute the SHA-256 hashed value of your attribute plus your organization's secure attribute salt.

For the async client, hash secure attributes before creating the UserContext:

import asyncio
import hashlib
from growthbook import GrowthBookClient, Options, UserContext

def hash_secure_attribute(value: str, salt: str) -> str:
"""Helper function to hash secure attributes"""
return hashlib.sha256(f"{salt}{value}".encode()).hexdigest()

async def main():
# Your secure attribute salt (set in Organization Settings)
salt = "f09jq3fij"

client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
)
)

try:
await client.initialize()

# Hash secure attributes
user_email = "user@example.com"
user_tags = ["premium", "beta"]

hashed_email = hash_secure_attribute(user_email, salt)
hashed_tags = [hash_secure_attribute(tag, salt) for tag in user_tags]

# Create user context with hashed secure attributes
user = UserContext(
attributes={
'id': "123",
'loggedIn': True,
'email': hashed_email, # secureString
'tags': hashed_tags, # secureString[]
'country': 'US' # regular attribute
}
)

# Use features with secure attributes
premium_feature = await client.is_on("premium-feature", user)
personalized_content = await client.get_feature_value("personalized-content", {}, user)

finally:
await client.close()

asyncio.run(main())

Tracking Experiments

Any time an experiment is run to determine the value of a feature, you want to track that event in your analytics system.

For the async client, you can set up experiment tracking through the Options:

import asyncio
from growthbook import GrowthBookClient, Options, UserContext, Experiment, Result

async def on_experiment_viewed(experiment: Experiment, result: Result):
"""Async callback for experiment tracking"""
# Use whatever event tracking system you want
user_id = result.hash_attribute or "anonymous"

# Example with async analytics
await analytics.track_async(user_id, "Experiment Viewed", {
'experimentId': experiment.key,
'variationId': result.key,
'variationValue': result.value,
'inExperiment': result.in_experiment,
'hashUsed': result.hash_used
})

print(f"Tracked experiment: {experiment.key} -> {result.value}")

async def main():
# Create client with experiment tracking
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
on_experiment_viewed=on_experiment_viewed
)
)

try:
await client.initialize()

# Create user context
user = UserContext(attributes={"id": "user_123", "premium": True})

# Feature evaluations will automatically trigger tracking
feature_value = await client.get_feature_value("button-color", "blue", user)

# Run inline experiments (also triggers tracking)
experiment = Experiment(
key="pricing-test",
variations=["$9.99", "$14.99", "$19.99"]
)

result = await client.run(experiment, user)
print(f"Experiment result: {result.value}")

finally:
await client.close()

asyncio.run(main())

You can also use synchronous callbacks with the async client:

def sync_experiment_tracker(experiment: Experiment, result: Result):
"""Synchronous callback that works with async client"""
# Send to synchronous analytics service
analytics.track("Experiment Viewed", {
'experimentId': experiment.key,
'variationId': result.key,
'userId': result.hash_attribute
})

client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
on_experiment_viewed=sync_experiment_tracker # Sync callback
)
)

Tracking Plugins

Available starting in version 1.3.0

The Python SDK supports tracking plugins that provide automated event tracking with batching, error handling, and retry logic. This is the recommended approach for production applications as it handles edge cases and provides better reliability than custom tracking callbacks.

Built-in Tracking Plugin

The SDK includes a built-in tracking plugin that automatically batches and sends events:

import asyncio
from growthbook import GrowthBookClient, Options, UserContext, TrackingPlugin

# Custom tracking callback for your analytics system
async def my_custom_tracker(events):
"""Process a batch of tracking events"""
for event in events:
# Send to your analytics system
await analytics.track_async(
user_id=event.user_id,
event_name="Experiment Viewed",
properties={
'experiment_id': event.experiment.key,
'variation_id': event.result.key,
'variation_value': event.result.value,
'timestamp': event.timestamp
}
)
print(f"Processed {len(events)} tracking events")

async def main():
# Create client with tracking plugin
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
tracking_plugins=[
TrackingPlugin(
# Your custom tracking function
callback=my_custom_tracker,
# Batch size (optional, default 10)
batch_size=5,
# Flush interval in seconds (optional, default 30)
flush_interval=10,
# Max retries (optional, default 3)
max_retries=2
)
]
)
)

try:
await client.initialize()

user = UserContext(attributes={"id": "user_123"})

# Events are now automatically tracked and batched
result1 = await client.get_feature_value("button-color", "blue", user)
result2 = await client.is_on("premium-feature", user)

# Plugin handles batching and sending automatically

finally:
await client.close()

asyncio.run(main())

Multiple Tracking Plugins

You can use multiple tracking plugins to send events to different analytics systems:

from growthbook import GrowthBookClient, Options, TrackingPlugin

# Different tracking callbacks
async def segment_tracker(events):
"""Send events to Segment"""
for event in events:
await segment.track_async(event.user_id, "Experiment Viewed", {
'experiment_id': event.experiment.key,
'variation': event.result.value
})

async def mixpanel_tracker(events):
"""Send events to Mixpanel"""
for event in events:
await mixpanel.track_async(event.user_id, "Experiment Viewed", {
'experiment_id': event.experiment.key,
'variation': event.result.value
})

# Create client with multiple tracking plugins
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
tracking_plugins=[
TrackingPlugin(
callback=segment_tracker,
batch_size=10,
flush_interval=30
),
TrackingPlugin(
callback=mixpanel_tracker,
batch_size=5,
flush_interval=15
)
]
)
)

# Events are now automatically sent to both Segment and Mixpanel

Tracking Plugin Benefits

The tracking plugin system provides several advantages over custom tracking callbacks:

  • Automatic Batching: Events are batched together to reduce API calls
  • Error Handling: Failed requests are automatically retried with exponential backoff
  • Non-blocking: Tracking doesn't block feature evaluations
  • Configurable: Batch sizes, intervals, and retry logic can be customized
  • Multiple Destinations: Send events to multiple analytics systems simultaneously

Working with Traditional Callbacks

Tracking plugins work alongside your existing on_experiment_viewed callbacks:

def legacy_tracker(experiment, result):
"""Traditional tracking callback"""
print(f"Legacy tracker: {experiment.key}")

client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# Traditional callback still works
on_experiment_viewed=legacy_tracker,
# Plus new tracking plugins
tracking_plugins=[
TrackingPlugin(callback=my_custom_tracker)
]
)
)

# Both the legacy callback and plugin will be triggered

Using Features

There are 3 main methods for interacting with features.

  • gb.is_on("feature-key") returns true if the feature is on
  • gb.is_off("feature-key") returns false if the feature is on
  • gb.get_feature_value("feature-key", "default") returns the value of the feature with a fallback

In addition, you can use gb.evalFeature("feature-key") to get back a FeatureResult object with the following properties:

  • value - The JSON-decoded value of the feature (or None if not defined)
  • on and off - The JSON-decoded value cast to booleans
  • source - Why the value was assigned to the user. One of unknownFeature, defaultValue, force, or experiment
  • experiment - Information about the experiment (if any) which was used to assign the value to the user
  • experimentResult - The result of the experiment (if any) which was used to assign the value to the user

Sticky Bucketing

Available starting in version 1.1.0

By default GrowthBook does not persist assigned experiment variations for a user. We rely on deterministic hashing to ensure that the same user attributes always map to the same experiment variation. However, there are cases where this isn't good enough. For example, if you change targeting conditions in the middle of an experiment, users may stop being shown a variation even if they were previously bucketed into it.

Sticky Bucketing is a solution to these issues. You can provide a Sticky Bucket Service to the GrowthBook instance to persist previously seen variations and ensure that the user experience remains consistent for your users.

A sample InMemoryStickyBucketService implementation is provided for reference, but in production you will definitely want to implement your own version using a database, cookies, or similar for persistence.

Sticky Bucket documents contain three fields

  • attributeName - The name of the attribute used to identify the user (e.g. id, cookie_id, etc.)
  • attributeValue - The value of the attribute (e.g. 123)
  • assignments - A dictionary of persisted experiment assignments. For example: {"exp1__0":"control"}

The attributeName/attributeValue combo is the primary key.

For the async client, implement an async sticky bucket service:

import asyncio
from typing import Optional, Dict
from growthbook import GrowthBookClient, Options, UserContext, AbstractAsyncStickyBucketService

class AsyncStickyBucketService(AbstractAsyncStickyBucketService):
"""Async sticky bucket service using database/Redis"""

def __init__(self):
# Initialize your async database connection
self.db = AsyncDatabase() # Your async DB client

async def get_assignments(self, attribute_name: str, attribute_value: str) -> Optional[Dict]:
"""Lookup a sticky bucket document"""
try:
doc = await self.db.find_one({
"attributeName": attribute_name,
"attributeValue": attribute_value
})
return doc
except Exception as e:
print(f"Error getting assignments: {e}")
return None

async def save_assignments(self, doc: Dict) -> None:
"""Save sticky bucket assignments"""
try:
await self.db.upsert(
{"attributeName": doc["attributeName"], "attributeValue": doc["attributeValue"]},
{"$set": {"assignments": doc["assignments"]}}
)
except Exception as e:
print(f"Error saving assignments: {e}")

async def main():
# Create sticky bucket service
sticky_service = AsyncStickyBucketService()

# Create client with sticky bucketing
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
sticky_bucket_service=sticky_service
)
)

try:
await client.initialize()

# User will get consistent experiment assignments
user = UserContext(attributes={"id": "user_123"})

# First time - gets assigned and saved
result1 = await client.get_feature_value("button-color", "blue", user)
print(f"First assignment: {result1}")

# Second time - retrieves saved assignment
result2 = await client.get_feature_value("button-color", "blue", user)
print(f"Consistent assignment: {result2}")

# Run inline experiments with sticky bucketing
from growthbook import Experiment

experiment = Experiment(
key="pricing-test",
variations=["$9.99", "$14.99", "$19.99"]
)

exp_result = await client.run(experiment, user)
print(f"Sticky experiment result: {exp_result.value}")

finally:
await client.close()

asyncio.run(main())

Redis Implementation Example

import asyncio
import json
from redis.asyncio import Redis
from growthbook import GrowthBookClient, Options, AbstractAsyncStickyBucketService

class RedisStickyBucketService(AbstractAsyncStickyBucketService):
def __init__(self):
self.redis = Redis(host='localhost', port=6379, decode_responses=True)
self.prefix = "gb:sticky:"

async def get_assignments(self, attribute_name: str, attribute_value: str) -> Optional[Dict]:
key = f"{self.prefix}{attribute_name}:{attribute_value}"
data = await self.redis.get(key)
if data:
return json.loads(data)
return None

async def save_assignments(self, doc: Dict) -> None:
key = f"{self.prefix}{doc['attributeName']}:{doc['attributeValue']}"
await self.redis.set(key, json.dumps(doc))

async def close(self):
await self.redis.close()

# Usage
sticky_service = RedisStickyBucketService()
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
sticky_bucket_service=sticky_service
)
)

Inline Experiments

Instead of declaring all features up-front and referencing them by ids in your code, you can also just run an experiment directly. This is done with the run method:

For the async client, use await with the run method:

import asyncio
from growthbook import GrowthBookClient, Options, UserContext, Experiment

async def main():
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
)
)

try:
await client.initialize()

# Create user context
user = UserContext(attributes={"id": "user_123", "country": "US"})

# Simple experiment
exp = Experiment(
key="my-experiment",
variations=["red", "blue", "green"]
)

# Run experiment (async)
result = await client.run(exp, user)
print(f"Variation: {result.value}") # Either "red", "blue", or "green"

# Complex experiment with all options
complex_exp = Experiment(
key="pricing-test",
variations=[9.99, 14.99, 19.99],
weights=[0.5, 0.3, 0.2], # 50%, 30%, 20%
coverage=0.8, # Include 80% of users
condition={"country": "US", "premium": True},
hashAttribute="id",
hashVersion=2,
namespace=("pricing", 0, 0.5)
)

pricing_result = await client.run(complex_exp, user)

if pricing_result.in_experiment:
print(f"User is in pricing experiment: ${pricing_result.value}")
print(f"Variation ID: {pricing_result.key}")
print(f"Hash used: {pricing_result.hash_used}")
else:
print("User not in pricing experiment")

finally:
await client.close()

asyncio.run(main())

As you can see, there are 2 required parameters for experiments, a string key, and an array of variations. Variations can be any data type, not just strings.

There are a number of additional settings to control the experiment behavior:

  • key (str) - The globally unique tracking key for the experiment
  • variations (any[]) - The different variations to choose between
  • seed (str) - Added to the user id when hashing to determine a variation. Defaults to the experiment key
  • weights (float[]) - How to weight traffic between variations. Must add to 1.
  • coverage (float) - What percent of users should be included in the experiment (between 0 and 1, inclusive)
  • condition (dict) - Targeting conditions
  • force (int) - All users included in the experiment will be forced into the specified variation index
  • hashAttribute (string) - What user attribute should be used to assign variations (defaults to "id")
  • hashVersion (int) - What version of our hashing algorithm to use. We recommend using the latest version 2.
  • namespace (tuple[str,float,float]) - Used to run mutually exclusive experiments.

Here's an example that uses all of them:

exp = Experiment(
key="my-test",
# Variations can be a list of any data type
variations=[0, 1],
# If this changes, it will re-randomize all users in the experiment
seed="abcdef123456",
# Run a 40/60 experiment instead of the default even split (50/50)
weights=[0.4, 0.6],
# Only include 20% of users in the experiment
coverage=0.2,
# Targeting condition using a MongoDB-like syntax
condition={
'country': 'US',
'browser': {
'$in': ['chrome', 'firefox']
}
},
# Use an alternate attribute for assigning variations (default is 'id')
hashAttribute="sessionId",
# Use the latest hashing algorithm
hashVersion=2,
# Includes the first 50% of users in the "pricing" namespace
# Another experiment with a non-overlapping range will be mutually exclusive (e.g. [0.5, 1])
namespace=("pricing", 0, 0.5),
)

Inline Experiment Return Value

A call to run returns a Result object with a few useful properties:

result = gb.run(exp)

# If user is part of the experiment
print(result.inExperiment) # True or False

# The string key of the assigned variation
print(result.key) # e.g. "0" or "1"

# The value of the assigned variation
print(result.value) # e.g. "A" or "B"

# If the variation was randomly assigned by hashing user attributes
print(result.hashUsed) # True or False

# The user attribute used to assign a variation
print(result.hashAttribute) # "id"

# The value of that attribute
print(result.hashValue) # e.g. "123"

The inExperiment flag will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions).

The hashUsed flag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false.

Example Experiments

3-way experiment with uneven variation weights:

result = await client.run(
Experiment(
key="3-way-uneven",
variations=["A", "B", "C"],
weights=[0.5, 0.25, 0.25]
),
user
)

Slow rollout (10% of users who match the targeting condition):

# Create user context with targeting attributes
user = UserContext(attributes={
"id": "123",
"beta": True,
"qa": True,
})

result = await client.run(
Experiment(
key="slow-rollout",
variations=["A", "B"],
coverage=0.1,
condition={'beta': True}
),
user
)

Complex variations:

result = await client.run(
Experiment(
key="complex-variations",
variations=[
("blue", "large"),
("green", "small")
],
),
user
)

# Either "blue,large" OR "green,small"
print(result.value[0] + "," + result.value[1])

Assign variations based on something other than user id:

# Create user context with company attribute
user = UserContext(attributes={
"id": "123",
"company": "growthbook"
})

# Users in the same company will always get the same variation
result = await client.run(
Experiment(
key="by-company-id",
variations=["A", "B"],
hashAttribute="company"
),
user
)

Working with Encrypted Features

The Python SDK supports encrypted feature flags for enhanced security. When encryption is enabled, the feature payload is encrypted before being sent from GrowthBook, and the SDK automatically decrypts it client-side.

For the async client, provide the decryption key in the Options:

import asyncio
from growthbook import GrowthBookClient, Options, UserContext

async def main():
# Create client with decryption key
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# Your decryption key from GrowthBook
decryption_key="your-secret-key-here"
)
)

try:
await client.initialize()

# Features are automatically decrypted
user = UserContext(attributes={"id": "user_123"})
feature_enabled = await client.is_on("encrypted-feature", user)

print(f"Encrypted feature enabled: {feature_enabled}")

finally:
await client.close()

asyncio.run(main())

Environment Variables

You can also set the decryption key via environment variable:

export GROWTHBOOK_DECRYPTION_KEY="your-secret-key-here"
import os
from growthbook import GrowthBookClient, Options

# Decryption key will be automatically picked up from environment
client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
# decryption_key will be read from GROWTHBOOK_DECRYPTION_KEY env var
)
)

Error Handling

If decryption fails (wrong key, corrupted data, etc.), the SDK will log an error and treat all features as disabled/default values:

import logging

# Enable logging to see decryption errors
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger('growthbook')

# This will log errors if decryption fails
gb = GrowthBook(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
decryption_key="wrong-key", # This will cause decryption to fail
attributes={"id": "user_123"}
)

Logging

The GrowthBook SDK uses a Python logger with the name growthbook and includes helpful info for debugging as well as warnings/errors if something is misconfigured.

Here's an example of logging to the console

import logging

logger = logging.getLogger('growthbook')
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

Integration Examples

This section provides practical examples for integrating GrowthBook with popular web frameworks.

Async Web Framework Integration (FastAPI)

The async client works great with modern async web frameworks like FastAPI:

from fastapi import FastAPI, Depends
from growthbook import GrowthBookClient, Options, UserContext

app = FastAPI()

# Create a single client instance (singleton)
gb_client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
)
)

@app.on_event("startup")
async def startup():
# Initialize the client when the app starts
await gb_client.initialize()

@app.on_event("shutdown")
async def shutdown():
# Clean up when the app shuts down
await gb_client.close()

@app.get("/")
async def root(user_id: str):
# Create user context for the request
user = UserContext(attributes={"id": user_id})

# Use features - no need to pass client around
show_new_ui = await gb_client.is_on("new-ui", user)
discount_amount = await gb_client.get_feature_value("discount-percent", 0, user)

return {
"new_ui": show_new_ui,
"discount": discount_amount
}

@app.get("/experiment")
async def run_experiment(user_id: str):
user = UserContext(attributes={"id": user_id})

# Run inline experiments
from growthbook import Experiment

experiment = Experiment(
key="button-color",
variations=["red", "blue", "green"]
)

result = await gb_client.run(experiment, user)

return {
"variation": result.value,
"in_experiment": result.in_experiment
}

Starlette Integration

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from growthbook import GrowthBookClient, Options, UserContext

# Create client instance
gb_client = GrowthBookClient(
Options(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123"
)
)

async def homepage(request):
user_id = request.query_params.get('user_id', 'anonymous')
user = UserContext(attributes={"id": user_id})

feature_enabled = await gb_client.is_on("new-homepage", user)

return JSONResponse({
"new_homepage": feature_enabled,
"user_id": user_id
})

async def startup():
await gb_client.initialize()

async def shutdown():
await gb_client.close()

app = Starlette(
routes=[
Route('/', homepage),
],
on_startup=[startup],
on_shutdown=[shutdown]
)

Traditional Web Frameworks (Django, Flask, etc.)

For new projects, we recommend using the Async Client instead for better performance.

For traditional synchronous web frameworks, you should create a new GrowthBook instance for every incoming request and call destroy() at the end of the request to clean up resources.

Django Integration

In Django, this is best done with a simple middleware:

from growthbook import GrowthBook

def growthbook_middleware(get_response):
def middleware(request):
request.gb = GrowthBook(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
attributes={
"id": getattr(request.user, 'id', 'anonymous'),
"loggedIn": request.user.is_authenticated,
"country": request.META.get('HTTP_CF_IPCOUNTRY', 'US')
}
)
request.gb.load_features()

response = get_response(request)

request.gb.destroy() # Cleanup

return response
return middleware

Then, you can easily use GrowthBook in any of your views:

def index(request):
feature_enabled = request.gb.is_on("my-feature")
button_color = request.gb.get_feature_value("button-color", "blue")

return render(request, 'index.html', {
'feature_enabled': feature_enabled,
'button_color': button_color
})

Flask Integration

from flask import Flask, g, request
from growthbook import GrowthBook

app = Flask(__name__)

@app.before_request
def before_request():
g.gb = GrowthBook(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
attributes={
"id": request.headers.get('User-ID', 'anonymous'),
"userAgent": request.user_agent.string,
"url": request.url
}
)
g.gb.load_features()

@app.teardown_request
def teardown_request(exception):
gb = getattr(g, 'gb', None)
if gb is not None:
gb.destroy()

@app.route('/')
def index():
feature_enabled = g.gb.is_on("new-layout")
theme = g.gb.get_feature_value("theme", "light")

return {
"feature_enabled": feature_enabled,
"theme": theme
}

Supported Features

FeaturesAll versions

ExperimentationAll versions

Saved Group References≥ v1.2.1

Prerequisites≥ v1.1.0

Sticky Bucketing≥ v1.1.0

SemVer Targeting≥ v1.1.0

v2 Hashing≥ v1.0.0

Encrypted Features≥ v1.0.0