Skip to content

Python SDK

The Python SDK is a headless server library: it decides entitlements and placements in-process, with no network call and no persistence beyond memory. The server holds the user context and a Playbook snapshot and passes both to the SDK; the SDK decides locally.

It is intentionally a small surface — the same decision core as the TypeScript SDK’s headless LocalRuntime, exposed as a clean public class. It is not a port of the browser SDK (no React, no hooks, no decision cache — see Non-goals).

The revturbine package (requires Python ≥ 3.10) is published to PyPI. Install it in a virtual environment:

Terminal window
python3 -m venv .venv
.venv/bin/python -m pip install revturbine

Use that environment’s Python to run your app too. A virtual environment also handles PEP 668’s “externally managed environment” restriction without modifying system Python.

Pin to a version whose schema matches the Playbook you deploy — see Refreshing the Playbook:

Terminal window
python -m pip install "revturbine==<version>"

Here python means your virtual environment’s interpreter, as in the commands above.

Construct one SDK per (user_context, exported_config), then check entitlements and decide placements:

Python
import json
from revturbine import RevTurbineCustomerSdk, UserContext
with open("revturbine.playbook.json") as f:
exported_config = json.load(f)
user_context: UserContext = {
"tenant_id": "tenant_abc",
"user_id": "user_123",
# Optional — the user's current plan and usage:
"plan_handle": "pro",
"usage": {"api_calls": {"used": 900.0, "limit": 1000.0}},
}
sdk = RevTurbineCustomerSdk(
user_context=user_context,
exported_config=exported_config,
)
# Entitlement gate
result = sdk.can("advanced_analytics")
if not result["allowed"]:
raise PermissionError(result["reason"])
# Placement decision (which payload, if any, to show)
decision = sdk.get_placement_decision(
{"placement_id": "pl_dashboard_upsell", "user_id": "user_123"}
)
if decision["visible"]:
render(decision["output"])
# Batch form — same decision path, order-preserving
decisions = sdk.get_placement_decisions(
[
{"placement_id": "pl_a", "user_id": "user_123"},
{"placement_id": "pl_b", "user_id": "user_123"},
]
)
KeyRequiredMeaning
tenant_id✅Tenant identifier.
user_id✅Current user identifier.
plan_handle—The user’s current plan handle (feeds the plan + entitlements providers).
plan_name—Display name; defaults to plan_handle.
usage—Per-entitlement overrides: {handle: {"used": float, "limit": float}}.
trial_status—Already-derived trial state, overlaid onto the plan provider.
payment_failed—Hard-failure billing-recovery signal.
payment_at_risk—Soft billing-attention signal.
tiers—Current tier handle per capability_tier entitlement handle.
segment_ids—Tenant segment handles your app already resolved for this user. They count as segment membership for chipped payloads, segment-targeted entitlement rules, and the eligible plan and add-on helpers.
custom—Customer traits that segment predicates evaluate over, e.g. {"sessions": 12}.
builtin_dimensions—Built-in segment dimension values your server sets, e.g. {"subscription_state": "paid"}. This is the only way a built-in rt.<dimension>.<value> segment can match on a server port.
experiments—Experiment-handle to variant-handle assignments, for experiment segments.

tenant_id and user_id are the only required fields; an empty/missing one raises ValueError at construction.

A user is in a segment only if segment_ids names it or its predicates match custom, the plan or builtin_dimensions. Anything else fails closed: a payload or rule chipped to a segment the SDK can’t confirm is not served. Since BL-0369 the SDK no longer treats every configured segment as matched.

plan_handle must equal the plan’s unique_handle in the Playbook. Usage keys must equal entitlement handles, and used is the current absolute balance, not an increment.

MethodReturns
can(handle, context=None){status, allowed, reason, ...}; preferred entitlement re-check
check_entitlement(handle, context=None){status, allowed, reason, ...}
get_placement_decision(input){placement_id, visible, output, content, ...}
get_placement_decisions(inputs)ordered list of the above

can() is an alias of check_entitlement(). There is no storage/persistence parameter — the instance is stateless and in-memory by construction; construct a fresh one per user context.

Unlike the TypeScript SDK (which has revturbine_server / custom_endpoints / local_only runtime modes), the Python SDK is local-only by design. There is no runtime-mode switch and no network path:

Python RevTurbineCustomerSdk
NetworkNone — decides from the supplied Playbook
PersistenceNone — InMemoryStorage only; no file/db, no decision cache
StateNone across calls; no interaction/suppression hydration
Inputsuser_context + Playbook, both supplied by the server

This makes it safe on a request hot path: an entitlement gate is a pure in-process computation, with no decision-API round-trip and no shared mutable state between requests.

The Playbook is a snapshot exported from the RevTurbine control plane. The SDK never fetches or caches it — your service owns its lifecycle:

  1. Fetch/export the latest Playbook JSON (on boot, on a timer, or on a webhook from the control plane).
  2. Construct a new RevTurbineCustomerSdk with the new snapshot. Because the instance is stateless, “refresh” is just “rebuild” — atomically swap the reference your handlers read.
Python
# e.g. behind a lock / atomic reference, refreshed every N minutes
_sdk = RevTurbineCustomerSdk(user_context=ctx, exported_config=load_config())

Pin the installed SDK tag to a version whose schema matches the Playbook you deploy; a snapshot newer than the SDK’s schema can decide differently than your TypeScript frontend.

Segment evaluation and targeting-state derivation from a raw user context are ported and parity-locked, so a Python gate and a TypeScript render agree given the same user context — not only the same pre-resolved traits. segment_ids remains available as an explicit override.

The Python SDK is the headless server decision core only. The following are intentionally not ported — they are browser/full-SDK concerns with no server equivalent:

  • React / JSX components and hooks (no Python equivalent).
  • Browser storage, localStorage theme persistence, window.RevTurbine.
  • HTTP-backed / dual-mode dispatch (runtime_mode) — the Python library is local-only.
  • identify, dismiss / snooze / convert, track_treatment_interaction, get_trial_status, capture, bootstrap_placement_decisions, get_user_context, decision-cache / interaction-state hydration.

Every public method delegates, with zero added decision logic, to the same decision substrate the TypeScript SDK uses. Our parity suite runs all three ports — TypeScript, Python, and Rust — through identical fixtures and asserts byte-identical normalized output; a divergence is treated as a Python-port bug, never a fixture to loosen. This is what lets you gate in Python and render in TypeScript and trust they decided the same thing.