Python SDK Guide
Framework integration, testing, and best practices for the Python SDK.
Type-safe with TypedDict
Generate TypedDict types from the Replane dashboard for full type safety with your configs.
Using generated types
from replane import Replane
from replane_types import Configs # Generated from Replane dashboard
# Pass the Configs TypedDict as a type parameter
with Replane[Configs](
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
) as replane:
# Access configs with dictionary-style notation
settings = replane.configs["app_settings"]
# Full type safety - IDE knows the structure
print(settings["max_upload_size_mb"])
print(settings["allowed_file_types"])
The .configs accessor
The .configs property provides a dictionary-like interface:
# Bracket access (raises KeyError if missing)
value = replane.configs["feature_flag"]
# Safe access with default
timeout = replane.configs.get("timeout", 30)
# Check if config exists
if "feature_flag" in replane.configs:
flag = replane.configs["feature_flag"]
# List all config names
for name in replane.configs.keys():
print(name)
Accessing configs
| Method | Description |
|---|---|
replane.configs["name"] | Returns config value; raises KeyError if not found |
replane.configs.get("name") | Returns config value or None if not found |
replane.configs.get("name", default) | Returns config value or default if not found |
"name" in replane.configs | Check if config exists |
Use bracket notation when you want an error on missing configs. Use .get() for safe access with fallback values.
Context and overrides
Context is used to evaluate override rules. Context data stays in your application and is never sent to the server.
Client-level context
Applied to all config accesses:
replane = Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
context={"environment": "production", "region": "us-east"},
)
# Uses client context
value = replane.configs["config_name"]
Per-evaluation context
Merged with client context (per-call values take precedence):
value = replane.with_context({
"user_id": user.id,
"plan": user.plan,
}).configs["feature_flag"]
Scoped clients with with_context()
Create lightweight scoped clients for specific users or requests:
with Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
) as replane:
# Create a scoped client for a specific user
user_client = replane.with_context({
"user_id": user.id,
"plan": user.plan,
})
# All operations use the merged context
rate_limit = user_client.configs["rate_limit"]
settings = user_client.configs["app_settings"]
# Chaining for additional context
request_client = user_client.with_context({"region": request.region})
This is especially useful in web frameworks for per-request context:
@app.get("/items")
async def get_items(request: Request):
user_client = replane.with_context({
"user_id": request.user.id,
"plan": request.user.plan,
})
return {"max_items": user_client.configs["limits"]["max_items"]}
Scoped defaults with with_defaults()
Create scoped clients with fallback values for missing configs:
with Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
) as replane:
# Create a client with fallback defaults
safe_client = replane.with_defaults({
"timeout": 30,
"max_retries": 3,
})
# Returns the default if config doesn't exist
timeout = safe_client.configs["timeout"] # 30 if not configured
Combine both methods for flexible scoping:
# User-specific context with safe defaults
user_client = replane.with_context({
"user_id": user.id,
"plan": user.plan,
}).with_defaults({
"rate_limit": 100,
"max_upload_size": 10,
})
# Uses context for override evaluation, falls back to defaults
rate_limit = user_client.configs["rate_limit"]
Testing
In-memory client
Use InMemoryReplaneClient or create_test_client for tests:
from replane.testing import create_test_client, InMemoryReplaneClient
# Simple usage
replane = create_test_client({
"feature_enabled": True,
"rate_limit": 100,
})
assert replane.configs["feature_enabled"] is True
assert replane.configs["rate_limit"] == 100
Testing with overrides
replane = InMemoryReplaneClient()
replane.set_config(
"feature",
value=False,
overrides=[{
"name": "premium-users",
"conditions": [
{"operator": "in", "property": "plan", "expected": ["pro", "enterprise"]}
],
"value": True,
}],
)
assert replane.with_context({"plan": "free"}).configs["feature"] is False
assert replane.with_context({"plan": "pro"}).configs["feature"] is True
Pytest fixture
import pytest
from replane.testing import create_test_client
@pytest.fixture
def replane():
return create_test_client({
"feature_flags": {"dark_mode": True, "new-ui": False},
"rate_limits": {"default": 100, "premium": 1000},
})
def test_feature_flag(replane):
flags = replane.configs["feature_flags"]
assert flags["dark_mode"] is True
Framework integration
FastAPI
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import FastAPI, Depends
from replane import AsyncReplane
_replane: AsyncReplane | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global _replane
_replane = AsyncReplane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
)
await _replane.connect()
yield
await _replane.close()
app = FastAPI(lifespan=lifespan)
def get_replane() -> AsyncReplane:
assert _replane is not None
return _replane
# Define reusable dependency type
Replane = Annotated[AsyncReplane, Depends(get_replane)]
@app.get("/items")
async def get_items(replane: Replane):
free_client = replane.with_context({"plan": "free"})
max_items = free_client.configs["max_items"]
return {"max_items": max_items}
Flask
from flask import Flask
from replane import Replane
app = Flask(__name__)
replane: Replane | None = None
def get_replane():
global replane
if replane is None:
replane = Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
)
replane.connect()
return replane
@app.route("/items")
def get_items():
rp = get_replane()
max_items = rp.configs["max_items"]
return {"max_items": max_items}
@app.teardown_appcontext
def close_replane(exception):
if replane is not None:
replane.close()
Django
# settings.py
from replane import Replane
REPLANE = Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
)
REPLANE.connect()
# views.py
from django.conf import settings
def my_view(request):
user_client = settings.REPLANE.with_context({"user_id": request.user.id})
rate_limit = user_client.configs["rate_limit"]
# ...
Best practices
Initialize once
Create the client once at application startup:
# config.py
from replane import Replane
import os
replane = Replane(
base_url=os.environ["REPLANE_URL"],
sdk_key=os.environ["REPLANE_SDK_KEY"],
)
replane.connect()
# app.py
from config import replane
value = replane.configs["feature_flag"]
Use context managers
Context managers ensure proper cleanup:
with Replane(...) as replane:
# Client is connected and ready
value = replane.configs["config"]
# Client is automatically closed
Use defaults for resilience
replane = Replane(
base_url="https://cloud.replane.dev",
sdk_key="rp_...",
defaults={
"feature_flag": False,
"rate_limit": 100,
},
)
Clean up on shutdown
import atexit
replane = Replane(...)
replane.connect()
atexit.register(replane.close)