Evaluate in your application
Python SDK
The Python SDK evaluates flags, variations and variables inside your Python application, from a datafile it holds in memory, so no decision waits on the network. Its command line runs your project's test specs through this SDK, and an OpenFeature provider is available.
Installation#
Upgrading an existing application? Follow the Python migration guide.
pip install "featurevisor>=4.0.0,<5"Initialization#
Initialize the SDK with Featurevisor datafile content:
from urllib.request import urlopenimport jsonfrom featurevisor import create_featurevisordatafile_url = "https://cdn.yoursite.com/featurevisor-all.json"with urlopen(datafile_url) as response: datafile_content = json.load(response)f = create_featurevisor({ "datafile": datafile_content,})try: print(f.is_enabled("checkout", {"userId": "123"}))finally: f.close()This example closes the instance after evaluation. In a long running application, keep it alive for subsequent evaluations and close it during shutdown. HTTP failures and invalid JSON propagate to the caller before initialization.
Public API#
The main runtime API is create_featurevisor():
from featurevisor import Featurevisor, create_featurevisorf: Featurevisor = create_featurevisor({ "datafile": datafile_content,})Most applications only need create_featurevisor and the Featurevisor instance type. Public extension and observability APIs include FeaturevisorModule, diagnostics, events, and the datafile dictionaries accepted by the factory.
Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to set_datafile, set_context, set_sticky_features, set_sticky_variables, add_module, remove_module, and close. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
Evaluation types#
Featurevisor evaluates four kinds of values:
- Flag (
bool): whether the feature is enabled or not - Variation (
str): the variation of the feature (if any) - Feature variable: a configuration value owned by a feature
- Global variable: a configuration value evaluated independently of a feature
Context#
Context is a plain dictionary of attribute values used during evaluation:
Conditions compare these attributes using the operators, including empty and notEmpty.
Date comparisons accept portable timestamp strings and aware datetime values with a usable UTC offset. Plain date and naive datetime values are not accepted. Offsets must stay within 14 hours and the UTC year within 0001 to 9999. Fractions are truncated to milliseconds after conversion to UTC.
context = { "userId": "123", "country": "nl",}Setting initial context#
Set context when you create the instance:
f = create_featurevisor({ "context": { "deviceId": "123", "country": "nl", },})Setting after initialization#
Merge more context later:
f.set_context({ "userId": "234",})Replacing existing context#
Or replace the existing context:
f.set_context( { "deviceId": "123", "userId": "234", "country": "nl", "browser": "chrome", }, True,)Manually passing context#
Pass additional context to one evaluation:
is_enabled = f.is_enabled("checkout", {"country": "nl"})variation = f.get_variation("checkout", {"country": "nl"})feature_variable = f.get_variable("checkout", "bgColor", {"country": "nl"})global_variable = f.get_variable("supportEmail", {"country": "nl"})Check if enabled#
if f.is_enabled("checkout"): passGetting variation#
variation = f.get_variation("checkout")if variation == "treatment": passGetting variables#
Getting feature variables#
bg_color = f.get_variable("checkout", "bgColor")Getting global variables#
Global variables use the same methods without a feature key:
support_email = f.get_variable("supportEmail", {"country": "nl"})evaluation = f.evaluate_variable("supportEmail", {"country": "nl"})Conditions, composition and overrides are applied for you. See Variables and Conditions for authoring guidance.
To see which overrides changed a value, inspect variableOverridePaths in the detailed evaluation:
evaluation = f.evaluate_variable("runtimePolicy", {"country": "nl"})value = evaluation.get("variableValue")matched_paths = evaluation.get("variableOverridePaths")Modules should return the same result for the same input during one evaluation. Pass values that change through context.
Conditions and composition#
Schema version "4" uses one condition language for segments, root gates, force entries, traffic and variable overrides. Leaves compare attributes, reference a segment by its literal key, check a feature's enabled state and optional variation, or compare an ordinary variable getter result. A variable condition can specify its feature owner and a compiled path such as ["items", 0, "enabled"].
Arrays imply AND. Groups use and, or and not. NOT negates the AND of its direct children. Empty expressions are false. Conditions preserve order and short circuiting. Missing paths remain distinct from explicit null.
Feature evaluation checks sticky state, force entries, the root gate, then traffic and variable overrides. A failed feature or global variable root gate reports conditions_unmet and records the normalized expression in evaluation.conditions. Variation and feature variable disabled reasons are unchanged.
A global variable can compose ordinary getter results:
datafile_content = { "schemaVersion": "4", "revision": "example", "segments": {}, "features": {}, "variables": { "policy": {"type": "object", "defaultValue": {"limit": 50}}, "screen": {"type": "object", "compose": [ {"key": "policy", "variable": "policy"}, ]}, "policyAlias": {"type": "object", "compose": "policy"}, },}f = create_featurevisor({"datafile": datafile_content})try: screen = f.get_variable("screen") policy = f.get_variable("policyAlias")finally: f.close()The compiled list creates a fresh outer dictionary with literal property names. Entries may read global variables, feature variables, enabled flags or variations. A string compose is an alias of one literal global variable key and returns its value directly. False, zero, empty strings, arrays and null remain ordinary source values. JSON is decoded exactly once, including alias chains.
Compositions have no defaultValue or useDefaultWhenDisabled. They may have root conditions and disabledValue. An unmet gate skips every source and override, returning the disabled value or null. Whole sticky state bypasses source reads. Otherwise all sources are read before the existing override program runs. Detailed evaluations report variable_composed, or variable_override_rule when an override matches.
Sources use effective context, modules and child sticky state without inheriting an outer explicit default. SDK mutations copy changed paths and never change source, sibling or sticky values. Treat supplied datafiles and returned dictionaries and lists as read only. Copy them before application mutations. Cycles and more than 256 dependency handoffs fail the entire evaluation, with no partial composition returned. Python's interpreter recursion limit can stop a very deep chain sooner. With the default recursion limit of 1000, a chain of about 120 feature handoffs fits. This depends on the host and the caller's own stack. Lint warns about chains deeper than 32, which fit comfortably. The logical bound counts active dependency frames rather than every entity name or terminal attribute predicate. The SDK preserves the host application's recursion setting. Exhaustion returns an error evaluation, and later independent evaluations remain usable.
Schema version "2" selectors are normalized eagerly before merging, without changing caller objects or their informational version metadata. Schema version "4" entities normalize lazily on their first read. Partial updates invalidate only incoming entities. Replacement clears all normalized entity caches. Concurrent evaluations and background datafile refreshes are supported. Diagnostic handlers may evaluate the same entity again.
Malformed stringified selector JSON fails that entire selector, even through NOT or a segment alias, while other entities remain usable. An error diagnostic with code conditions_parse_error is reported once per selector per incoming entity load, eagerly for schema version "2" and on first read for schema version "4". Its details contain exactly conditions, entityType and entityKey. Accepted data is stored before eager callbacks, and lazy results are cached before callbacks, so diagnostic handlers can safely reenter evaluation.
Type specific methods#
Typed convenience methods are also available:
f.get_variable_boolean(feature_key, variable_key, context={})f.get_variable_string(feature_key, variable_key, context={})f.get_variable_integer(feature_key, variable_key, context={})f.get_variable_number(feature_key, variable_key, context={})f.get_variable_array(feature_key, variable_key, context={})f.get_variable_object(feature_key, variable_key, context={})f.get_variable_json(feature_key, variable_key, context={})The same helpers evaluate global variables when the feature key is omitted:
f.get_variable_string("supportEmail", context={})f.get_variable_object("checkoutSettings", context={})Type specific methods do not coerce values. get_variable_integer() returns None for the string "1", and boolean getters return None for non-boolean values.
get_variable_number() reads a variable of type number as a float, whether it holds 3 or 4.5. Datafiles built before v4 call that type double, and it reads the same way. The v3 name get_variable_double() was removed in v4. Each method also has a camelCase name, such as getVariableNumber().
Getting evaluations#
features = f.get_feature_evaluations()variables = f.get_variable_evaluations()Sticky#
Initialize with sticky#
Pin feature evaluations with sticky values:
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides. Pass stickyFeatures or stickyVariables when a child needs its own state.
Treat supplied sticky maps and their nested values as read only after handoff. Use the instance or child sticky setters to change them. Mutating a supplied map in place is not supported.
f = create_featurevisor({ "stickyFeatures": { "checkout": { "enabled": True, "variation": "treatment", "variables": { "myVariableKey": "myVariableValue", }, }, }, "stickyVariables": { },})Set sticky afterwards#
Or update them later:
f.set_sticky_features({ "checkout": { "enabled": False, }})Setting datafile#
You may initialize the SDK without passing datafile, and set it later on. The SDK accepts either parsed JSON content or a JSON string:
f.set_datafile(datafile_content)f.set_datafile(json.dumps(datafile_content))set_datafile returns True when the datafile is accepted, including unchanged content. It returns False when the datafile is rejected or the instance is closed. Rejected datafiles leave the last accepted datafile in place. Diagnostics and events keep their existing behaviour. The setDatafile alias returns the same boolean.
Merging by default#
By default, set_datafile(datafile) merges incoming content into the SDK's current datafile:
- top-level metadata such as
schemaVersion,revision, andfeaturevisorVersioncomes from the incoming datafile segmentsare merged, with incoming entries overriding existing onesfeaturesare merged, with incoming entries overriding existing ones- global
variablesare merged, with incoming entries overriding existing ones
Call set_datafile with different datafiles to merge their features, segments, and global variables. Use this to load datafiles on demand.
Replacing#
To fully replace the stored datafile, pass replace=True:
f.set_datafile(datafile_content, replace=True)Loading datafiles on demand#
Merge datafiles for different targets into the same instance. Here, the two datafile values have already been loaded and validated by your application from the same published release:
f.set_datafile(products_datafile)f.set_datafile(checkout_datafile)Use immutable release URLs, check HTTP responses and keep shared dependencies consistent. See Loading datafiles on demand for the complete loading and refresh flow.
Updating datafile#
Each accepted datafile update emits a datafile_set event. Subscribe to react to updates.
Interval-based update#
Replace the complete datafile on each refresh so definitions removed upstream are removed from the SDK too. Use merging only when loading partial datafiles intentionally. Schedule the next attempt even if fetching or decoding fails.
import jsonimport loggingimport threadingfrom urllib.request import urlopendef update_datafile(): try: with urlopen(datafile_url, timeout=30) as response: f.set_datafile(json.load(response), replace=True) except Exception: logging.exception("Failed to update datafile") finally: threading.Timer(5 * 60, update_datafile).start()update_datafile()Diagnostics#
By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Levels#
Available diagnostic levels are fatal, error, warn, info, and debug.
Set the level during initialization or update it afterwards:
f = create_featurevisor({"logLevel": "debug"})f.set_log_level("info")Handler#
Use onDiagnostic to send structured diagnostics to your observability system:
f = create_featurevisor({ "logLevel": "info", "onDiagnostic": lambda diagnostic: print( diagnostic["level"], diagnostic["code"], diagnostic["message"], ),})Every diagnostic has level, code, message, and an object-shaped details dictionary. Optional module, moduleName, and originalError fields describe provenance. Evaluation metadata belongs in details.
Diagnostic handlers are isolated from SDK behaviour. An exception in a handler does not stop other handlers or evaluations.
Events#
Subscribe to SDK events to respond to context, sticky state and datafile changes.
datafile_set#
def handle_datafile_set(event): revision = event["revision"] previous_revision = event["previousRevision"] revision_changed = event["revisionChanged"] features = event["features"] replaced = event["replaced"]unsubscribe = f.on("datafile_set", handle_datafile_set)unsubscribe()The features list will contain keys of features that have either been added, updated, or removed compared to the previous datafile content.
context_set#
unsubscribe = f.on("context_set", lambda event: print(event["context"]))unsubscribe()sticky_features_set and sticky_variables_set#
unsubscribe = f.on("sticky_features_set", lambda event: print(event["features"]))unsubscribe()unsubscribe = f.on("sticky_variables_set", lambda event: print(event["variables"]))unsubscribe()error#
unsubscribe = f.on("error", lambda event: print(event["diagnostic"]["message"]))unsubscribe()The error event is emitted for diagnostics reported with level set to error.
Evaluation details#
Inspect how variations and variables are evaluated against a context:
# flagevaluation = f.evaluate_flag(feature_key, context={})# variationevaluation = f.evaluate_variation(feature_key, context={})# variableevaluation = f.evaluate_variable(feature_key, variable_key, context={})The returned object will always contain the following properties:
type: flag, variation or variablereason: why the value was evaluated
Feature evaluations also contain featureKey. Global variable evaluations contain variableKey.
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
bucketValue: the bucket value between 0 and 100,000ruleKey: the rule keyerror: the error objectenabled: if feature itself is enabled or notvariation: the variation objectvariationValue: the variation valuevariableKey: the variable keyvariableValue: the variable valuevariableSchema: the variable schemavariableOverrideIndex: index of matched variable override when applicablevariableOverrideKey: key of the matched variable override when availablevariableOverridePath: nested authoring path of the matched global variable overridevariableOverridePaths: all matched paths from an accumulating global variable overrideconditions: the normalized root expression when its gate prevents evaluationvariable: the global variable definition when available
Modules#
The before evaluation and after evaluation phases each use the module membership at the start of that phase. Where an evaluation uses bucketing, the bucket key and bucket value phases each use their own starting membership. Adding or removing a module during a phase does not change the iteration already in progress. The next phase uses the membership then installed. A module added before the after phase can receive after callbacks for evaluations already in progress, even if it did not receive their before callbacks. A module removed before the after phase receives no after callback.
Modules can intercept evaluation and participate in SDK lifecycle:
setupbucketKeybucketValuebeforeEvaluationafterEvaluationclose
Defining a module#
my_module = { "name": "my-module", "setup": lambda api: api["onDiagnostic"](lambda diagnostic: print(diagnostic)), "beforeEvaluation": lambda options: {**options, "context": {**options["context"], "country": "nl"}}, "bucketKey": lambda options: options["bucketKey"], "bucketValue": lambda options: options["bucketValue"], "afterEvaluation": lambda evaluation, options: evaluation, "close": lambda: None,}beforeEvaluation and afterEvaluation run for feature and global variable evaluations. Required feature and variable checks use the same module pipeline, while an accumulating override publishes only its final result to afterEvaluation.
For an internal prerequisite read, both callbacks receive options["dependencyOf"] identifying the immediate requester, for example {"type": "variable", "variableKey": "checkoutPolicy"}. Feature variable requesters also include featureKey. Ordinary application calls have no dependencyOf entry. Callbacks can inspect this metadata without adding it to application options.
The module API passed to setup exposes getRevision, onDiagnostic, and reportDiagnostic.
If setup raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports module_setup_error, and calls close when present.
Internal reads carry dependencyOf identifying the immediate requester and dependencyPurpose set to condition or composition. Direct application calls carry neither. Conditions use owned comparison snapshots. Composition follows ordinary getter ownership. Memoization separates these purposes and is scoped to one public call. Tracking modules should normally skip condition reads, but composition reads consume real values. Private request state is never exposed to callbacks.
Registering modules#
Modules can be registered at initialization or afterwards:
f = create_featurevisor({ "modules": [my_module],})remove_module = f.add_module(my_module)remove_module()f.remove_module("my-module")Child instance#
A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling close() removes both child-owned listeners and subscriptions delegated to the parent.
child = f.spawn({"country": "de"})child.is_enabled("checkout")child.evaluate_flag("checkout")child.evaluate_variation("checkout")child.evaluate_variable("checkout", "bgColor")child.evaluate_variable("supportEmail")Close#
f.close()OpenFeature#
The provider targets OpenFeature specification 0.8.0 through OpenFeature Python SDK 0.10.x, tested with 0.10.0. OpenFeature remains optional and is not installed or imported by the base Featurevisor package.
Installation#
pip install "featurevisor[openfeature]>=4.0.0,<5"If the extra is not installed, importing featurevisor.openfeature reports the installation command needed to enable it.
Provider setup#
from featurevisor.openfeature import FeaturevisorOpenFeatureProviderfrom openfeature import apifrom openfeature.evaluation_context import EvaluationContextprovider = FeaturevisorOpenFeatureProvider({"datafile": datafile_content})api.set_provider_and_wait(provider)client = api.get_client()enabled = client.get_boolean_value( "checkout", False, EvaluationContext(targeting_key="user-123", attributes={"country": "nl"}),)Call api.shutdown() during application shutdown. This closes a Featurevisor instance created by the provider and releases provider subscriptions.
Flag key mapping#
| OpenFeature key | Featurevisor evaluation |
|---|---|
checkout | Boolean flag for checkout |
checkout:variation | Variation value for checkout |
checkout:title | Variable title for checkout |
variable:supportEmail | Global variable supportEmail |
Boolean variables use the boolean resolver. Integer and double variables use their matching numeric resolvers. Arrays, objects, and JSON variables use the object resolver.
The first separator divides the feature key from the selector. Use key_separator and variation_key when project keys require a different grammar:
provider = FeaturevisorOpenFeatureProvider( {"datafile": datafile_content}, key_separator="/", variation_key="$variation", global_variable_prefix="$variable",)This makes checkout/$variation the variation key, checkout/title a feature variable key, and $variable/supportEmail a global variable key. The global prefix defaults to variable and cannot contain the separator.
Context mapping#
OpenFeature's targeting key maps to userId by default. Use targeting_key_field to map it to another Featurevisor context field:
provider = FeaturevisorOpenFeatureProvider( {"datafile": datafile_content}, targeting_key_field="accountId",)The provider distinguishes a missing targeting key from an explicitly empty one when called directly. The upstream OpenFeature Python client merges contexts with a truthy fallback, so it drops an empty targeting key before the provider receives it. To override an existing targeting field with an empty string through that client, set the configured field itself in attributes, such as {"userId": ""}. Clear any inherited nonempty targeting key at its source first, since it would overwrite that attribute again. A narrower empty key may not clear it.
OpenFeature context attributes are copied without mutating the incoming context. Nested arrays and mappings are preserved. Datetimes are normalized to UTC ISO strings, matching the JavaScript provider.
Resolution details#
The provider maps Featurevisor evaluation results to OpenFeature details:
| Featurevisor result | OpenFeature result |
|---|---|
| Forced, sticky, rule match or composed variable | TARGETING_MATCH |
| Traffic allocation | SPLIT |
| Unmet root conditions, disabled variation or disabled variable | DISABLED |
| No match or variable default | DEFAULT |
| Missing feature, variable, or variations | ERROR with FLAG_NOT_FOUND |
| Wrong resolver type | ERROR with TYPE_MISMATCH |
| Invalid datafile | ERROR with PARSE_ERROR |
| Evaluation failure | ERROR with GENERAL |
Errors return the default value supplied to OpenFeature. A malformed datafile uses the stable message Could not parse datafile. A later successful set_datafile call clears the parse error.
Resolution metadata can include featureKey, variableKey, featurevisorReason, revision, schemaVersion, ruleKey, bucketKey, bucketValue, forceIndex, variableOverrideIndex, and variableOverrideKey. The selected variation is exposed as the OpenFeature variant when available.
Tracking#
Tracking is a no-op unless on_track is configured:
def handle_track(name, context, details): print(name, context, details)provider = FeaturevisorOpenFeatureProvider( {"datafile": datafile_content}, on_track=handle_track,)Using an existing Featurevisor instance#
from featurevisor import create_featurevisorfeaturevisor = create_featurevisor({"datafile": datafile_content})provider = FeaturevisorOpenFeatureProvider(featurevisor=featurevisor)The caller owns an instance passed this way. Provider shutdown does not close it. Call featurevisor.close() when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, featurevisor takes precedence over the options dictionary.
An invalid initial datafile in an owned instance fails set_provider_and_wait with ParseError, independently of logLevel. A valid datafile update clears the error and emits readiness. Supplied diagnostic callbacks keep their normal filtering. Shutdown removes provider subscriptions and closes only an owned instance.
See the OpenFeature provider guide for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
CLI usage#
Install the package in your Python environment:
python -m pip install "featurevisor>=4.0.0,<5"The native test runner requires @featurevisor/cli 4.0.0 or later and Node.js 24 or later. Install the Node.js CLI in your Featurevisor project:
npm install --save-dev @featurevisor/cli@^4.0.0The native CLI provides test, benchmark and assess-distribution. Use the Node.js CLI for authoring commands such as init, lint and build. Native commands delegate project parsing, test listing, matrix expansion and datafile generation to npx featurevisor, then evaluate with this Python SDK. The SDK runtime has no Node.js dependency.
Options use kebab case, such as --project-directory-path. The camelCase forms from earlier releases still work, and --root-directory-path is an alias for --project-directory-path. Pass values as --name=value or --name value. Repeat --target to select several targets in order. Run <command> --help to list every option.
Duplicate targets are evaluated once, at their first position. --n and -n accept a positive integer.
Test#
python -m featurevisor test \ --project-directory-path=/path/to/featurevisor-project \ --only-failuresSee Testing for authoring specs and Checks for check syntax and operators.
Useful test options:
--key-pattern=<pattern>and--assertion-pattern=<pattern>filter tests.--entity-type=scenarioselects scenarios.--set=<set>selects one project set. Omitting--setruns all sets.--case=<id>reruns one expanded case. Copy the opaque ID or rerun command from a failure report.--json --prettyproduces an indented JSON report.--coveragereports observed rule and override selections.--uncoveredlists features and global variables without associated specs.--only-failures,--quiet,--verboseand--show-datafilecontrol diagnostic output.
Benchmark#
The examples below use a project without environments. Add --environment=<name> if your project configures environments. In a sets project, omit --set to run every set or pass --set=<name>. Repeat --target=<key> to evaluate specific target datafiles. The Node.js CLI validates these options and builds the datafiles. Python evaluates them. Replace checkout with a feature defined in your project.
python -m featurevisor benchmark \ --project-directory-path=/path/to/featurevisor-project \ --feature=checkout \ --context='{"country":"nl"}' \ --n=1000Use --variation for a variation or --variable=<key> for a feature variable. Omit --feature to benchmark a global variable. Do not combine --variation with --variable. See Benchmarking.
Assess distribution#
python -m featurevisor assess-distribution \ --project-directory-path=/path/to/featurevisor-project \ --feature=checkout \ --populate-uuid=userId \ --n=1000Repeat --populate-uuid=<key> to generate UUID values for multiple context attributes. Supply one key per option. See Assess distribution.
GitHub repositories#
- SDK: featurevisor/featurevisor-python
- Example application: featurevisor/featurevisor-example-python

