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 ConfigSnapshotEvaluation 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 mappingFeatureResult
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 configA 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 fieldsDiagnosticReason
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 workLifecycle 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 NoneTrackingError 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 stringConfig 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 -> bool — True 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/RefreshConfigreference - Diagnostics — reason codes and
diagnose_*workflow - Extending — Protocol-based custom adapter injection
- Code Examples — practical usage of every type