Skip to content

TypeHints

Usman Abbas edited this page Jun 8, 2026 · 3 revisions

Type Hints

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().

Field Type Notes
experience_key str Human-readable key from the dashboard
experience_id str Internal config id
variation_id str Internal variation id
variation_key str | None Human-readable variation key; None if the variation config has no key
variation Mapping[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().

Field Type Notes
feature_key str Human-readable feature key
feature_id str Internal config id
status FeatureStatus Always ENABLED when a result is returned
variables Mapping[str, Any] Type-cast feature variables, read-only
experience_key str | None Backing experience key, or None
variation_key str | None Backing 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:

Field Type Notes
status ConversionStatus QUEUED, DEDUPLICATED, or GOAL_NOT_FOUND
goal_key str The goal key passed by the caller (always echoed back)
goal_id str | None Resolved goal id; None on a miss
visitor_id str The visitor the tracking call was made for
event ConversionEvent | None In-process conversion event; None on a miss
tracked bool (property) True only when status is QUEUED
reason str | 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().

Field Type Notes
matched_segment_ids tuple[str, ...] Segment IDs newly matched by this call; empty tuple = normal no-match
matched bool (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:

Field Type Notes
reason DiagnosticReason Closed enum value — the machine-readable outcome code
message str Short human-readable explanation
details Mapping[str, Any] Read-only, redaction-safe operational fields (e.g. visitor_ref, bucket_value)
resolved bool (property) True when reason is DiagnosticReason.RESOLVED

The four diagnostic types

Type Returned by
ExperienceDiagnostic Context.diagnose_experience(key)
FeatureDiagnostic Context.diagnose_feature(key)
GoalDiagnostic Context.diagnose_goal(goal_key)
EntityDiagnostic Context.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.

Field Type
visitor_id str
goal_id str
goal_key str
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.

Field Type Notes
reason ReleaseReason SIZE, EXPLICIT, TIMEOUT, or ATEXIT — see convert_sdk.tracking.queue.ReleaseReason
batch_size int Number of events in the released batch
visitor_count int Number of distinct visitors in the batch
event_count int Total conversion events in the batch
status_code int | None HTTP status on delivery failure; absent on success
retry_attempts int | None Transport 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: ...
Method Signature Description
fetch_config (config: SDKConfig) -> Dict[str, Any] Fetch raw config; raise ConfigLoadError on failure
send_tracking (payload: Dict[str, Any], *, sdk_key: str) -> None Deliver tracking batch; raise typed ConvertSDKError on failure
close () -> None Release held resources (e.g. HTTP client pool)
__enter__ / __exit__ context-manager pair Required 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: ...
Method Signature Description
get (key: str) -> Any Return stored value or None if absent/expired
set (key: str, value: Any, ttl: Optional[int] = None) -> None Store value; ttl in seconds; None = no expiry
has (key: str) -> bool True if key has a present, unexpired value
delete (key: str) -> None Remove 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: ...
Method Signature Description
on (event: LifecycleEvent, handler: EventHandler) -> None Register handler for event
emit (event: LifecycleEvent, payload: Any, error: Optional[BaseException] = None) -> None Invoke 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:

Attribute Type Description
code str | None Stable, machine-readable failure-category string
context Mapping[str, Any] Immutable operational metadata (redaction-safe)

Known codes:

Class code
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.

Field Type Default Notes
sdk_key str | None None Remote-config mode — fetch over HTTPS
data dict | None None Direct-config mode — no network call
environment str | None None Non-default environment
cache_level str | None None None or "low"
transport TransportConfig TransportConfig() HTTPS transport settings
batch_size int 10 Events per batch before auto-release
auto_flush_interval_ms int | None None Periodic flush interval; None = explicit-only
data_store DataStore | None None Custom persistence adapter; None = InMemoryDataStore
logger logging.Logger | None None Custom logger; None = logging.getLogger("convert_sdk")
refresh RefreshConfig | None None Auto-refresh policy for sdk_key mode

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

TransportConfig

Frozen dataclass. All fields have defaults.

Field Type Default Notes
base_url str "https://cdn-4.convertexperiments.com" Must be HTTPS — raises TransportError otherwise
timeout float 10.0 Request timeout in seconds
auth_secret str | None None Bearer secret for authenticated keys
headers Mapping[str, str] {} Extra headers on config requests
verify_tls bool True Whether 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.

Field Type Default Notes
interval_seconds float 300.0 Base period between refresh attempts; must be > 0
jitter_seconds float 30.0 Max random jitter per interval; 0 <= jitter <= interval
backoff_factor float 2.0 Multiplier after consecutive failures; must be >= 1.0
backoff_max_seconds float 600.0 Backoff 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

Clone this wiki locally