Overview
This page is the reference card for what the Python Virtual Machine (PVM) accepts and enforces: the static checks applied to actor code, which modules an actor may import, the runtime determinism guards, and the limits the continuation compiler imposes. For the why, see Determinism & Sandbox; for gas limits and pricing, see Resource Limits and the fee model.Static validation
Actor code is validated before execution. A violation rejects the code with a validation error:Module imports
The PVM enforces imports with a whitelist, with a blacklist veto checked first. Importing anything outside the allowlist raisesNonDeterministicError: module not allowed (older conceptual pages describe this as an ImportError; the implementation raises the dedicated error type).
Hard-banned (blacklist veto):
import time and import random never resolve to the stdlib modules — the import guard aliases them to the SDK’s deterministic replacements before the deny check. Don’t rely on the redirect: CIP-6 §17 forbids import time / import random in actor code; use the supported surface instead — runtime.get_block_height() / runtime.get_timestamp_ms() for time, and runtime.randomness(domain) for randomness.
Commonly used stdlib modules that are importable:
The authoritative allowlist lives in the PVM’s import guard; if a module isn’t importable, treat that as the protocol’s answer rather than working around it. The SDK itself (
cowboy_sdk) and the host module (pvm_host) are always importable.
Runtime guards
These are enforced during execution regardless of what static validation saw:CIP-6 §17 requires
cowboy_sdk.ordered_set in place of set() / frozenset(). The VM does not currently block the set builtin, but treat the spec rule as binding — hash-based iteration order is exactly the kind of implicit nondeterminism these rules exist to keep out of actor logic.Continuation limits
Functions decorated with@runner.continuation (or @actor.continuation) are compiled to state machines at import time. The compiler statically rejects:
@bounded_loop(max_iterations=N) (default 1,000) establishes a per-call iteration budget; the loop body must count against it by iterating with BoundedRange / check_iteration(), and exceeding the budget raises LoopBoundExceeded — a plain for loop under the decorator is not counted automatically. Variables needed after an await must be declared via capture() (CIP-6 §10).
Execution caps
- Dual gas meters — every execution is metered in Cycles (compute) and Cells (data); exhausting either aborts with out-of-gas. See Resource Limits.
- Sub-limits — certain invocations run under caps below the transaction budget; for example the current implementation holds CIP-20 transfer hooks to 50,000 Cycles / 50,000 Cells per call.
- Storage reads — reads are tracked during execution and settled to the meters afterward; a read-heavy handler can run out of gas at settlement.
- Synchronous call depth — cross-actor
call()chains are capped at 32.

