Python SDK Reference
TL;DR: Install
featureflip, callclient.variation("flag-key", {"user_id": "u-123"}, default=False), and you’re evaluating flags — synchronous, noasyncio.
The featureflip Python package provides a synchronous client for evaluating feature flags in any Python 3.10+ application — Django, Flask, FastAPI, Celery workers, scripts, or notebooks. The SDK is intentionally synchronous: variation calls do not return coroutines, so you can drop them into existing sync codebases without rewriting call sites or introducing asyncio to a non-async project.
Internally the client uses httpx for HTTP and SSE streaming, and stores flag configurations in a thread-safe in-memory cache. Evaluations read from this cache under a lock and return immediately, so they are safe to call from gunicorn workers, threaded WSGI servers, and Celery prefork pools alike. There is no GIL contention because the hot path is a dictionary lookup, not Python bytecode crunching JSON.
Evaluation context is a plain dict[str, Any] rather than a class — {"user_id": "u-123", "plan": "pro"} is the entire API. The user_id key is reserved: it anchors percentage rollouts so a given user lands in a stable bucket across processes and pods. Custom keys feed into targeting rules. String enum values from the API (rule operators, evaluation reasons) are normalized to lowercase on the client side, so you can compare them against Python literals without worrying about casing.
Installation
Section titled “Installation”pip install featureflipThe package depends on httpx, httpx-sse, and structlog.
Lifetime
Section titled “Lifetime”The Featureflip Python SDK is designed to be used as a singleton across your application. Construct the client once and share it:
# In your application startupfrom featureflip import FeatureflipClient
client = FeatureflipClient(sdk_key="your-sdk-key")Calling FeatureflipClient(sdk_key="x") multiple times with the same SDK key returns distinct handle objects that share a single underlying client — the SDK maintains a process-wide cache keyed by SDK key. You cannot accidentally open duplicate streaming connections by constructing multiple clients.
# Both of these return distinct handle objects that share one underlying clientc1 = FeatureflipClient(sdk_key="your-sdk-key")c2 = FeatureflipClient(sdk_key="your-sdk-key")assert c1 is not c2 # different handles# ...but only one HTTP connection, one polling thread, one event processor
c1.close() # decrements refcount; c2 is still alivec2.close() # last handle closed; real shutdown runsDifferent SDK keys get independent clients:
client_a = FeatureflipClient(sdk_key="sdk-key-a")client_b = FeatureflipClient(sdk_key="sdk-key-b")# Two independent clientsUse with Django, Flask, FastAPI
Section titled “Use with Django, Flask, FastAPI”Register the client as a module-level singleton at application startup. Any framework-specific startup / shutdown hook can own the construction and closing:
# django: in AppConfig.ready() or a module-level variable# flask: in create_app() or an extension init# fastapi: in a lifespan context manager
from featureflip import FeatureflipClient
_client: FeatureflipClient | None = None
def get_client() -> FeatureflipClient: global _client if _client is None: _client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"]) return _clientEven if your framework ends up constructing multiple clients (e.g., under autoreload or gunicorn’s pre-fork model), the singleton-by-construction cache ensures duplicate constructions are harmless.
Closing
Section titled “Closing”close() is refcounted. When multiple handles share one cached client, closing one handle does not shut down the shared background threads — the real shutdown runs only when the last handle is closed. In a typical application the client lives for the entire process lifetime and is cleaned up automatically at process exit or via a framework’s shutdown lifecycle.
As a context manager:
with FeatureflipClient(sdk_key="your-sdk-key") as client: value = client.variation("flag", {"user_id": "123"}, default=False)# client.close() is called automatically; decrements refcountConfiguration
Section titled “Configuration”The Config dataclass accepts the following options:
| Option | Type | Default | Description |
|---|---|---|---|
base_url | str | "https://eval.featureflip.io" | Base URL of the Evaluation API |
connect_timeout | float | 5.0 | Connection timeout in seconds |
read_timeout | float | 10.0 | Read timeout in seconds |
streaming | bool | True | Enable SSE for real-time flag updates |
poll_interval | float | 30.0 | Polling interval in seconds (used when streaming is disabled) |
send_events | bool | True | Enable analytics event tracking |
flush_interval | float | 30.0 | Interval in seconds between event flush batches |
flush_batch_size | int | 100 | Maximum number of events per flush batch |
init_timeout | float | 10.0 | Maximum time in seconds to wait for initialization |
Example configuration
Section titled “Example configuration”from featureflip import Config
config = Config( base_url="https://eval.featureflip.io", streaming=True, poll_interval=60.0, flush_interval=15.0, init_timeout=30.0,)All timeout and interval values must be positive. The constructor validates these constraints and raises ValueError for invalid values.
Initialization
Section titled “Initialization”Create a client by providing an SDK key and optional configuration:
from featureflip import FeatureflipClient, Config
client = FeatureflipClient( sdk_key="sdk-dev-abc123", config=Config(base_url="https://eval.featureflip.io"),)The constructor performs initialization synchronously: it fetches the initial flag configuration, starts streaming or polling, and begins the event processor. If initialization fails or times out, an InitializationError is raised.
SDK key resolution
Section titled “SDK key resolution”If sdk_key is not provided, the client falls back to the FEATUREFLIP_SDK_KEY environment variable. A ValueError is raised if neither is available.
import osos.environ["FEATUREFLIP_SDK_KEY"] = "sdk-dev-abc123"
# SDK key read from environmentclient = FeatureflipClient()Context manager
Section titled “Context manager”The client supports Python’s context manager protocol for automatic cleanup:
with FeatureflipClient(sdk_key="sdk-dev-abc123") as client: enabled = client.variation("new-feature", {"user_id": "123"}, default=False)# client.close() is called automaticallyis_initialized
Section titled “is_initialized”client.is_initialized: boolA read-only property that returns True once the client has successfully loaded flag data.
Evaluating Flags
Section titled “Evaluating Flags”The Python SDK uses a single variation() method for all flag types, rather than separate typed methods.
variation()
Section titled “variation()”enabled = client.variation("dark-mode", {"user_id": "u-123"}, default=False)plan = client.variation("pricing-tier", {"user_id": "u-123"}, default="free")limit = client.variation("rate-limit", {"user_id": "u-123"}, default=100)Signature:
def variation( self, key: str, context: dict[str, Any], default: T, track: bool = True,) -> T| Parameter | Type | Description |
|---|---|---|
key | str | The flag key to evaluate |
context | dict[str, Any] | User attributes for targeting. The user_id key is used for percentage rollouts |
default | T | Value returned if the flag is not found or evaluation fails |
track | bool | Whether to track this evaluation event (default True) |
This method never raises exceptions. On any error, it logs a warning and returns the default value.
Evaluation Details
Section titled “Evaluation Details”variation_detail()
Section titled “variation_detail()”Returns an EvaluationDetail object with the evaluated value and metadata about the evaluation decision.
detail = client.variation_detail("dark-mode", {"user_id": "u-123"}, default=False)
print(detail.value) # Trueprint(detail.reason) # EvaluationReason.RULE_MATCHprint(detail.rule_id) # "rule-abc"Signature:
def variation_detail( self, key: str, context: dict[str, Any], default: T,) -> EvaluationDetailEvaluationDetail
Section titled “EvaluationDetail”A frozen dataclass containing the evaluation result:
| Property | Type | Description |
|---|---|---|
value | Any | The evaluated flag value |
reason | EvaluationReason | Why this value was returned |
rule_id | str | None | The ID of the matched targeting rule, if applicable |
error | Exception | None | The exception that occurred, if reason is ERROR |
EvaluationReason
Section titled “EvaluationReason”An enum describing why a particular flag value was returned:
| Value | Description |
|---|---|
FALLTHROUGH | No rules matched; the fallthrough variation was served |
RULE_MATCH | A targeting rule matched the context |
FLAG_DISABLED | The flag is disabled; the off variation was served |
FLAG_NOT_FOUND | The flag key does not exist |
ERROR | An error occurred during evaluation |
Event Tracking
Section titled “Event Tracking”track()
Section titled “track()”Records a custom analytics event.
client.track("purchase-completed", {"user_id": "u-123"}, metadata={ "plan": "pro", "amount": 29.99,})Signature:
def track( self, event_name: str, context: dict[str, Any], metadata: dict[str, Any] | None = None,) -> Noneidentify()
Section titled “identify()”Sends user attributes for segment building and analytics.
client.identify({ "user_id": "u-123", "plan": "pro",})Signature:
def identify(self, context: dict[str, Any]) -> NoneCleanup
Section titled “Cleanup”close()
Section titled “close()”Shuts down the client, stops streaming or polling, flushes remaining events, and closes HTTP connections.
client.close()flush()
Section titled “flush()”Forces all buffered events to be sent to the server immediately.
client.flush()Using the context manager (with statement) is the recommended approach, as it calls close() automatically when the block exits.
Testing
Section titled “Testing”FeatureflipClient.for_testing()
Section titled “FeatureflipClient.for_testing()”Creates a test client with fixed flag values. No network calls are made and no background threads are started.
client = FeatureflipClient.for_testing({ "dark-mode": True, "pricing-tier": "enterprise", "rate-limit": 500,})
client.variation("dark-mode", {}, default=False) # Trueclient.variation("pricing-tier", {}, default="free") # "enterprise"Signature:
@classmethoddef for_testing(cls, flags: dict[str, Any]) -> FeatureflipClientset_test_value()
Section titled “set_test_value()”Updates a flag value on a test client at runtime. Raises RuntimeError if called on a non-test client.
client = FeatureflipClient.for_testing({"dark-mode": True})
# Change the value during the testclient.set_test_value("dark-mode", False)client.variation("dark-mode", {}, default=True) # FalseSignature:
def set_test_value(self, key: str, value: Any) -> NonePublic exports
Section titled “Public exports”All public types are available from the top-level featureflip package:
from featureflip import ( FeatureflipClient, Config, EvaluationContext, EvaluationDetail, EvaluationReason, InitializationError, ConfigurationError, FeatureflipError,)Exception hierarchy
Section titled “Exception hierarchy”| Exception | Description |
|---|---|
FeatureflipError | Base exception for all SDK errors |
InitializationError | Raised when initialization fails or times out |
ConfigurationError | Raised for invalid configuration values |
See also
Section titled “See also”- Python Quickstart — get started in under 5 minutes
- Targeting & Segments — control which users see which variation
- Rollout Strategies — gradual percentage-based rollouts
- Evaluation API — REST API reference
- SDK Overview — compare server-side and client-side SDKs