> ## Documentation Index
> Fetch the complete documentation index at: https://docs.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Install the GrowthBook Python SDK to evaluate feature flags and run A/B tests in backends, scripts, and data applications.

*Requires **Python 3.9** or above*

python

Python SDK Resources

Get help on Slack

## Installation

```bash theme={null}
pip install growthbook
```

## Quick Usage

```python theme={null}
from growthbook import GrowthBook

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

# The parameter names matter: the SDK invokes this callback with keyword
# arguments (experiment=..., result=..., user_context=...)
def on_experiment_viewed(experiment, result, user_context):
  # 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")
```

## Async Client (Recommended)

**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

```python theme={null}
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](#integration-examples) below.

### Real-time Feature Updates

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

```python theme={null}
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:

```python theme={null}
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.

**Async Client**

For the async client, use `GrowthBookClient` with `Options`:

```python theme={null}
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())
```

**Legacy Client**

For the traditional client, pass `api_host` and `client_key` to the constructor:

```python theme={null}
from growthbook import GrowthBook

# Create client with built-in fetching and caching
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
  # How long to cache features in seconds (Optional, default 60s)
    cache_ttl=60,
    # Optional: Encryption support
    decryption_key="your-decryption-key",
    # User attributes
    attributes={"id": "user_123"}
)

# Load features (synchronous)
gb.load_features()

# Features are now cached and ready to use
feature_enabled = gb.is_on("my-feature")
print(f"Feature enabled: {feature_enabled}")

# Clean up
gb.destroy()
```

#### Custom Caching

GrowthBook comes with a custom in-memory cache.

**Legacy Client**

For the traditional client, configure a custom cache globally:

```python theme={null}
from redis import Redis
import json
from growthbook import GrowthBook, AbstractFeatureCache, feature_repo

class RedisFeatureCache(AbstractFeatureCache):
  def __init__(self):
    self.r = Redis(host='localhost', port=6379)
    self.prefix = "gb:"

  def get(self, key: str):
    data = self.r.get(self.prefix + key)
    # Data stored as a JSON string, parse into dict before returning
    return None if data is None else json.loads(data)

  def set(self, key: str, value: dict, ttl: int) -> None:
    self.r.set(self.prefix + key, json.dumps(value))
    self.r.expire(self.prefix + key, ttl)

# Configure GrowthBook to use your custom cache class
feature_repo.set_cache(RedisFeatureCache())

# Now all GrowthBook instances will use Redis caching
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123"
)
gb.load_features()  # Will use Redis cache
```

### 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:

```python theme={null}
# 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 called with keyword arguments `experiment`, `result`, and `user_context` whenever an experiment is run. The parameter names matter — the SDK invokes it with these exact keywords.
* **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:

```python theme={null}
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.

**Async Client**

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

```python theme={null}
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())
```

**Legacy Client**

For the traditional client, attributes are set globally on the instance:

```python theme={null}
from growthbook import GrowthBook

# Define user attributes
attributes = {
    'id': "123",
    'loggedIn': True,
    'age': 21.5,
    'tags': ["tag1", "tag2"],
    'account': {
        'age': 90
    },
    'country': 'US',
    'premium': True
}

# Pass into constructor
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes=attributes
)

gb.load_features()

# Use features (uses global attributes)
feature_enabled = gb.is_on("premium-feature")
discount = gb.get_feature_value("discount-percent", 0)

# Or update attributes later
new_attributes = {
    'id': "456",
    'loggedIn': False,
    'country': 'CA'
}
gb.set_attributes(new_attributes)

# Features will now use updated attributes
updated_feature = gb.is_on("premium-feature")

gb.destroy()
```

#### 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.

**Async Client**

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

```python theme={null}
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())
```

**Legacy Client**

For the traditional client, hash secure attributes before passing to the constructor:

```python theme={null}
import hashlib
from growthbook import GrowthBook

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

# Your secure attribute salt (set in Organization Settings)
salt = "f09jq3fij"

# 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 GrowthBook instance with hashed secure attributes
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={
    'id': "123",
    'loggedIn': True,
        'email': hashed_email,  # secureString
        'tags': hashed_tags,    # secureString[]
        'country': 'US'         # regular attribute
    }
)

gb.load_features()

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

gb.destroy()
```

### 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.

**Async Client**

For the async client, you can set up experiment tracking through the `Options`. Starting in version 2.4.0, the callback may be a coroutine. It is scheduled on the event loop without blocking evaluation, and a tracking callback that raises is retried on the next evaluation of the same experiment/user pair:

```python theme={null}
import asyncio
from growthbook import GrowthBookClient, Options, UserContext, Experiment, Result

async def on_experiment_viewed(experiment: Experiment, result: Result, user_context: UserContext):
    """Async callback for experiment tracking (runs in the background)"""
    # Use whatever event tracking system you want
    user_id = result.hashValue or "anonymous"

    await analytics.track_async(user_id, "Experiment Viewed", {
        'experimentId': experiment.key,
        'variationId': result.key,
        'variationValue': result.value,
        'inExperiment': result.inExperiment,
        'hashUsed': result.hashUsed
    })
    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:

```python theme={null}
def sync_experiment_tracker(experiment: Experiment, result: Result, user_context: UserContext):
    """Synchronous callback that works with async client"""
    # Send to synchronous analytics service
    analytics.track("Experiment Viewed", {
        'experimentId': experiment.key,
        'variationId': result.key,
        'userId': result.hashValue
    })

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

The same applies to `on_feature_usage` and callbacks registered via `client.subscribe()`. Both accept plain functions or coroutines starting in version 2.4.0. On earlier versions, callbacks must be synchronous; schedule async work manually with `asyncio.get_running_loop().create_task(...)`.

**Legacy Client**

For the traditional client, pass the callback to the constructor:

```python theme={null}
from growthbook import GrowthBook, Experiment, Result, UserContext

# The parameter names matter: the SDK invokes this callback with keyword
# arguments (experiment=..., result=..., user_context=...)
def on_experiment_viewed(experiment: Experiment, result: Result, user_context: UserContext):
    """Callback for experiment tracking"""
    # Use whatever event tracking system you want
    user_id = result.hashValue or "anonymous"

    analytics.track(user_id, "Experiment Viewed", {
        'experimentId': experiment.key,
        'variationId': result.key,
        'variationValue': result.value,
        'inExperiment': result.inExperiment,
        'hashUsed': result.hashUsed,
    })

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

# Create GrowthBook instance with experiment tracking
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={"id": "user_123", "premium": True},
    on_experiment_viewed=on_experiment_viewed
)

gb.load_features()

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

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

result = gb.run(experiment)
print(f"Experiment result: {result.value}")

gb.destroy()
```

### Tracking Plugins

**Available starting in version 2.2.0**

The Python SDK includes a built-in `GrowthBookTrackingPlugin` that batches "Experiment Viewed" and "Feature Evaluated" events and sends them to the GrowthBook ingestor. It runs alongside any `on_experiment_viewed` callback you already have.

The plugin needs the `requests` library, which is not installed with the SDK. Without it, creating the plugin raises an `ImportError`:

```bash theme={null}
pip install growthbook requests
```

> [!NOTE]
>
> Only the legacy `GrowthBook` class sends events today. Support for `GrowthBookClient` is coming soon.

#### Built-in Tracking Plugin

```python theme={null}
from growthbook import GrowthBook, GrowthBookTrackingPlugin

# Custom tracking callback for your analytics system
def my_custom_tracker(experiment, result, user_context):
    """Process a tracking event"""
    analytics.track(
        user_id=user_context.attributes.get('id'),
        event_name="Experiment Viewed",
        properties={
            'experiment_id': experiment.key,
            'variation_id': result.key,
            'variation_value': result.value,
        }
    )

# Create GrowthBook with tracking plugin
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={"id": "user_123"},
    plugins=[
        GrowthBookTrackingPlugin(
            # Use the region you selected when setting up your Managed Warehouse
            # (defaults to https://us-east-1.gb-ingest.com; https://eu-west-1.gb-ingest.com for EU)
            ingestor_host="https://us-east-1.gb-ingest.com",
            # Additional callback for custom analytics
            additional_callback=my_custom_tracker,
            # Set to False to stop sending "Feature Evaluated" events (optional, default True)
            track_feature_evaluated=True,
            # Number of events to accumulate before flushing (optional, default 10)
            batch_size=5,
            # Max seconds to wait before flushing a partial batch (optional, default 10.0)
            batch_timeout=10.0
        )
    ]
)

gb.load_features()

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

# Send a custom event through the same pipeline
gb.log_event("button_clicked", {"location": "header"})

# Flushes any pending events
gb.destroy()
```

Use one `GrowthBookTrackingPlugin` per `GrowthBook` instance; each one sends its own copy of every event to the ingestor. To forward events to more than one analytics tool, call them all from `additional_callback`.

#### Custom Events

`gb.log_event(event_name, properties)` sends a custom event to the ingestor with the current user's attributes. It's synchronous and only works when a `GrowthBookTrackingPlugin` is configured; otherwise it logs a warning and does nothing.

#### Tracking Plugin Benefits

The tracking plugin provides several advantages over custom tracking callbacks:

* **Automatic Batching**: Events are batched together to reduce API calls
* **Non-blocking**: Batches are sent on a background thread, so tracking doesn't block feature evaluations
* **Configurable**: Batch size and flush interval can be customized

#### Working with Traditional Callbacks

Tracking plugins work alongside your existing `on_experiment_viewed` callbacks:

```python theme={null}
def legacy_tracker(experiment, result, user_context):
    """Traditional tracking callback"""
    print(f"Legacy tracker: {experiment.key}")

gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    # Traditional callback still works
    on_experiment_viewed=legacy_tracker,
    # Plus the tracking plugin
    plugins=[
        GrowthBookTrackingPlugin(additional_callback=my_custom_tracker)
    ]
)

# Both the legacy callback and plugin will be triggered
```

#### LLM tracing tags

To run experiments on prompts or models and analyze them with Langfuse or Arize Phoenix data, tag each trace with the assignment in the form `gb.exp:<experimentKey>=<variationKey>`. The Python SDK does not have a tracing plugin yet, so build the tag in your `on_experiment_viewed` callback and pass it to your tracing SDK (Langfuse `propagate_attributes(tags=...)` or OpenInference `using_attributes(tags=...)`) at the top of the traced request. Skip experiment keys that contain `=`, since GrowthBook's exposure SQL splits the tag on the first `=`.

```python theme={null}
tags: list[str] = []

def on_experiment_viewed(experiment, result, user_context):
    if "=" in experiment.key:
        return
    tags.append(f"gb.exp:{experiment.key}={result.key}")
```

Keep the `tags` list scoped to a single request so one user's assignments do not leak onto another user's trace. See [Experimenting on LLM Features](https://docs.growthbook.io/integrations/llm-tracing.md) for the full setup, including matching the tracer's user id to the GrowthBook hash attribute.

## 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.eval_feature("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

## Type Safety

**Available starting in version 3.0.0**

The SDK ships inline type hints ([PEP 561](https://peps.python.org/pep-0561/) `py.typed`), so once type checking is enabled — `mypy your_app/`, `pyright your_app/`, or an IDE with checking on, like VS Code (Pylance) or PyCharm — your GrowthBook calls are checked with no SDK-specific configuration. This also gives coding agents a feedback loop: wrong feature usage comes back as a checker error instead of shipping.

All of this happens at check time only — the SDK performs no runtime validation, and code that skips the type checker behaves exactly as before.

Basic inference works out of the box — the return type follows the fallback you provide, and experiments infer their result type from the variations:

**Async Client**

```python theme={null}
# Inferred as `str` from the fallback ("blue")
color = await client.get_feature_value("button-color", "blue", user)

# result.value is inferred as `str` from the variations
result = await client.run(
    Experiment(key="my-test", variations=["blue", "green"]), user
)
```

**Legacy Client**

```python theme={null}
# Inferred as `str` from the fallback ("blue")
color = gb.get_feature_value("button-color", "blue")

# result.value is inferred as `str` from the variations
result = gb.run(Experiment(key="my-test", variations=["blue", "green"]))
```

Misuse is a checker error:

```python theme={null}
gb.get_feature_value("banner-text", "hello") + 1   # error: str + int
```

Misspelled keyword arguments to `Experiment` and `FeatureRule` are caught too:

```python theme={null}
Experiment(key="my-test", variations=["a", "b"], weigths=[0.5, 0.5])  # checker error: typo
```

### Typed Callbacks

The parameter names of a tracking callback are part of its contract — both clients invoke it with keyword arguments (`experiment=...`, `result=...`, `user_context=...`), so implementations must use these exact names. The SDK exports the contracts (`TrackingCallback`, `FeatureUsageCallback`, `EventLogger` for the legacy client; `AsyncTrackingCallback`, `AsyncFeatureUsageCallback`, `AsyncEventLogger` for the async client, whose callbacks may also be coroutines) and checkers verify your implementation against them:

```python theme={null}
def on_experiment_viewed(experiment, result, user_context):  # accepted
    ...

def wrong_names(exp, res, ctx):  # checker error when passed to the client
    ...
```

Passing an `async def` callback to the legacy synchronous client is also a checker error — only the async client schedules coroutines.

Legacy camelCase methods (`isOn`, `getFeatureValue`, `evalFeature`, ...) are marked with `@deprecated`, so editors strike them through and point at the snake\_case replacements.

### Strict Typing (Generated Feature Keys)

For compile-time checking of feature *keys* and per-key value types — the equivalent of the JS SDK's `GrowthBook<AppFeatures>` — generate a typed client from your features JSON. The generator ships inside the SDK (no separate CLI needed):

```bash theme={null}
python -m growthbook.codegen --input features.json --output growthbook_features.py
```

`features.json` is the SDK endpoint payload (e.g. `https://cdn.growthbook.io/api/features/<client_key>`) or a bare `{feature_key: definition}` map; pass `--format payload|map` to override auto-detection, and `--decryption-key <key>` if the endpoint has encryption enabled.

The generated subclasses add zero runtime behavior — all checking happens in your type checker and IDE:

**Async Client**

```python theme={null}
from growthbook_features import TypedGrowthBookClient

client = TypedGrowthBookClient(options)

await client.is_on("dark-mode", user)                     # OK
await client.is_on("dark-mod", user)                      # checker error: typo in feature key
await client.get_feature_value("max-items", "10", user)   # checker error: this feature is a number
```

**Legacy Client**

```python theme={null}
from growthbook_features import TypedGrowthBook

gb = TypedGrowthBook(api_host="...", client_key="...")

gb.is_on("dark-mode")                     # OK
gb.is_on("dark-mod")                      # checker error: typo in feature key
gb.get_feature_value("max-items", "10")   # checker error: this feature is a number
gb.get_feature_value("banner-text", None) # Optional[str]: None when the flag isn't set
gb.eval_feature("dark-mode").value        # Optional[bool] instead of Any
```

For dynamic keys (looping over feature names), the generated `FeatureKey` Literal is the escape hatch. Note that `cast` performs no runtime validation — it just silences the checker — so only cast values you know are real feature keys (enumerated from the generated module or your feature payload), never arbitrary input:

```python theme={null}
from typing import cast
from growthbook_features import FeatureKey

for k in keys_from_your_feature_payload:
    gb.is_on(cast(FeatureKey, k))
```

Commit the generated file. Its types describe the feature snapshot it was generated from, so they go stale when features change — regenerate in CI and fail the build if the committed file is out of date:

```bash theme={null}
python -m growthbook.codegen --input features.json --output growthbook_features.py
git diff --exit-code growthbook_features.py  # fails if the committed file is stale
mypy .                                       # or: pyright
```

Features whose value type can't be inferred (no `defaultValue` and no typed rules) fall back to `Any` and are listed in a warning at generation time.

## 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.

**Async Client**

**Async sticky bucket services available starting in version 2.4.0**

The async client supports network-backed sticky bucket stores (Redis, DynamoDB, etc.) without blocking the event loop. Subclass `AbstractAsyncStickyBucketService` and implement `async` versions of `get_assignments` and `save_assignments`. Optionally override `get_all_assignments` to batch all lookups for a user into a single round trip (e.g. one Redis `MGET`):

```python theme={null}
import json
from growthbook import AbstractAsyncStickyBucketService, GrowthBookClient, Options, UserContext

class RedisStickyBucketService(AbstractAsyncStickyBucketService):
    def __init__(self, redis):  # e.g. redis.asyncio.Redis
        self.redis = redis

    async def get_assignments(self, attributeName, attributeValue):
        raw = await self.redis.get(self.get_key(attributeName, attributeValue))
        return json.loads(raw) if raw else None

    async def save_assignments(self, doc):
        key = self.get_key(doc["attributeName"], doc["attributeValue"])
        await self.redis.set(key, json.dumps(doc))

    # Optional: batch all lookups for a user into a single MGET
    async def get_all_assignments(self, attributes):
        keys = [self.get_key(k, v) for k, v in attributes.items()]
        docs = {}
        for raw in await self.redis.mget(keys):
            if raw:
                doc = json.loads(raw)
                docs[self.get_key(doc["attributeName"], doc["attributeValue"])] = doc
        return docs

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

    try:
        await client.initialize()

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

        # ...
    finally:
        # close() waits for any in-flight assignment writes to persist
        await client.close()
```

Existing synchronous `AbstractStickyBucketService` implementations also work with the async client without changes. Their blocking calls are offloaded to a thread pool so the event loop is never blocked. They share the event loop's default thread pool, so for network-backed stores an async service is preferred.

A few behaviors worth understanding before choosing a store:

* **Reads happen per evaluation.** Assignments are fetched from your service for the supplied `UserContext` on each evaluation (matching the JavaScript SDK's multi-user client), so assignment changes made by other processes are picked up promptly. Concurrent evaluations for the same user share a single in-flight lookup. If your service lookups are expensive and your traffic concentrates on hot users, you can opt into a small bounded cache with `Options(sticky_bucket_cache_ttl=30, sticky_bucket_cache_size=1000)` (seconds / max users; disabled by default). The trade-off is that cross-process assignment changes may not be seen until the TTL expires.

* **Writes are fire-and-forget.** Evaluation never waits for persistence, and new assignments are immediately visible to later evaluations in the same process. The trade-off is a small durability window: if the process crashes before a background write completes, that assignment is lost. Long-running web services don't need to do anything; short-lived processes (serverless functions, scripts) should flush before exit:

  ```python theme={null}
  # Wait for all pending assignment writes to persist
  await client.flush_sticky_bucket_saves()
  ```

  `await client.close()` flushes automatically.

* **Async services must subclass `AbstractAsyncStickyBucketService`.** The client dispatches on the class, so a duck-typed object with `async` methods that doesn't inherit from it is treated as a synchronous service and will not work. The synchronous `GrowthBook` class accepts only synchronous services and raises `ValueError` if given an async one.

**Legacy Client**

For the traditional client, implement a synchronous sticky bucket service:

```python theme={null}
from typing import Optional, Dict
from growthbook import GrowthBook, AbstractStickyBucketService

class MyStickyBucketService(AbstractStickyBucketService):
    """Synchronous sticky bucket service using database"""

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

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

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

# Create GrowthBook with sticky bucketing
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={"id": "user_123"},
    sticky_bucket_service=MyStickyBucketService()
)

gb.load_features()

# User will get consistent experiment assignments
result1 = gb.get_feature_value("button-color", "blue")
print(f"First assignment: {result1}")

# Second time - retrieves saved assignment
result2 = gb.get_feature_value("button-color", "blue")
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 = gb.run(experiment)
print(f"Sticky experiment result: {exp_result.value}")

gb.destroy()
```

#### SQLite Implementation Example

```python theme={null}
import sqlite3
import json
from growthbook import GrowthBook, AbstractStickyBucketService

class SQLiteStickyBucketService(AbstractStickyBucketService):
    def __init__(self, db_path="sticky_buckets.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute('''
            CREATE TABLE IF NOT EXISTS sticky_buckets (
                attribute_name TEXT,
                attribute_value TEXT,
                assignments TEXT,
                PRIMARY KEY (attribute_name, attribute_value)
            )
        ''')
        self.conn.commit()

    def get_assignments(self, attribute_name: str, attribute_value: str) -> Optional[Dict]:
        cursor = self.conn.execute(
            "SELECT assignments FROM sticky_buckets WHERE attribute_name=? AND attribute_value=?",
            (attribute_name, attribute_value)
        )
        row = cursor.fetchone()
        if row:
            return {
                "attributeName": attribute_name,
                "attributeValue": attribute_value,
                "assignments": json.loads(row[0])
            }
        return None

    def save_assignments(self, doc: Dict) -> None:
        self.conn.execute(
            "INSERT OR REPLACE INTO sticky_buckets (attribute_name, attribute_value, assignments) VALUES (?, ?, ?)",
            (doc["attributeName"], doc["attributeValue"], json.dumps(doc["assignments"]))
        )
        self.conn.commit()

# Usage
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={"id": "user_123"},
    sticky_bucket_service=SQLiteStickyBucketService()
)
```

## 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:

**Async Client**

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

```python theme={null}
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.inExperiment:
            print(f"User is in pricing experiment: ${pricing_result.value}")
            print(f"Variation ID: {pricing_result.key}")
            print(f"Hash used: {pricing_result.hashUsed}")
        else:
            print("User not in pricing experiment")

    finally:
        await client.close()

asyncio.run(main())
```

**Legacy Client**

For the traditional client, call `run` directly:

```python theme={null}
from growthbook import GrowthBook, Experiment

# Create GrowthBook instance
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    attributes={"id": "user_123", "country": "US"}
)

gb.load_features()

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

# Run experiment
result = gb.run(exp)
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 = gb.run(complex_exp)

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

gb.destroy()
```

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:

```python theme={null}
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:

```python theme={null}
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

**Async Client**

3-way experiment with uneven variation weights:

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

**Legacy Client**

3-way experiment with uneven variation weights:

```python theme={null}
gb.run(Experiment(
    key="3-way-uneven",
    variations=["A", "B", "C"],
    weights=[0.5, 0.25, 0.25]
))
```

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

**Async Client**

```python theme={null}
# 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
)
```

**Legacy Client**

```python theme={null}
# User is marked as being in "qa" and "beta"
gb = GrowthBook(
    attributes={
    "id": "123",
    "beta": True,
    "qa": True,
  },
)

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

Complex variations:

**Async Client**

```python theme={null}
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])
```

**Legacy Client**

```python theme={null}
result = gb.run(Experiment(
    key="complex-variations",
    variations=[
    ("blue", "large"),
    ("green", "small")
  ],
))

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

Assign variations based on something other than user id:

**Async Client**

```python theme={null}
# 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
)
```

**Legacy Client**

```python theme={null}
gb = GrowthBook(
    attributes={
    "id": "123",
    "company": "growthbook"
  }
)

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

## 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.

If you use the [typed-client generator](#strict-typing-generated-feature-keys), pass the same key as `--decryption-key` so it can read the encrypted payload.

**Async Client**

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

```python theme={null}
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())
```

**Legacy Client**

For the traditional client, pass the decryption key to the constructor:

```python theme={null}
from growthbook import GrowthBook

# Create GrowthBook with decryption key
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    # Your decryption key from GrowthBook
    decryption_key="your-secret-key-here",
    attributes={"id": "user_123"}
)

gb.load_features()

# Features are automatically decrypted
feature_enabled = gb.is_on("encrypted-feature")
feature_value = gb.get_feature_value("encrypted-config", {"default": "value"})

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

gb.destroy()
```

### Environment Variables

You can also set the decryption key via environment variable:

```bash theme={null}
export GROWTHBOOK_DECRYPTION_KEY="your-secret-key-here"
```

**Async Client**

```python theme={null}
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=os.environ.get("GROWTHBOOK_DECRYPTION_KEY")
    )
)
```

**Legacy Client**

```python theme={null}
import os
from growthbook import GrowthBook

# Decryption key will be automatically picked up from environment
gb = GrowthBook(
    api_host="https://cdn.growthbook.io",
    client_key="sdk-abc123",
    # decryption_key will be read from GROWTHBOOK_DECRYPTION_KEY env var
    attributes={"id": "user_123"}
)
```

### Error Handling

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

```python theme={null}
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

```python theme={null}
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:

```python theme={null}
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.inExperiment
    }
```

### Starlette Integration

```python theme={null}
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](#async-client-recommended) 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:

```python theme={null}
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:

```python theme={null}
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

```python theme={null}
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
