varianz package (install). Its test-side counterpart, varianz-pytest, is covered in Testing → pytest.
Runtime
VARIANZ_ENABLED=true); otherwise every API is a no-op and VPoints pass through.
Defining VPoints
- The
name=keyword argument is required in practice — always set it explicitly. - Decorating is not enough: the function, class, or instance must be registered.
register() is idempotent: passing an already-registered @vz.vpoint function returns the same
object, so calling it twice or discarding the result is harmless.The module-level decorator is different. from varianz import vpoint does not register, and
register() on a plain function returns a new wrapper — so there you must rebind:Registration methods
For classes, register the instance to preserve constructor state (
vz.register(PricingService(config))); registering the class auto-instantiates via cls(). Don’t pass unbound methods.
Context functions bind to the instance registered first under a given VPoint name. Registering a
second instance of the same class re-points the VPoint body to the new instance, while its
@functions continue to resolve against the first. The behaviour is silent, so a stage calling
ctx.* reads the earlier instance’s state without reporting anything.Create the Varianz runtime per test, alongside the service instance it registers, and the binding
follows the instance you expect. This matters most in the common pytest pattern of building a fresh
service for each test.vz.register(...) blocks until the coordinator confirms the schema (up to 5 seconds), then proceeds regardless — Varianz fails open if the coordinator is unreachable.
Schema resolution: annotate your types
With type annotations (recommended): the schema is built at decoration time, the VPD registers eagerly, and stages work from the first request. Statically mapped types:int, float, bool, str, None, list[T], dict[K, V], Optional[T], and @dataclass types. Proto messages resolve eagerly from their DESCRIPTOR.
Without annotations (lazy fallback): a VPoint defers its schema when any parameter it has to expose lacks a usable annotation. It then registers an unknown placeholder and fills the schema in from real values later. A missing return annotation is not a trigger — that registers eagerly, with an unknown return kind.
opaque= is how you keep a signature eager: it excludes a slot that can never carry a schema, such as a gRPC servicer’s context, so the remaining parameters still derive. It is not a cause of deferral.
Deferral costs you validation rather than delivery — stages still apply on the first call (details). Runtime-introspectable types include Pydantic models, attrs classes, and anything with __annotations__, __slots__, or vars().
Prefer annotations; where you can’t, wire varianz-scan into the build.
gRPC servicer methods
Python can instrument servicer methods directly — annotate the proto types and mark the gRPC context as opaque:opaque=["context"] passes the ServicerContext through without exposing it to CEL. Proto field names, types, and nested message types come from the descriptor — no warmup needed. CEL overrides that construct proto structs (ChargeResponse { transaction_id: "test" }) are converted back to real proto messages automatically. Use the message’s simple name in CEL, not the package-qualified one.
Set up the server interceptor once so sessions reach these handlers.
Local stages
For unit and in-process integration tests, attach a Python callback straight to a VPoint — no coordinator, no session, no CEL:attach_stage(target, anchor, name, stage_fn, enabled=True). target is a VPoint lookup name or the @vpoint-decorated function itself; anchor is a varianz.Anchor ("VALIDATE" | "PRE" | "MID" | "POST" | "AUDIT", checked eagerly, so a typo raises at attach time even while the SDK is disabled). attach_cel_stage(target, anchor, name, expression, enabled=True) takes the same shape with a CEL string instead of a callback, and the same methods are available on the decorated wrapper itself (calculate.attach_stage(anchor, name, fn)).
The callback contract:
ctx.argsexposes the VPoint’s named arguments — the caller’s live objects, with Python identity preserved.ctx.selfis the registered instance for a class-based VPoint,Nonefor a plain function.- Return
varianz.CONTINUEto fall through to the next stage or the original function; return anything else, includingNone, to override the result. - The override is converted against the VPoint’s declared return type — a mistyped one raises
TypeErrorrather than returning corrupted data. Struct returns are checked against the declared class, not just scalars. - An exception raised inside the callback reaches the caller unchanged, with its original type and traceback. That’s the way to inject failures.
async defcallbacks are rejected: stages are synchronous, and an un-awaited coroutine would silently do nothing.
Handles and scoping
attach_stage and attach_cel_stage return a StageHandle with set_enabled(bool), is_enabled(), and remove(). Operating on a stage that’s already gone raises rather than silently doing nothing. Use it as a context manager to scope a stage to a block — the analogue of Node’s using:
enabled=False to attach a stage dormant. When the SDK is disabled, attaching returns an inert handle with the same methods, so test code doesn’t need to branch — but argument validation still runs, so a bad anchor fails identically either way.
Typed helpers
Runningpython -m varianz.buildscan during your build emits varianz_stages.py alongside the descriptor file, with one attach_<vpoint>_stage helper per VPoint. The helper types ctx.args field by field and types the override’s return, so a wrong field name or return type is a type error in your editor rather than a runtime one:
--stages-output at hand-written code fails the build instead of eating it.
Local stages apply to every call in the process, not just calls carrying a session ID. Use them for behavior owned by the process under test; use CEL stages when you need session isolation, cross-service delivery, or samples.
Session API
Propagation is interceptor-driven; these are the primitives interceptors use:contextvar, so it follows asyncio tasks automatically.
