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 SDKPython SDKNotes
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 eventLifecycleEvent.CONFIG_UPDATEDSame 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 blocking

Core.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 exit

For 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 definition

FeatureStatus 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:

  • revenue is a named keyword argument, not embedded in a goalData list.
  • conversion_data is a flat Mapping[str, Any].
  • An unknown goal returns ConversionResult(status=GOAL_NOT_FOUND) — it never raises.
  • Use force_multiple=True to 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 matched

set_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 memberWire valueWhen 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 value

Flush 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.

ConcernJavaScriptPython
Default behaviourAlways onOff (SDKConfig.refresh=None)
Config fielddataRefreshIntervalSDKConfig(refresh=RefreshConfig(...))
Unitsmillisecondsseconds
Failure handlingLogs and stops reschedulingExponential backoff, keeps retrying
Update event'config.updated' after every successful fetchCONFIG_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

ConcernJavaScriptPythonWhy
Naming conventioncamelCasesnake_casePEP 8
ResultsPlain objectFrozen dataclass with named fieldsImmutability and type safety
AsyncPromise / awaitSynchronous (blocking)SDK is sync-first; see Async and Framework Integrations
Config object{ sdkKey, environment, ... }SDKConfig(sdk_key=..., environment=...)Typed frozen dataclass, no Pydantic
Normal missMethod may return nullReturns NoneSame semantic, Pythonic spelling
Goal missMay throwConversionResult(status=GOAL_NOT_FOUND)Misses are typed results, not exceptions
Event payloadsSingle generic payloadDistinct frozen dataclass per event typeType safety
ExtensionSubclassing / callbackstyping.Protocol implementationsStructural typing; no base class needed
DiagnosticsConsole debug modecontext.diagnose_*() + stdlib loggingPython 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