Skip to main content
The Foff Python SDK gives you a synchronous Client and an asyncio-compatible AsyncClient for resolving feature flag values. Both clients poll the Foff API in the background, cache configs in memory, and expose the same get_feature_config method. Use the sync client for standard Python applications and the async client when running an async framework such as FastAPI or asyncio.

Requirements

Python 3.10 or later.

Installation

pip install foff

Quick start

from foff import Client, Config

# STEP 1: Create appropriate config
config = Config(
    api_key="your-api-key",
    base_url="https://foff.twospoon.ai/live",
    scope="name-of-your-scope",
    polling_interval=30,  # refresh configs every 30 seconds (0 to disable)
)

# STEP 2: Create the client with the config
with Client(config) as client:
    # STEP 3: Retrieve configs for your created features for given hierarchies
    value = client.get_feature_config("my-feature", ["org-1", "team-a", "user-123"])
    print(value)
Store your API key in an environment variable and read it with os.environ["FOFF_API_KEY"]. Never hard-code it in source files.

Configuration

Configure the SDK using the foff.Config dataclass.
FieldTypeRequiredDefaultDescription
api_keystrYesYour Foff API key.
base_urlstrYesBase URL of the Foff API. Use https://foff.twospoon.ai/live.
scopestrYesThe scope to fetch configs for, such as production or staging.
polling_intervalintNo30How often, in seconds, to poll for config updates. Use 0 to disable polling.

Validation

Both Client and AsyncClient validate the config on construction. If api_key, base_url, or scope is empty, a ValueError is raised immediately before any network request is made.

Creating a client

Sync client

Use Client as a context manager. The with block handles startup and shutdown automatically.
with Client(config) as client:
    value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])
Client performs the following steps on entry:
1

Validate the config

Raises ValueError if api_key, base_url, or scope is empty.
2

Fetch all configs (blocking)

Makes a GET request to {base_url}/api/v1/scopes/{scope}/configs and populates the in-memory cache.
3

Start background polling

If polling_interval > 0, starts a background thread that re-fetches configs on the configured interval.

Async client

Use AsyncClient as an async context manager in any asyncio-based application.
async with AsyncClient(config) as client:
    value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])
AsyncClient follows the same steps as Client, but uses asyncio primitives for the background polling loop instead of a thread.

Resolving feature configs

get_feature_config(feature_name, ordered_hierarchy)

get_feature_config(feature_name: str, ordered_hierarchy: list[str]) -> Any
Returns the config value for a feature, resolved against a hierarchy. The call is synchronous (even on AsyncClient) and reads from the in-memory cache — no network request is made.
value = client.get_feature_config("dark-mode", ["org-1", "team-a", "user-123"])

Hierarchy resolution algorithm

The SDK resolves from most-specific to least-specific:
  1. It checks the full combination first — org-1 + team-a + user-123.
  2. It then tries shorter prefixes — org-1 + team-a, then org-1.
  3. If no override matches, it returns the feature’s "default" value.
  4. If the feature does not exist at all, it returns None.
This means you can define overrides at any level of your hierarchy — organisation, team, user, environment, region, service — and the SDK finds the most specific match automatically.

Polling

When polling_interval > 0, the client refreshes configs in the background on every interval.
  • Polling errors are silently ignored. The SDK continues serving the last successfully fetched config.
  • Calling close() on Client, or await close() on AsyncClient, stops background polling and releases resources.
Use caseInterval
Near-real-time10s
Standard30–60s
Low-traffic / batch300–600s

Client lifecycle

Use the context manager form wherever possible — it handles startup and shutdown automatically.
# Context manager (recommended)
with Client(config) as client:
    ...

# Manual
client = Client(config)
try:
    ...
finally:
    client.close()