Migrating from the JavaScript SDK
Concept-by-concept map from the JavaScript SDK to the Pythonic Python SDK shapes
If you know the Convert FullStack JavaScript SDK, the Python SDK will feel conceptually familiar — same bucketing algorithm, same feature resolution logic, same queue-and-flush tracking model, and the same cross-SDK diagnostic reason codes. But it is not a syntax port. The Python SDK is deliberately Pythonic, and several shapes differ by design. This guide maps the concepts side by side, then calls out the differences so you do not fight the grain of the language.
Concept map
| JavaScript SDK | Python SDK | Notes |
|---|---|---|
new Core({ sdkKey, ... }) + await core.onReady() | Core(SDKConfig(sdk_key=...)).initialize() | Python init is synchronous; no async promise or onReady. |
core.createContext(visitorId, attrs) | core.create_context(visitor_id, visitor_attributes=attrs) | snake_case; named visitor_attributes= kwarg. |
context.runExperience(key, opts) | context.run_experience(key, attributes=...) | Returns typed frozen ExperienceResult or None. |
context.runExperiences(opts) | context.run_experiences() | Returns list[ExperienceResult]. |
runExperience(key, { enableTracking }) | run_experience(key, enable_tracking=...) | Per-call control over the bucketing/activation event; keyword-only, default True. JS's global network.tracking has no Python equivalent — Python's control is per-call only. |
context.runFeature(key, opts) | context.run_feature(key) | Returns typed FeatureResult | None; variables cast to declared types. |
context.runFeatures(opts) | context.run_features() | Returns list[FeatureResult]. |
context.trackConversion(key, data) | context.track_conversion(key, revenue=..., conversion_data=...) | Returns ConversionResult; unknown goal is never an exception. |
context.setDefaultSegments(segments) | context.set_segments(segments) | Persists default segments, kept strictly separate from set_attributes. |
context.runCustomSegments(keys, attrs) | context.run_custom_segments(keys, rule_data=...) | Returns typed CustomSegmentsResult. |
context.releaseQueues() | core.flush() | Flush is on Core, not Context; synchronous, no return value. |
core.on(event, handler) | core.on(LifecycleEvent.X, handler) | Typed enum, not bare strings. Handler signature: (payload, error=None). |
dataRefreshInterval: 300000 (default-on, ms) | SDKConfig(refresh=RefreshConfig(interval_seconds=300.0)) (opt-in, seconds) | Unit and default both differ — see below. |
'config.updated' string event | LifecycleEvent.CONFIG_UPDATED | Same wire string; Python fires only when the snapshot actually differs. |
Initialization
JavaScript:
import { Core } from '@convertcom/js-sdk';
const core = new Core({ sdkKey: process.env.CONVERT_SDK_KEY });
await core.onReady();Python:
import os
from convert_sdk import Core, SDKConfig
core = Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize()
assert core.is_ready # True immediately — init is synchronous and blockingCore.initialize() is synchronous. If the config fetch fails, ConfigLoadError is raised at initialize() time, not deferred. There is no on_ready() coroutine.
You can also use Core as a context manager, which calls close() on exit:
with Core(SDKConfig(sdk_key=os.environ["CONVERT_SDK_KEY"])).initialize() as core:
# core.is_ready is True here
...
# transport resources released on exitFor offline initialization from a preloaded config dict (no network call):
from convert_sdk import Core, SDKConfig
core = Core(SDKConfig(data=my_config_dict)).initialize()Context creation
JavaScript:
const context = core.createContext('visitor-abc123', {
browser: 'chrome',
country: 'US',
});Python:
context = core.create_context(
"visitor-abc123",
visitor_attributes={"browser": "chrome", "country": "US"},
)The Python Context is reusable across multiple evaluations. Update stored attributes with context.set_attributes({...}) and default segments with context.set_segments({...}) — these two states are kept strictly separate. For a one-off attribute overlay on a single call, pass attributes= directly to run_experience(...).
Running experiences
JavaScript:
const result = context.runExperience('checkout-flow');
if (result) {
console.log(result.variationKey, result.variationId);
}Python:
result = context.run_experience("checkout-flow")
if result is not None:
print(result.variation_key, result.variation_id)The Python result is a frozen dataclass (ExperienceResult) with typed named fields — not a plain dict. There is no bucket_value public field on the result; use context.diagnose_experience("checkout-flow") to inspect bucketing details. See Diagnostics.
Running features
JavaScript:
const feature = context.runFeature('checkout-banner');
if (feature) {
console.log(feature.status, feature.variables);
}Python:
from convert_sdk import FeatureStatus
feature = context.run_feature("checkout-banner")
if feature is not None:
print(feature.status.value) # "enabled" or "disabled"
print(dict(feature.variables)) # type-cast per feature definitionFeatureStatus is a string enum. Compare against the enum member (feature.status == FeatureStatus.ENABLED) rather than the raw string for type safety.
Tracking conversions
JavaScript:
context.trackConversion('purchase', {
goalData: [{ key: 'revenue', value: 49.99 }],
});Python:
from convert_sdk import ConversionStatus
result = context.track_conversion(
"purchase_completed",
revenue=49.99,
conversion_data={"products_count": 2},
)
# result is always a typed ConversionResult — never raises for a missing goal
if result.status is ConversionStatus.GOAL_NOT_FOUND:
print(result.reason)Key differences from JavaScript:
revenueis a named keyword argument, not embedded in agoalDatalist.conversion_datais a flatMapping[str, Any].- An unknown goal returns
ConversionResult(status=GOAL_NOT_FOUND)— it never raises. - Use
force_multiple=Trueto allow intentional repeat conversions for the same visitor and goal (overrides built-in deduplication).
Segments
JavaScript:
context.setDefaultSegments({ browser: 'CH', country: 'US' });
const matched = context.runCustomSegments(['segment-key-1'], {});Python:
context.set_segments({"loyalty_tier": "gold"})
seg = context.run_custom_segments(
["segment-premium-eu", "segment-mobile"],
rule_data={"device": "mobile"},
)
# seg: CustomSegmentsResult
# seg.matched is True when any segment key matchedset_segments and set_attributes are distinct methods for distinct state concerns. The stored default segments and stored visitor attributes are never merged.
Lifecycle events
JavaScript:
core.on('conversionCreated', (payload) => {
console.log(payload.goalId);
});Python:
from convert_sdk import LifecycleEvent
from convert_sdk.events import ConversionEventPayload
def on_conversion(payload: ConversionEventPayload, error=None) -> None:
print(payload.goal_key, payload.visitor_id)
core.on(LifecycleEvent.CONVERSION, on_conversion)Python uses a typed LifecycleEvent enum instead of bare strings. Each event type carries a distinct typed payload dataclass — there is no single generic payload type. Handlers always receive (payload, error=None). Available events:
LifecycleEvent member | Wire value | When fired |
|---|---|---|
READY | "ready" | After initialize() succeeds |
CONFIG_UPDATED | "config.updated" | When auto-refresh swaps to a new snapshot |
BUCKETING | "bucketing" | On each bucketing decision |
CONVERSION | "conversion" | When a conversion is enqueued |
API_QUEUE_RELEASED | "api.queue.released" | On every queue release (success or failure) |
DIAGNOSTIC | "diagnostic" | When a diagnose_* call produces a result |
Queue release
JavaScript:
await context.releaseQueues();Python:
core.flush() # synchronous; no return valueFlush is on Core, not Context. Python's flush() is synchronous. The queue also releases automatically when SDKConfig.batch_size (default 10) events accumulate, when an opt-in auto_flush_interval_ms timer fires, or at process exit via atexit. See Code Examples — Queue Control.
Background config refresh
The two SDKs handle background config refresh differently. A direct port without reading this section will run on stale config in long-lived processes.
| Concern | JavaScript | Python |
|---|---|---|
| Default behaviour | Always on | Off (SDKConfig.refresh=None) |
| Config field | dataRefreshInterval | SDKConfig(refresh=RefreshConfig(...)) |
| Units | milliseconds | seconds |
| Failure handling | Logs and stops rescheduling | Exponential backoff, keeps retrying |
| Update event | 'config.updated' after every successful fetch | CONFIG_UPDATED only when snapshot actually differs |
JavaScript:
const core = new Core({
sdkKey: process.env.CONVERT_SDK_KEY,
dataRefreshInterval: 300000, // 5 minutes, milliseconds
});
core.on('config.updated', () => myCache.invalidate());Python:
import os
from convert_sdk import Core, LifecycleEvent, RefreshConfig, SDKConfig
core = Core(
SDKConfig(
sdk_key=os.environ["CONVERT_SDK_KEY"],
refresh=RefreshConfig(interval_seconds=300.0), # 5 minutes, in SECONDS
)
).initialize()
core.on(LifecycleEvent.CONFIG_UPDATED, lambda payload, error=None: my_cache.invalidate())RefreshConfig fields: interval_seconds (default 300.0), jitter_seconds (default 30.0), backoff_factor (default 2.0), backoff_max_seconds (default 600.0). The Python SDK exposes this richer backoff policy that the JavaScript SDK does not have. See Initialization § automatic config refresh.
Deliberate Pythonic differences
| Concern | JavaScript | Python | Why |
|---|---|---|---|
| Naming convention | camelCase | snake_case | PEP 8 |
| Results | Plain object | Frozen dataclass with named fields | Immutability and type safety |
| Async | Promise / await | Synchronous (blocking) | SDK is sync-first; see Async and Framework Integrations |
| Config object | { sdkKey, environment, ... } | SDKConfig(sdk_key=..., environment=...) | Typed frozen dataclass, no Pydantic |
| Normal miss | Method may return null | Returns None | Same semantic, Pythonic spelling |
| Goal miss | May throw | ConversionResult(status=GOAL_NOT_FOUND) | Misses are typed results, not exceptions |
| Event payloads | Single generic payload | Distinct frozen dataclass per event type | Type safety |
| Extension | Subclassing / callbacks | typing.Protocol implementations | Structural typing; no base class needed |
| Diagnostics | Console debug mode | context.diagnose_*() + stdlib logging | Python stdlib conventions |
Behavioral equivalence
The bucketing algorithm is byte-identical between the two SDKs. For the same (visitor_id, experience_id) pair, both compute the same MurmurHash3-32 bucket value (seed 9999) and select the same variation. Both SDKs encode the input string to UTF-8 bytes before hashing — the JavaScript SDK via new TextEncoder().encode(), the Python SDK via str.encode("utf-8") — so hashing is byte-exact across all non-ASCII visitor IDs too. This equivalence is verified by the parity test suite at tests/parity/ in the Python SDK repository — those fixtures are the release-blocking cross-SDK contract.
The cross-SDK DiagnosticReason vocabulary is also shared, so a Python diagnostic reason correlates directly with a JavaScript one. See Diagnostics.
Future async / framework support
The Python SDK is sync-first today. An async public API (AsyncCore / AsyncContext) and framework-specific helpers (convert-sdk-django, convert-sdk-fastapi, convert-sdk-flask) are planned for Phase 3 and not yet implemented. The async surface, when it ships, will share the same evaluation core and the same Story 3.5 parity vectors as the sync API — the answers are the same, only the calling convention changes.
Until then, async Python code can call the sync SDK from a coroutine via asyncio.to_thread(). See Async and Framework Integrations for the design intent.
What to read next
- Initialization —
sdk_keyvsdata, context manager lifecycle - Code Examples — full
run_experience()/run_feature()/ tracking reference - Type Hints — the full typed result surface
- Diagnostics —
diagnose_experience()and the shared diagnostic vocabulary - Async and Framework Integrations — Phase 3 design intent