Type Hints & Models

Every frozen dataclass result type, enum, Protocol, and error in the public surface

Complete reference for every frozen dataclass, enum, Protocol, and error type in the Python SDK's public surface. All symbols listed as top-level are importable directly from convert_sdk. Symbols listed as submodule require the full import path shown.

This is the Python analogue of the JavaScript SDK's ConfigTypes and the PHP SDK's ReturnTypes — same role, Python idioms.


Import cheat-sheet

# ---- top-level (convert_sdk) -------------------------------------------------
from convert_sdk import (
    Core, Context, __version__,

    # Config
    SDKConfig, TransportConfig, RefreshConfig,

    # Evaluation results
    ExperienceResult,
    FeatureResult, FeatureStatus,
    ConversionResult, ConversionStatus,
    CustomSegmentsResult,

    # Diagnostic surface
    DiagnosticReason,
    ExperienceDiagnostic, FeatureDiagnostic,
    GoalDiagnostic, EntityDiagnostic,

    # Lifecycle events
    LifecycleEvent,

    # Persistence boundary
    DataStore, InMemoryDataStore,

    # Error hierarchy
    ConvertSDKError, ConfigError,
    InvalidConfigError, ConfigLoadError,
    TransportError, TrackingDeliveryError,
)

# ---- submodule paths (stable, not re-exported at top level) ------------------
from convert_sdk.events import ConversionEventPayload, QueueReleasedPayload
from convert_sdk.errors import TrackingError, ConversionDataError
from convert_sdk.ports.transport import Transport
from convert_sdk.ports.event_bus import EventBus, EventHandler
from convert_sdk.ports.storage import DataStore, visitor_state_key
from convert_sdk.domain.context_state import ContextState
from convert_sdk.domain.config_snapshot import ConfigSnapshot

Evaluation result dataclasses

All result types are frozen dataclasses (@dataclass(frozen=True)). Fields cannot be reassigned after construction. Mapping fields (variation, variables) are wrapped in types.MappingProxyType so callers cannot mutate snapshot-owned config through the result.

ExperienceResult

Returned by Context.run_experience() and as elements of Context.run_experiences().

FieldTypeNotes
experience_keystrHuman-readable key from the dashboard
experience_idstrInternal config id
variation_idstrInternal variation id
variation_keystr | NoneHuman-readable variation key; None if the variation config has no key
variationMapping[str, Any]Read-only view of the selected variation's raw config

bucket_value is not a field on ExperienceResult. It is available only on the details mapping of an ExperienceDiagnostic returned by Context.diagnose_experience().

from convert_sdk import ExperienceResult
from typing import Optional, Mapping, Any

result: Optional[ExperienceResult] = context.run_experience("checkout-experiment")
if result is not None:
    print(result.experience_key)   # e.g. "checkout-experiment"
    print(result.variation_id)     # e.g. "10045"
    print(result.variation_key)    # e.g. "variation-1" (or None)
    title = result.variation.get("page_title")  # read-only mapping

FeatureResult

Returned by Context.run_feature() and as elements of Context.run_features().

FieldTypeNotes
feature_keystrHuman-readable feature key
feature_idstrInternal config id
statusFeatureStatusAlways ENABLED when a result is returned
variablesMapping[str, Any]Type-cast feature variables, read-only
experience_keystr | NoneBacking experience key, or None
variation_keystr | NoneBacking variation key, or None
from convert_sdk import FeatureResult, FeatureStatus
from typing import Optional

result: Optional[FeatureResult] = context.run_feature("checkout-banner")
if result is not None:
    assert result.status is FeatureStatus.ENABLED
    headline = result.variables.get("headline", "")

FeatureStatus

str enum — subclasses str so FeatureStatus.ENABLED == "enabled".

from convert_sdk import FeatureStatus

FeatureStatus.ENABLED   # "enabled"
FeatureStatus.DISABLED  # "disabled"

Normal misses (visitor not bucketed) are represented as None from run_feature() rather than a DISABLED result, consistent with the no-result convention for experiences. DISABLED is available for callers that explicitly model a declared-but-unbucketed feature.

ConversionResult

Returned by Context.track_conversion(). Always returned — never raised — for both success and the unknown-goal miss. Diagnose the outcome via .status:

FieldTypeNotes
statusConversionStatusQUEUED, DEDUPLICATED, or GOAL_NOT_FOUND
goal_keystrThe goal key passed by the caller (always echoed back)
goal_idstr | NoneResolved goal id; None on a miss
visitor_idstrThe visitor the tracking call was made for
eventConversionEvent | NoneIn-process conversion event; None on a miss
trackedbool (property)True only when status is QUEUED
reasonstr | None (property)None on success; "deduplicated" or "goal_not_found" otherwise
from convert_sdk import ConversionResult, ConversionStatus

result = context.track_conversion("purchase_completed", revenue=49.99)
if result.tracked:
    print("queued", result.goal_id)
elif result.status is ConversionStatus.GOAL_NOT_FOUND:
    print("goal not in config:", result.reason)  # "goal_not_found"
elif result.status is ConversionStatus.DEDUPLICATED:
    print("already tracked:", result.reason)     # "deduplicated"

ConversionEvent (the type of result.event) is an internal domain type in convert_sdk.domain.results; callers rarely need to import it directly.

ConversionStatus

str enum.

from convert_sdk import ConversionStatus

ConversionStatus.QUEUED          # "queued"     — event enqueued
ConversionStatus.DEDUPLICATED    # "deduplicated" — duplicate for (visitor, goal)
ConversionStatus.GOAL_NOT_FOUND  # "goal_not_found" — key absent from config

A missing goal key is not an exception. It surfaces as ConversionStatus.GOAL_NOT_FOUND so callers inspect result.status without a try/except. There is no GoalNotFoundError.

CustomSegmentsResult

Returned by Context.run_custom_segments().

FieldTypeNotes
matched_segment_idstuple[str, ...]Segment IDs newly matched by this call; empty tuple = normal no-match
matchedbool (property)True when at least one segment was matched
from convert_sdk import CustomSegmentsResult

seg = context.run_custom_segments(["high-value"], rule_data={"orders": 12})
if seg.matched:
    print("matched:", seg.matched_segment_ids)

Diagnostic dataclasses

Returned by Context.diagnose_*() methods. Never raise on a normal miss; instead they describe the outcome and its reason. All four are frozen dataclasses that share a common base with three fields:

FieldTypeNotes
reasonDiagnosticReasonClosed enum value — the machine-readable outcome code
messagestrShort human-readable explanation
detailsMapping[str, Any]Read-only, redaction-safe operational fields (e.g. visitor_ref, bucket_value)
resolvedbool (property)True when reason is DiagnosticReason.RESOLVED

The four diagnostic types

TypeReturned by
ExperienceDiagnosticContext.diagnose_experience(key)
FeatureDiagnosticContext.diagnose_feature(key)
GoalDiagnosticContext.diagnose_goal(goal_key)
EntityDiagnosticContext.diagnose_entity(entity_type, key)
from convert_sdk import (
    DiagnosticReason,
    ExperienceDiagnostic,
    FeatureDiagnostic,
    GoalDiagnostic,
    EntityDiagnostic,
)

exp_diag: ExperienceDiagnostic = context.diagnose_experience("checkout-experiment")
feat_diag: FeatureDiagnostic   = context.diagnose_feature("checkout-banner")
goal_diag: GoalDiagnostic      = context.diagnose_goal("purchase_completed")
ent_diag:  EntityDiagnostic    = context.diagnose_entity("experiences", "checkout-experiment")

# All share the same field access pattern:
print(exp_diag.reason)             # DiagnosticReason.RESOLVED (or a miss code)
print(exp_diag.resolved)           # bool shortcut
print(dict(exp_diag.details))      # redaction-safe operational fields

DiagnosticReason

Closed str enum — exactly eight members. The set is frozen; adding or renaming a member is a breaking change across all Convert SDKs.

from convert_sdk import DiagnosticReason

DiagnosticReason.RESOLVED                           # "resolved"
DiagnosticReason.AUDIENCE_MISMATCH                  # "audience_mismatch"
DiagnosticReason.EXPERIENCE_NOT_FOUND               # "experience_not_found"
DiagnosticReason.FEATURE_NOT_IN_SELECTED_VARIATIONS # "feature_not_in_selected_variations"
DiagnosticReason.FEATURE_NOT_FOUND                  # "feature_not_found"
DiagnosticReason.GOAL_NOT_FOUND                     # "goal_not_found"
DiagnosticReason.ENTITY_NOT_FOUND                   # "entity_not_found"
DiagnosticReason.PROJECT_MAPPING_REQUIRED           # "project_mapping_required"

Because DiagnosticReason subclasses str, you can compare against either the enum member or its string value:

assert diag.reason is DiagnosticReason.RESOLVED
assert diag.reason == "resolved"   # both work

Lifecycle event types

LifecycleEvent

from convert_sdk import LifecycleEvent

LifecycleEvent.READY                      # "ready"
LifecycleEvent.CONFIG_UPDATED             # "config.updated"
LifecycleEvent.BUCKETING                  # "bucketing"
LifecycleEvent.CONVERSION                 # "conversion"
LifecycleEvent.API_QUEUE_RELEASED         # "api.queue.released"
LifecycleEvent.DATA_STORE_QUEUE_RELEASED  # "datastore.queue.released"
LifecycleEvent.DIAGNOSTIC                 # "diagnostic"

LifecycleEvent is a plain enum.Enum (not a str enum); its .value attribute is the JS-parity wire string.

Lifecycle event payload dataclasses

There is no generic LifecycleEventPayload class. Each event carries its own typed frozen dataclass. Handlers receive (payload, error=None)error is a BaseException | None.

ConversionEventPayload

Carried on LifecycleEvent.CONVERSION. Import path: convert_sdk.events.ConversionEventPayload.

FieldType
visitor_idstr
goal_idstr
goal_keystr
from convert_sdk import LifecycleEvent
from convert_sdk.events import ConversionEventPayload

def on_conversion(payload: ConversionEventPayload, error=None) -> None:
    print(f"goal={payload.goal_key} visitor={payload.visitor_id}")

core.on(LifecycleEvent.CONVERSION, on_conversion)

QueueReleasedPayload

Carried on LifecycleEvent.API_QUEUE_RELEASED. Import path: convert_sdk.events.QueueReleasedPayload.

FieldTypeNotes
reasonReleaseReasonSIZE, EXPLICIT, TIMEOUT, or ATEXIT — see convert_sdk.tracking.queue.ReleaseReason
batch_sizeintNumber of events in the released batch
visitor_countintNumber of distinct visitors in the batch
event_countintTotal conversion events in the batch
status_codeint | NoneHTTP status on delivery failure; absent on success
retry_attemptsint | NoneTransport retry count, or None/0 if adapter does not retry
from convert_sdk import LifecycleEvent
from convert_sdk.events import QueueReleasedPayload

def on_released(payload: QueueReleasedPayload, error=None) -> None:
    if error is not None:
        print(f"delivery failed: status={payload.status_code}")
    else:
        print(f"released {payload.batch_size} events, reason={payload.reason}")

core.on(LifecycleEvent.API_QUEUE_RELEASED, on_released)

Extension Protocols

Three @runtime_checkable typing.Protocols define the SDK's swappable I/O seams. No base class is required — any object whose methods match structurally satisfies the protocol and passes isinstance(obj, Protocol).

See Extending for complete injection examples.

Transport

Import path: convert_sdk.ports.transport.Transport

from convert_sdk.ports.transport import Transport

class MyTransport:
    def fetch_config(self, config: "SDKConfig") -> dict[str, Any]: ...
    def send_tracking(self, payload: dict[str, Any], *, sdk_key: str) -> None: ...
    def close(self) -> None: ...
    def __enter__(self) -> "Transport": ...
    def __exit__(self, *exc: Any) -> None: ...
MethodSignatureDescription
fetch_config(config: SDKConfig) -> Dict[str, Any]Fetch raw config; raise ConfigLoadError on failure
send_tracking(payload: Dict[str, Any], *, sdk_key: str) -> NoneDeliver tracking batch; raise typed ConvertSDKError on failure
close() -> NoneRelease held resources (e.g. HTTP client pool)
__enter__ / __exit__context-manager pairRequired for isinstance(obj, Transport) to return True

Injected as a keyword-only argument on Core: Core(config, transport=my_transport).

DataStore

Import path: convert_sdk.ports.storage.DataStore (also exported from convert_sdk at top level)

from convert_sdk import DataStore  # top-level export

class MyStore:
    def get(self, key: str) -> Any: ...
    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: ...
    def has(self, key: str) -> bool: ...
    def delete(self, key: str) -> None: ...
MethodSignatureDescription
get(key: str) -> AnyReturn stored value or None if absent/expired
set(key: str, value: Any, ttl: Optional[int] = None) -> NoneStore value; ttl in seconds; None = no expiry
has(key: str) -> boolTrue if key has a present, unexpired value
delete(key: str) -> NoneRemove key; idempotent no-op on absent key

Injected as a field on SDKConfig: SDKConfig(data_store=my_store).

The built-in default is InMemoryDataStore (top-level export) — thread-safe, per-process, per-instance. See Persistent DataStore.

The helper visitor_state_key(visitor_id: str) -> str (from convert_sdk.ports.storage) builds the namespaced DataStore key for a visitor's persisted state.

EventBus

Import path: convert_sdk.ports.event_bus.EventBus

from convert_sdk.ports.event_bus import EventBus, EventHandler

# EventHandler type alias:
# EventHandler = Callable[..., None]
# Invoked as handler(payload, error=None)

class MyEventBus:
    def on(
        self, event: "LifecycleEvent", handler: "EventHandler"
    ) -> None: ...
    def emit(
        self,
        event: "LifecycleEvent",
        payload: Any,
        error: Optional[BaseException] = None,
    ) -> None: ...
MethodSignatureDescription
on(event: LifecycleEvent, handler: EventHandler) -> NoneRegister handler for event
emit(event: LifecycleEvent, payload: Any, error: Optional[BaseException] = None) -> NoneInvoke all handlers; raising handlers are isolated and swallowed

The SDK owns the bus — you do not inject it. Subscribe via Core.on(event, handler). See Event System and Extending.


Error hierarchy

ConvertSDKError                    (base; .code: str|None, .context: Mapping[str, Any])
├── ConfigError                    (base for config shape + loading failures)
│   ├── InvalidConfigError         (malformed config, missing or ambiguous init fields)
│   └── ConfigLoadError            (network/HTTP failure fetching config)
└── TransportError                 (non-HTTPS base_url or transport config failure)
    (TrackingDeliveryError is a direct subclass of ConvertSDKError, not TransportError)
TrackingDeliveryError              (tracking POST delivery failure)

# Not top-level exports — import from convert_sdk.errors:
TrackingError                      (programmer-misuse base for tracking enqueue)
└── ConversionDataError            (invalid conversion_data value at enqueue time)

Every ConvertSDKError carries two structured attributes:

AttributeTypeDescription
codestr | NoneStable, machine-readable failure-category string
contextMapping[str, Any]Immutable operational metadata (redaction-safe)

Known codes:

Classcode
ConfigLoadError"config_load_failed"
TrackingDeliveryError"tracking_delivery_failed"

ConfigLoadError additionally exposes .endpoint (redacted URL) and .status_code (HTTP status or None). TrackingDeliveryError additionally exposes .endpoint, .status_code, .batch_size, and .retry_count.

from convert_sdk import ConfigLoadError, TrackingDeliveryError

try:
    core = Core(SDKConfig(sdk_key="my-key")).initialize()
except ConfigLoadError as exc:
    print(exc.code)           # "config_load_failed"
    print(exc.endpoint)       # redacted URL — no SDK key in query string
    print(exc.status_code)    # int or None
    print(dict(exc.context))  # structured metadata mapping

# Tracking delivery failure (raised by core.flush() on a POST failure):
from convert_sdk import LifecycleEvent
from convert_sdk.errors import TrackingDeliveryError

try:
    core.flush()
except TrackingDeliveryError as exc:
    print(exc.code)         # "tracking_delivery_failed"
    print(exc.batch_size)   # int or None
    print(exc.retry_count)  # int or None

TrackingError and ConversionDataError are not top-level exports. Import them from convert_sdk.errors when catching programmer-misuse failures at tracking enqueue time:

from convert_sdk.errors import ConversionDataError

try:
    context.track_conversion("purchase", conversion_data={"items": [1, 2, 3]})
except ConversionDataError as exc:
    print(exc.key)     # the offending attribute name
    print(exc.reason)  # short safe reason string

Config dataclasses

SDKConfig

Top-level frozen dataclass. Exactly one of sdk_key or data must be supplied.

FieldTypeDefaultNotes
sdk_keystr | NoneNoneRemote-config mode — fetch over HTTPS
datadict | NoneNoneDirect-config mode — no network call
environmentstr | NoneNoneNon-default environment
cache_levelstr | NoneNoneNone or "low"
transportTransportConfigTransportConfig()HTTPS transport settings
batch_sizeint10Events per batch before auto-release
auto_flush_interval_msint | NoneNonePeriodic flush interval; None = explicit-only
data_storeDataStore | NoneNoneCustom persistence adapter; None = InMemoryDataStore
loggerlogging.Logger | NoneNoneCustom logger; None = logging.getLogger("convert_sdk")
refreshRefreshConfig | NoneNoneAuto-refresh policy for sdk_key mode

Property: is_direct_config -> boolTrue when data mode is active.

TransportConfig

Frozen dataclass. All fields have defaults.

FieldTypeDefaultNotes
base_urlstr"https://cdn-4.convertexperiments.com"Must be HTTPS — raises TransportError otherwise
timeoutfloat10.0Request timeout in seconds
auth_secretstr | NoneNoneBearer secret for authenticated keys
headersMapping[str, str]{}Extra headers on config requests
verify_tlsboolTrueWhether to verify TLS certificates

RefreshConfig

Frozen dataclass for the opt-in auto-refresh policy (Story 5.2). Only applies in sdk_key mode; ignored (with a diagnostic log) in data mode.

FieldTypeDefaultNotes
interval_secondsfloat300.0Base period between refresh attempts; must be > 0
jitter_secondsfloat30.0Max random jitter per interval; 0 <= jitter <= interval
backoff_factorfloat2.0Multiplier after consecutive failures; must be >= 1.0
backoff_max_secondsfloat600.0Backoff ceiling; must be >= interval_seconds

Internal types for custom adapter authors

These are not re-exported at the top level but are stable for direct import.

ContextState

convert_sdk.domain.context_state.ContextState — frozen dataclass holding per-visitor state. Fields: visitor_id: str, snapshot: ConfigSnapshot, visitor_attributes: Mapping[str, Any], default_segments: Mapping[str, Any]. Immutable rebind helpers: with_attributes(...), with_segments(...), with_overlay(...).

ConfigSnapshot

convert_sdk.domain.config_snapshot.ConfigSnapshot — frozen dataclass holding the loaded config with precomputed O(1) indexes. Fields include account_id, project_id, experiences, features, goals, audiences, segments. Read-only accessor methods: get_experience_by_key, get_feature_by_key, get_goal_by_key, etc.


What to read next

  • Configuration — field-by-field SDKConfig / TransportConfig / RefreshConfig reference
  • Diagnostics — reason codes and diagnose_* workflow
  • Extending — Protocol-based custom adapter injection
  • Code Examples — practical usage of every type