Files
python-sdk/pkg/hanzo-flags/README.md
T
zeekay 6ef78a34c9 hanzo-flags: the Python client for the native flags engine
Flags were natively supported in Rust (the hanzo-flags crate — the evaluation
core, pub-exported for Rust callers and FFI-exported for the rest), Go (in-process
via CGO to that core), and TypeScript (@hanzo/flags over /v1/flags). Python was the
gap. This closes it.

hanzo-flags POSTs an evaluation context to cloud /v1/flags — the same
PostHog-compatible endpoint every other client speaks — and answers is_enabled /
variant / payload from the response. It mirrors @hanzo/flags exactly: a HanzoFlags
client bound to one host, load(distinct_id, person_properties=, groups=), the same
three-field result, an async twin, and a one-shot evaluate().

Two invariants match the family: fail-open (a transport error yields the last good
or empty result with errors_while_computing set, never a raise on the hot path)
and cache-by-context+TTL. Zero runtime dependencies — stdlib urllib only, so a
flag check never drags httpx/pydantic into a service.

Tests stand up a local /v1/flags stub and prove the request shape is
PostHog-compatible, the accessors resolve booleans/variants/payloads, the TTL
cache holds, and both a 5xx and an unreachable host fail open. 7/7 green.
2026-07-17 13:00:33 -07:00

2.4 KiB

hanzo-flags

Feature flags + A/B testing for Python, on the Hanzo native flags engine.

The Python client for cloud /v1/flags — the same PostHog-compatible evaluation endpoint the Rust core, Go binding, and @hanzo/flags (TypeScript) all speak. One flag definition, evaluated identically in every language.

Zero runtime dependencies — stdlib urllib only. A flag check must never drag a dependency tree into a service.

Install

pip install hanzo-flags     # or: uv add hanzo-flags

Use

from hanzo_flags import HanzoFlags

flags = HanzoFlags("https://api.hanzo.ai", token="hk-...")
flags.load("user-123", person_properties={"plan": "pro"})

if flags.is_enabled("checkout-exp"):
    ...

variant = flags.variant("pricing-test")   # "control" | "b" | None
payload = flags.payload("pricing-test")   # the flag's JSON payload, or None

Group targeting (orgs/teams):

from hanzo_flags import Group

flags.load(
    "user-123",
    groups={"0": Group(key="acme", properties={"tier": "gold"})},
)

Async (asyncio services):

from hanzo_flags import AsyncHanzoFlags

flags = AsyncHanzoFlags("https://api.hanzo.ai", token="hk-...")
await flags.load("user-123")
if flags.is_enabled("checkout-exp"):
    ...

One-shot:

from hanzo_flags import evaluate
res = evaluate("https://api.hanzo.ai", "user-123", token="hk-...")
res.is_enabled("checkout-exp")

Guarantees

  • Fail-open. A transport or decode error returns the last good (or empty) result with errors_while_computing set — load() never raises on the hot path.
  • Cached by context + TTL (default 15s). Re-evaluating the same context inside the TTL is free, so a hot path may call load() freely.

The family

Language Package Mode
Rust hanzo-flags crate native (the evaluation core)
Go cloud/clients/flags in-process via FFI to the Rust core
TypeScript @hanzo/flags HTTP to /v1/flags (browser + node)
Python hanzo-flags HTTP to /v1/flags (this package)

All four resolve the same flag to the same value: the definitions live once in the cloud flags cockpit (/v1/flags/defs), and every client evaluates against them.