Python
Getting Started with the Python SDK
Add quonfig to your package dependencies
pip install quonfig
# pyproject.toml
[tool.poetry.dependencies]
quonfig = "^1.0.0"
Initialize Client
If you set QUONFIG_BACKEND_SDK_KEY as an environment variable, initializing the client is as easy as
from quonfig import Quonfig
client = Quonfig()
client.init()
You can also pass the SDK key explicitly:
from quonfig import Quonfig
client = Quonfig(sdk_key="sdk-...")
client.init()
init() returns the client, so client = Quonfig(sdk_key="sdk-...").init() works too.
Unless your options are configured to run using only local data, the client will attempt to connect to
the remote CDN.
Forking servers (Gunicorn --preload, Celery prefork, uWSGI, multiprocessing)
--preload, Celery prefork, uWSGI, multiprocessing)As of quonfig 1.4.0, no wiring is required on POSIX. Importing quonfig
installs a child-only os.register_at_fork handler. A client built in the
master is re-initialized in each forked worker on that worker's first use:
the worker fetches its own config and opens its own SSE stream, and the
master is never touched. A forked worker that never calls the SDK costs
nothing. Details, platform notes (gevent, uWSGI --enable-threads, macOS)
and the exact list of calls that trigger the rebuild are in the SDK README's
"Forking servers" section.
Two things worth knowing:
- The first evaluation in a forked worker pays one config fetch, under the
usual
init_timeout_ms/on_init_failurerules. To pay it before the first request instead, callinit()on the master-built client in the worker's post-fork hook (only under--preload; a client that was built in the worker is already fresh and must not beinit()ed twice):
# gunicorn.conf.py, with --preload
def post_fork(server, worker):
from myapp import client # the client built and init()ed in the master
client.init() # re-initializes this worker now instead of on first use
- Health accessors such as
connection_state()andready()are diagnostic-only: in a worker that has not evaluated anything yet they report the pre-start state (initializing,False) without starting threads or making a request.
Before 1.4.0 the SDK had no fork handling: a forked worker inherited dead
background threads, stopped receiving updates, and connection_state() kept
reporting connected. If you are on an older version, upgrade — or re-create
the client after the fork:
from quonfig import Quonfig
# gunicorn configuration hook
def post_worker_init(worker):
global client
client = Quonfig().init()
The same shape works with @uwsgidecorators.postfork. Re-creating the client
after the fork is also the right move under gevent with a monkey-patched
master, and on macOS development machines, where a forked child that makes
HTTPS requests can be killed by the platform regardless of the SDK.
Basic Usage
Defaults
Here we ask for the value of a config named max-jobs-per-second, and we specify
10 as a default value if no value is available.
client.get("max-jobs-per-second", default=10) # => 10
If no default is provided, the default behavior is to raise a QuonfigKeyNotFoundError.
# raises a `QuonfigKeyNotFoundError`
client.get("max-jobs-per-second")
Handling Undefined Configs
If you would prefer your application return None instead of raising an error,
you can set on_no_default="warn" (log a warning and return None) or
on_no_default="ignore" (silently return None) when constructing the client.
from quonfig import Quonfig
client = Quonfig(sdk_key="sdk-...", on_no_default="warn").init()
client.get("max-jobs-per-second") # => None
Getting Started
Now create a config named my-first-int-config in the Quonfig UI. Set a default
value to 50 and sync your change to the API.
Add a feature flag named my-first-feature-flag in the Quonfig UI. Add boolean
variants of true and false. Set the inactive variant to false, make the flag
active and add a rule of type ALWAYS_TRUE with the variant to serve as true.
Remember to sync your change to the API.
config_key = "my-first-int-config"
print(config_key, client.get(config_key))
ff_key = "my-first-feature-flag"
print(ff_key, client.is_feature_enabled(ff_key))
Run the code above and you should see:
my-first-int-config 50
my-first-feature-flag True
Congrats! You're ready to rock!
Feature Flags
Feature flags become more powerful when we give the flag evaluation rules more information to work with.
We do this by providing a context for the current user (and/or team, request, etc)
Context
Global Context
When initializing the client, you can set a global context that will be used for all evaluations. Use global context for information that doesn't change - for example, your application's key, availability zone, machine type, etc.
from quonfig import Quonfig
import os, platform
global_context = {
"application": {
"key": "my-python-app",
"environment": "production"
},
"host": {
"name": platform.node(),
"cpu_count": os.cpu_count()
}
}
client = Quonfig(sdk_key="sdk-...", global_context=global_context).init()
Global context is the least specific context and will be overridden by more specific context passed in at the time of evaluation.
Providing Context at Evaluation Time
You can provide context when evaluating individual flags or config values via the contexts= keyword argument:
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
result = client.is_feature_enabled("my-first-feature-flag", contexts=context)
Feature flags don't have to return just true or false. You can get other data types using get:
client.get("ff-with-string", default="default-string", contexts=context)
client.get("ff-with-int", default=5)
Bound context
To avoid having to pass a context explicitly to every call to get or is_feature_enabled, you can
bind a context once with with_context, which returns a client wrapper that applies that context to
every evaluation:
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
bound = client.with_context(context)
# these two calls are equivalent
result1 = bound.is_feature_enabled("my-first-feature-flag")
result2 = client.is_feature_enabled("my-first-feature-flag", contexts=context)
result1 == result2 #=> True
Scoped context
It is also possible to scope a context for a particular block of code using the scoped_context
context manager — calls inside the with block pick up the context automatically:
context = {
"user": {
"key": 123,
"subscription_level": "pro",
"email": "bob@example.com"
},
"team": {
"key": 432,
},
"device": {
"key": "abcdef",
"mobile": False
}
}
with client.scoped_context(context):
result1 = client.is_feature_enabled("my-first-feature-flag")
result2 = client.is_feature_enabled("my-first-feature-flag", contexts=context)
result1 == result2 #=> True
Dynamic Log Levels
Log levels in Quonfig are stored as a log_level config (e.g. log-level.my-app). The SDK consults that config on every log call, so changes made in Quonfig take effect live via SSE without restarting.
Concept
- One
log_levelconfig per app, keyed likelog-level.my-app. Value is one ofTRACE,DEBUG,INFO,WARN,ERROR,FATAL. - Tell the client which config to consult via the
logger_keyconstructor argument. client.should_log(logger_path=..., desired_level=...)pusheslogger_pathinto the evaluation context asquonfig-sdk-logging.key(verbatim — no normalization) so a single config can drive per-module rules.client.should_log(config_key=..., desired_level=...)is the primitive form — use it when you want to evaluate a specific config without the convenience layer.- Logger names flowing through
quonfig-sdk-logging.keyare auto-captured by example-context telemetry, so the dashboard can auto-suggest rule targets.
Install
pip install quonfig
Basic usage
from quonfig import Quonfig
client = Quonfig(
sdk_key="sdk-...",
logger_key="log-level.my-app",
).init()
if client.should_log(logger_path="my_app.auth", desired_level="DEBUG"):
# ...
pass
# Primitive form — no auto-injection
if client.should_log(config_key="log-level.my-app", desired_level="DEBUG"):
pass
Rule example
Create a log_level config with key log-level.my-app and target individual loggers via quonfig-sdk-logging.key:
# Default to INFO for every logger in this app
default: INFO
rules:
# Bump a module to DEBUG
- criteria:
quonfig-sdk-logging.key:
starts-with: "my_app.auth"
value: DEBUG
# Silence a chatty library
- criteria:
quonfig-sdk-logging.key:
starts-with: "urllib3"
value: ERROR
# Turn DEBUG on for one developer, everywhere
- criteria:
user.email: "developer@example.com"
value: DEBUG
Because the evaluator sees your full context — global context, per-request context, and quonfig-sdk-logging.key — you can combine logger rules with user, environment, or request context for targeted debugging.
Stdlib logging integration
QuonfigLoggerFilter is a logging.Filter you can attach to any logger or handler. It gates each record through client.should_log(logger_path=record.name, ...):
import logging
from quonfig import Quonfig, QuonfigLoggerFilter
client = Quonfig(sdk_key="sdk-...", logger_key="log-level.my-app").init()
root = logging.getLogger()
root.setLevel(logging.DEBUG) # let Quonfig do the filtering
root.addFilter(QuonfigLoggerFilter(client))
logging.getLogger("my_app.auth").debug("filtered by Quonfig")
The record's name flows into the context verbatim as quonfig-sdk-logging.key, so rules that starts-with: "my_app.auth" match what your app already logs.
structlog integration
structlog is an optional dependency. Install it separately (pip install structlog) and use QuonfigLoggerProcessor in your processor pipeline:
import structlog
from quonfig import Quonfig, QuonfigLoggerProcessor
client = Quonfig(sdk_key="sdk-...", logger_key="log-level.my-app").init()
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level, # must come before QuonfigLoggerProcessor
QuonfigLoggerProcessor(client),
structlog.dev.ConsoleRenderer(),
]
)
logger = structlog.get_logger("my_app.auth")
logger.debug("filtered by Quonfig")
QuonfigLoggerProcessor raises at construction time if structlog isn't installed. Place it after structlog.stdlib.add_log_level so the level name is populated before the processor checks it.
Reference
| Name | Example | Description |
|---|---|---|
logger_key (constructor) | Quonfig(sdk_key=..., logger_key="log-level.my-app") | The log_level config consulted by the should_log(logger_path=...) convenience. Required for the logger_path form. |
should_log(logger_path=..., desired_level=...) | client.should_log(logger_path="my_app.auth", desired_level="DEBUG") | Convenience. Uses logger_key + injects quonfig-sdk-logging.key = logger_path. |
should_log(config_key=..., desired_level=...) | client.should_log(config_key="log-level.my-app", desired_level="DEBUG") | Primitive. Evaluates the named config directly — no auto-injection. Useful for custom adapters. |
QuonfigLoggerFilter(client, logger_path=None) | root.addFilter(QuonfigLoggerFilter(client)) | Stdlib logging.Filter. Reads the record's name into quonfig-sdk-logging.key. |
QuonfigLoggerProcessor(client, logger_path=None) | structlog.configure(processors=[..., QuonfigLoggerProcessor(client)]) | structlog processor. Requires structlog to be installed. |
Telemetry
The SDK sends evaluation summaries and context data in the background so the dashboard can show
which flags are used and with what contexts. Opt out with collect_evaluation_summaries=False and
context_upload_mode="shapes_only" (no example contexts) or "none" (no context data); see
constructor parameters.
How the SDK delivers telemetry, and what it does when the endpoint is slow or down, is the same in
every SDK and is explained once on the Telemetry
page. The Python option names and defaults (quonfig 1.5.0+), all keyword arguments to
Quonfig(...):
| Option | Default |
|---|---|
telemetry_flush_interval_ms | 60_000 (60s) |
telemetry_timeout_ms | 15_000 (15s) |
telemetry_connect_timeout_ms | 5_000 (5s) |
telemetry_max_retained_batches | 5 |
telemetry_max_retained_bytes | 2_097_152 (2MB) |
telemetry_max_retained_age_ms | 300_000 (5 min) |
telemetry_max_evaluation_summaries | 10_000 |
telemetry_max_context_shape_fields | 10_000 |
telemetry_max_example_contexts | 10_000 |
None or a non-positive value means the default. telemetry_timeout_ms bounds each read of the
response (requests has no whole-request deadline), so a server that never answers is abandoned
after 15s. Telemetry logs through the stdlib logger quonfig.telemetry.
A client you never close still sends its last window at interpreter exit, from an atexit hook
(all clients in parallel, 5s at most in total). A forked child never re-sends its parent's window.
Changes in 1.5.0: the flush interval went from 30s to 60s, so telemetry reaches the dashboard up to
a minute after an evaluation; the in-place 3x retry is replaced by keeping the batch and resending
it later; the atexit final flush is new.
Testing
Point the client at a local data directory instead of the remote CDN with datadir. Datadir mode
requires an environment: pass environment= or set QUONFIG_ENVIRONMENT, otherwise init() raises
RuntimeError.
from quonfig import Quonfig
client = Quonfig(datadir="path/to/quonfig-data", environment="development").init()
client.get(...)
Reference
Available constructor parameters
All parameters are keyword arguments to Quonfig(...).
sdk_key- your quonfig.com SDK key. Falls back to theQUONFIG_BACKEND_SDK_KEYenvironment variable.api_urls- override the API endpoints your SDK key connects to (list of base URLs).datadir- path to a local data directory to load config from instead of the remote CDN.environment- which environment to evaluate (production,staging,development); falls back toQUONFIG_ENVIRONMENT. Applies only indatadirmode, where it is required. With an SDK key the environment comes from the key, so this setting is ignored and the SDK logs a warning.on_no_default- one of"error"(default),"warn", or"ignore". Controls behavior when a config has no value and no default is supplied: raiseQuonfigKeyNotFoundError, log a warning and returnNone, or silently returnNone.on_init_failure- one of"raise"(default),"return", or"return_zero_value". Controls what happens if the initial fetch fails or times out.init_timeout_ms- how long to wait for the first successful fetch (defaults to10000). With an SDK key,init()returns immediately and fetches in the background; the first getter call waits up to this long for that fetch, then applieson_init_failure. Indatadirmodeinit()loads synchronously and this setting does not apply.collect_evaluation_summaries- send aggregate data about config and feature flag evaluations (defaults toTrue).context_upload_mode- send context information to Quonfig. One of"none","shapes_only"(field names and types only), or"periodic_example"(types plus example contexts; the default).global_context- a global context to be used in all lookups. Use this for things like availability zone, machine type...fallback_poll_enabled/fallback_poll_interval_ms- poll the CDN when the SSE stream is unavailable (defaultsTrue/60000).enable_sse- keep a live SSE stream open for real-time updates (defaults toTrue). PassFalseon serverless hosts; withfallback_poll_enabled=Truepolling becomes the primary update channel, with bothFalsethe client only updates viarefresh()/update_if_staler_than(). Requiresquonfig >= 1.3.0; see Lambdas / Serverless.logger_key- thelog_levelconfig key consulted byshould_log(logger_path=...). No default — set it to enable thelogger_pathconvenience (defaults toNone).