Convert Python SDK
The Convert Experiences FullStack SDK for Python — server-side A/B testing, feature flags, and personalizations for Python 3.9+ applications (Django, Flask, FastAPI, and plain Python services).
The SDK is framework-agnostic and sync-first: you can reach your first experiment value in plain Python, with no web framework and — using direct config — no network call.
Installation
pip install convert-python-sdk
- Distribution name (PyPI):
convert-python-sdk - Import package:
convert_sdk
The two differ by design — the hyphenated name is the discoverability surface on PyPI, the snake_case name is the ergonomic import path.
Compatibility
- Python 3.9+
- No required web framework
- No JavaScript runtime dependency
- One runtime dependency (
httpx), used only forsdk_keyinitialization
Quickstart
The fastest path to a first successful run uses direct config — a preloaded config payload, no network call:
from convert_sdk import Core, SDKConfig
config_data = {
"account_id": "100123",
"project": {"id": "200456"},
"experiences": [
{
"id": "e1",
"key": "checkout-experiment",
"variations": [
{"id": "v1", "key": "control", "traffic_allocation": 50.0},
{"id": "v2", "key": "treatment", "traffic_allocation": 50.0},
],
}
],
}
# Initialize from direct config — ready immediately, no network.
core = Core(SDKConfig(data=config_data)).initialize()
# Create a visitor-scoped context.
context = core.create_context("visitor-001")
# Evaluate an experience.
result = context.run_experience("checkout-experiment")
if result is not None:
print("Bucketed into:", result.variation_key)
core.close()
Initialization
Core is the entry point. Construct it with an SDKConfig, then call
initialize(). Provide exactly one of data (direct config) or sdk_key
(remote config).
Direct config (offline, no network)
from convert_sdk import Core, SDKConfig
core = Core(SDKConfig(data=config_data)).initialize()
assert core.is_ready
Direct-config initialization makes no network call and is ideal for local development, tests, and environments that load config out of band.
sdk_key (fetch config over HTTPS)
import os
from convert_sdk import Core, SDKConfig
# Read the key from the environment — never hard-code credentials.
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
sdk_key initialization fetches config over HTTPS through the built-in
transport. Inject the key from an environment variable or your secret store;
do not embed real keys in source.
Core is also a context manager, so it releases transport resources cleanly:
with Core(SDKConfig(data=config_data)).initialize() as core:
context = core.create_context("visitor-001")
...
Creating a visitor context
create_context binds a visitor identity (and optional visitor attributes) to
the current immutable config snapshot:
context = core.create_context(
"visitor-001",
visitor_attributes={"country": "US", "plan": "pro"},
)
Visitor attributes are used for audience qualification. They are copied
defensively — later mutations to the dict you pass never affect the context.
Keep and reuse the returned context to evaluate the same visitor repeatedly;
the SDK does not cache contexts for you.
Experience evaluation
run_experience evaluates a single experience for the visitor. It returns a
typed ExperienceResult when the visitor qualifies and buckets into a
variation, or None for any normal miss (missing experience, unqualified
visitor, no active variation). It never raises for normal outcomes and performs
no network I/O.
result = context.run_experience("checkout-experiment")
if result is not None:
print(result.experience_key, result.variation_key, result.variation_id)
# Evaluate all applicable experiences at once:
for result in context.run_experiences():
print(result.experience_key, "->", result.variation_key)
You can overlay request-time attributes for a single call without mutating the stored context:
result = context.run_experience(
"checkout-experiment",
attributes={"country": "DE"},
)
Feature evaluation
run_feature resolves a feature flag and its typed variables for the visitor.
It reads the feature change from the visitor's selected variation and casts each
variable using the feature's declared types. It returns a typed FeatureResult
when the feature is enabled for the visitor, or None for a normal miss
(undeclared, unavailable, or disabled feature). It never raises for normal
outcomes and performs no network I/O.
feature = context.run_feature("checkout-banner")
if feature is not None:
print(feature.status.value) # "enabled"
print(feature.variables["enabled"]) # typed per the feature definition (bool)
print(feature.variables["headline"]) # str
# Resolve all applicable features:
for feature in context.run_features():
print(feature.feature_key, feature.variables)
Conversion tracking
track_conversion records a goal conversion for the visitor. It is lightweight
and synchronous — it deduplicates by (visitor_id, goal_id) and appends to an
in-process batch queue. No network call happens on track_conversion;
queued events are delivered when the queue is released via core.flush(),
batch-size release (SDKConfig.batch_size, default 10), an opt-in periodic
timer, or a best-effort atexit hook.
result = context.track_conversion("purchase_completed", revenue=49.99)
print(result.tracked, result.reason) # True None
# A default duplicate for the same (visitor, goal) is suppressed:
again = context.track_conversion("purchase_completed")
print(again.tracked, again.reason) # False "deduplicated"
# force_multiple re-tracks (e.g. repeated revenue/transactions):
context.track_conversion("purchase_completed", revenue=10.0, force_multiple=True)
# Deliver queued events explicitly (the canonical control point):
core.flush()
Runtime Integration
Choosing when to flush depends on your runtime (Lambda, Cloud Run, gunicorn,
uvicorn, Celery, CLI). The default lifecycle is explicit-flush-only, which is
safe everywhere. See the project wiki
for per-runtime decision tables and copy-pasteable flush snippets, including the
opt-in daemonic periodic timer (SDKConfig.auto_flush_interval_ms), the
best-effort atexit hook, and the documented SIGTERM pattern.
Runnable examples
Self-contained, framework-agnostic examples live in examples/ and
run locally with no external services:
python examples/direct_config.py # direct-config initialization
python examples/basic_experience.py # bucket a visitor into a variation
python examples/basic_feature.py # resolve a feature and read typed variables
They share a small sample config (examples/_sample_config.py) and read any
sdk_key from the CONVERT_SDK_KEY environment variable rather than embedding
credentials.
Documentation
The advanced guides live on the project wiki:
- Topic guides: Initialization, Evaluation, Tracking, Queue control, Debugging, Extending, Support workflows, Runtime integration
- Migration guides: Migrating from raw REST, Migrating from the JavaScript SDK
Public API
Importable from convert_sdk:
Core,ContextSDKConfig,TransportConfigExperienceResult,FeatureResult,FeatureStatusConversionResult,ConversionStatus- error types:
ConvertSDKError,ConfigError,InvalidConfigError,ConfigLoadError,TransportError,TrackingDeliveryError __version__
Development
This project uses uv and the hatchling build
backend.
# Install dev tooling (pytest, ruff, mypy, coverage)
uv sync --group dev
# Run the test suite
uv run pytest
# Lint and type-check (the CI gates)
uv run ruff check src tests scripts
uv run mypy --strict
# Build wheel and sdist
uv build
Every change runs through CI (.github/workflows/ci.yml): Ruff lint, mypy
--strict, a 15-cell test matrix (Python 3.9–3.13 × {ubuntu, macos, windows})
with an 85% project / 95% evaluation/ coverage floor, a release-blocking parity
suite, and a dependency-bounds check.
Reproduce all the release gates locally in one command:
python scripts/verify_release.py
Releasing
The SDK publishes to PyPI as convert-python-sdk via a workflow_run-
triggered GitHub Actions workflow using OIDC Trusted Publishing — there are
no long-lived PyPI tokens in repository secrets.
Releases are fully automatic: merge a Conventional-Commit PR to main and the
pipeline handles the rest. See RELEASE.md for the full
maintainer workflow, one-time setup, dry-run instructions, and troubleshooting.
License
Apache-2.0
Release files for convert-python-sdk 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| convert_python_sdk-2.1.0.tar.gz | 111.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| convert_python_sdk-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 246.1 kB
Release files / convert_python_sdk-2.1.0.tar.gz
| Download URL | convert_python_sdk-2.1.0.tar.gz |
|---|---|
| Size | 111.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3caf07972bbef10f8580c1854a3654c5ce6558ab64cf30539ca66239ec7afaff
|
|
BLAKE2b-256 checksum How to use checksums |
3e2b8b44e0288b94c3553add6f1ca19379c747424689575fd82e3518354da8e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / convert_python_sdk-2.1.0-py3-none-any.whl
| Download URL | convert_python_sdk-2.1.0-py3-none-any.whl |
|---|---|
| Size | 134.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64c171cc2eae012e9f7b8ecec843514ca086ed08b2f740d66e1b9b78fb52ac4a
|
|
BLAKE2b-256 checksum How to use checksums |
40b850a08b3eb5625915f24a337c514a294dc7cdbc2e7413693c52a4da1f9f3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log