Skip to main content

Python sandbox powered by WebAssembly

Project description

PyEryx - Python Bindings for Eryx

Python bindings for the Eryx sandbox - execute Python code securely inside WebAssembly.

Installation

pip install pyeryx

Note: The package is installed as pyeryx but imported as eryx.

Or build from source using maturin:

cd crates/eryx-python
maturin develop

Quick Start

import eryx

# Create a sandbox with the embedded Python runtime
sandbox = eryx.Sandbox()

# Execute Python code in complete isolation
result = sandbox.execute('''
print("Hello from the sandbox!")
x = 2 + 2
print(f"2 + 2 = {x}")
''')

print(result.stdout)
# Output:
# Hello from the sandbox!
# 2 + 2 = 4

print(f"Execution took {result.duration_ms:.2f}ms")

Features

  • Complete Isolation: Sandboxed code cannot access files, network, or system resources
  • Resource Limits: Configure timeouts and memory limits
  • Fast Startup: Pre-initialized Python runtime embedded for ~1-5ms sandbox creation
  • Pre-initialization: Custom snapshots with packages for even faster specialized sandboxes
  • Package Support: Load Python packages (.whl, .tar.gz) including native extensions
  • Persistent Sessions: Maintain Python state across executions with Session
  • Virtual Filesystem: Sandboxed file storage with VfsStorage
  • Type Safe: Full type stubs for IDE support and static analysis

Python Version

The sandbox runs CPython 3.14 compiled to WebAssembly (WASI). This is the same Python build used by componentize-py from the Bytecode Alliance.

Note: Python 3.14 is currently in development. The sandbox tracks the latest WASI-compatible CPython build from the componentize-py project.

Performance

The pyeryx package ships with a pre-initialized Python runtime embedded in the binary. This means Python's interpreter initialization (~450ms) has already been done at build time, so creating a sandbox is very fast:

import eryx
import time

# First sandbox - fast! (~1-5ms)
start = time.perf_counter()
sandbox = eryx.Sandbox()
print(f"Sandbox created in {(time.perf_counter() - start) * 1000:.1f}ms")

# Execution is also fast
start = time.perf_counter()
result = sandbox.execute('print("Hello!")')
print(f"Execution took {(time.perf_counter() - start) * 1000:.1f}ms")

For repeated sandbox creation with custom packages, see SandboxFactory below.

API Reference

Sandbox vs Session

Feature Sandbox Session
State persistence No - fresh each execute() Yes - variables persist
Virtual filesystem No Yes (optional)
Use case One-off execution REPL, multi-step workflows
Isolation Complete per-call Complete from host

Use Sandbox when:

  • Running untrusted code that should start fresh each time
  • Each execution is independent
  • You want maximum isolation between executions

Use Session when:

  • Building up state across multiple executions
  • You need file persistence (VFS)
  • Implementing a REPL or notebook-like experience
  • Performance matters (no re-initialization per call)

Sandbox

The main class for executing Python code in isolation.

sandbox = eryx.Sandbox(
    resource_limits=eryx.ResourceLimits(
        execution_timeout_ms=5000,      # 5 second timeout
        max_memory_bytes=100_000_000,   # 100MB memory limit
    )
)

result = sandbox.execute("print('Hello!')")

Loading Packages

To use custom packages, use SandboxFactory which bundles packages into a reusable runtime snapshot:

import eryx

# Create a factory with your packages (one-time, takes 3-5 seconds)
factory = eryx.SandboxFactory(
    packages=[
        "/path/to/jinja2-3.1.2-py3-none-any.whl",
        "/path/to/markupsafe-2.1.3-wasi.tar.gz",  # WASI-compiled native extension
    ],
    imports=["jinja2"],  # Optional: pre-import for faster first execution
)

# Create sandboxes with packages already loaded (~10-20ms each)
sandbox = factory.create_sandbox()
result = sandbox.execute('''
from jinja2 import Template
template = Template("Hello, {{ name }}!")
print(template.render(name="World"))
''')

For packages with native extensions (like markupsafe), you need WASI-compiled versions. These are automatically late-linked into the WebAssembly component.

ExecuteResult

Returned by sandbox.execute() with execution results:

  • stdout: str - Captured standard output
  • duration_ms: float - Execution time in milliseconds
  • callback_invocations: int - Number of callback invocations
  • peak_memory_bytes: Optional[int] - Peak memory usage (if available)

ResourceLimits

Configure execution constraints:

limits = eryx.ResourceLimits(
    execution_timeout_ms=30000,        # Max script runtime (default: 30s)
    callback_timeout_ms=10000,         # Max single callback time (default: 10s)
    max_memory_bytes=134217728,        # Max memory (default: 128MB)
    max_callback_invocations=1000,     # Max callbacks (default: 1000)
)

# Or create unlimited (use with caution!)
unlimited = eryx.ResourceLimits.unlimited()

SandboxFactory

For use cases with custom packages, SandboxFactory lets you create a reusable factory with your packages pre-loaded and pre-imported.

Note: For basic usage without packages, eryx.Sandbox() is already fast (~1-5ms) because the base runtime ships pre-initialized. Use SandboxFactory only when you need to bundle custom packages.

Use cases for SandboxFactory:

  • Load packages (jinja2, numpy, etc.) once and create many sandboxes from the factory
  • Pre-import modules to eliminate import overhead on first execution
  • Save/load factory state to disk for persistence across process restarts
import eryx

# One-time factory creation with packages (takes 3-5 seconds)
factory = eryx.SandboxFactory(
    packages=[
        "/path/to/jinja2-3.1.2-py3-none-any.whl",
        "/path/to/markupsafe-2.1.3-wasi.tar.gz",
    ],
    imports=["jinja2"],  # Pre-import modules
)

# Create sandboxes with packages already loaded (~10-20ms each)
sandbox = factory.create_sandbox()
result = sandbox.execute('''
from jinja2 import Template
print(Template("Hello {{ name }}").render(name="World"))
''')

# Create many sandboxes from the same factory
for i in range(100):
    sandbox = factory.create_sandbox()
    sandbox.execute(f"print('Sandbox {i}')")

Saving and Loading

Save factories to disk for instant startup across process restarts:

# Save the factory (includes pre-compiled WASM state + package state)
factory.save("/path/to/jinja2-factory.bin")

# Later, in another process - loads in ~10ms (vs 3-5s to recreate)
factory = eryx.SandboxFactory.load("/path/to/jinja2-factory.bin")
sandbox = factory.create_sandbox()

Properties and Methods

  • factory.size_bytes - Size of the pre-compiled factory in bytes
  • factory.create_sandbox(resource_limits=...) - Create a new sandbox
  • factory.save(path) - Save factory to a file
  • factory.to_bytes() - Get factory as bytes
  • SandboxFactory.load(path) - Load factory from a file

Session

Unlike Sandbox which runs each execution in isolation, Session maintains persistent Python state across multiple execute() calls. This is useful for:

  • Interactive REPL-style execution
  • Building up state incrementally
  • Faster subsequent executions (no Python initialization overhead per call)
import eryx

session = eryx.Session()

# State persists across executions
session.execute("x = 42")
session.execute("y = x * 2")
result = session.execute("print(f'{x} * 2 = {y}')")
print(result.stdout)  # "42 * 2 = 84"

# Functions and classes persist too
session.execute("""
def greet(name):
    return f"Hello, {name}!"
""")
result = session.execute("print(greet('World'))")
print(result.stdout)  # "Hello, World!"

Session with Virtual Filesystem

Sessions can optionally use a virtual filesystem (VFS) for persistent file storage that survives across executions and even session resets:

import eryx

# Create shared storage
storage = eryx.VfsStorage()

# Create session with VFS enabled
session = eryx.Session(vfs=storage)

# Write files to the virtual filesystem
session.execute("""
with open('/data/config.json', 'w') as f:
    f.write('{"setting": "value"}')
""")

# Files persist across executions
result = session.execute("""
import json
with open('/data/config.json') as f:
    config = json.load(f)
print(config['setting'])
""")
print(result.stdout)  # "value"

# Files even persist across session.reset()
session.reset()
result = session.execute("print(open('/data/config.json').read())")
# File still exists!

Sharing Storage Between Sessions

Multiple sessions can share the same VfsStorage for inter-session communication:

import eryx

# Shared storage instance
storage = eryx.VfsStorage()

# Session 1 writes data
session1 = eryx.Session(vfs=storage)
session1.execute("open('/data/shared.txt', 'w').write('from session 1')")

# Session 2 reads it
session2 = eryx.Session(vfs=storage)
result = session2.execute("print(open('/data/shared.txt').read())")
print(result.stdout)  # "from session 1"

Custom Mount Path

By default, VFS files are accessible under /data. You can customize this:

session = eryx.Session(vfs=storage, vfs_mount_path="/workspace")
session.execute("open('/workspace/file.txt', 'w').write('custom path')")

State Snapshots

Capture and restore Python state for checkpointing:

session = eryx.Session()
session.execute("x = 42")
session.execute("data = [1, 2, 3]")

# Capture state as bytes (uses pickle internally)
snapshot = session.snapshot_state()

# Clear state
session.clear_state()

# Restore from snapshot
session.restore_state(snapshot)
result = session.execute("print(x, data)")
print(result.stdout)  # "42 [1, 2, 3]"

# Snapshots can be saved to disk and restored in new sessions
with open("state.bin", "wb") as f:
    f.write(snapshot)

Session Properties and Methods

  • session.execute(code) - Execute code, returns ExecuteResult
  • session.reset() - Reset Python state (VFS persists)
  • session.clear_state() - Clear variables without full reset
  • session.snapshot_state() - Capture state as bytes
  • session.restore_state(snapshot) - Restore from snapshot
  • session.execution_count - Number of executions performed
  • session.execution_timeout_ms - Get/set timeout in milliseconds
  • session.vfs - Get the VfsStorage (if enabled)
  • session.vfs_mount_path - Get the VFS mount path (if enabled)

VfsStorage

In-memory virtual filesystem storage. Files written to the VFS are completely isolated from the host filesystem - sandboxed code cannot access real files.

import eryx

# Create storage (can be shared across sessions)
storage = eryx.VfsStorage()

# Use with Session
session = eryx.Session(vfs=storage)

The VFS supports standard Python file operations:

  • open(), read(), write() - File I/O
  • os.makedirs(), os.listdir(), os.remove() - Directory operations
  • os.path.exists(), os.path.isfile() - Path checks
  • pathlib.Path - Full pathlib support

Exceptions

  • eryx.EryxError - Base exception for all Eryx errors
  • eryx.ExecutionError - Python code raised an exception
  • eryx.InitializationError - Sandbox failed to initialize
  • eryx.ResourceLimitError - Resource limit exceeded
  • eryx.TimeoutError - Execution timed out

Package Loading

Supported Formats

  • .whl - Standard Python wheels (zip archives)
  • .tar.gz / .tgz - Tarballs (used by wasi-wheels project)
  • Directories - Pre-extracted package directories

Native Extensions

Packages containing native Python extensions (.so files compiled for WASI) are automatically detected and late-linked into the WebAssembly component. This allows packages like numpy, markupsafe, and others to work in the sandbox.

Note: You need WASI-compiled versions of native extensions, not regular Linux/macOS/Windows binaries.

Error Handling

import eryx

sandbox = eryx.Sandbox()

try:
    result = sandbox.execute("raise ValueError('oops')")
except eryx.ExecutionError as e:
    print(f"Code failed: {e}")

try:
    sandbox = eryx.Sandbox(
        resource_limits=eryx.ResourceLimits(execution_timeout_ms=100)
    )
    result = sandbox.execute("while True: pass")
except eryx.TimeoutError as e:
    print(f"Timed out: {e}")

Development

Building

# Install maturin
pip install maturin

# Build and install in development mode
maturin develop

# Build release wheel
maturin build --release

Testing

pip install pytest
pytest

License

MIT OR Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyeryx-0.2.1.tar.gz (12.8 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

pyeryx-0.2.1-cp312-abi3-win_amd64.whl (44.2 MB view details)

Uploaded CPython 3.12+Windows x86-64

pyeryx-0.2.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (45.4 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ x86-64

pyeryx-0.2.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (44.6 MB view details)

Uploaded CPython 3.12+manylinux: glibc 2.17+ ARM64

pyeryx-0.2.1-cp312-abi3-macosx_11_0_arm64.whl (44.6 MB view details)

Uploaded CPython 3.12+macOS 11.0+ ARM64

pyeryx-0.2.1-cp312-abi3-macosx_10_12_x86_64.whl (44.8 MB view details)

Uploaded CPython 3.12+macOS 10.12+ x86-64

File details

Details for the file pyeryx-0.2.1.tar.gz.

File metadata

  • Download URL: pyeryx-0.2.1.tar.gz
  • Upload date:
  • Size: 12.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pyeryx-0.2.1.tar.gz
Algorithm Hash digest
SHA256 b7b44a5f479cedc2860219e8d7313a47df7dbd4b0538650fd385a18524f3c773
MD5 7cba1c1c280b57ffc3e0da1d278cab12
BLAKE2b-256 00e319e123a1d0d6f2bd77b9a716cd8522408c49bab05493c6d326e357ad69a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1.tar.gz:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeryx-0.2.1-cp312-abi3-win_amd64.whl.

File metadata

  • Download URL: pyeryx-0.2.1-cp312-abi3-win_amd64.whl
  • Upload date:
  • Size: 44.2 MB
  • Tags: CPython 3.12+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pyeryx-0.2.1-cp312-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 3426680f9219336f016b90e77870fc3228382f1d94d9c61dc10aab15afcae535
MD5 a0f704113ac17563be882035047d2a15
BLAKE2b-256 f710813a00acc635198eb12950d6ae0574b230aba5c14e89adecc0ea5ff9123c

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1-cp312-abi3-win_amd64.whl:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeryx-0.2.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pyeryx-0.2.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e16a4588113bf238aecef474e3261c46cd8bfc23f03c6e740e62f4eadad87089
MD5 a5ad8abaa002d64573adbdaaab9eff3b
BLAKE2b-256 8de53d1a1e5e82289057aa5ba8b1d1ddbeea633ec86b47881fee81decda6bf11

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeryx-0.2.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pyeryx-0.2.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 3c83f4454b8bc7692ad2023ee54447dc629ae47a77559f60ec337e593d741f21
MD5 cec82e911e16a80ae2645f8e8a406b1b
BLAKE2b-256 86630469630678401ab09a013159b8095d7b3bcad0955fef8b81daa42f4ee37a

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeryx-0.2.1-cp312-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for pyeryx-0.2.1-cp312-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7ffdbe7499c637b10c15614afa9a440d13d2ece3ed67946da11e4841b893941c
MD5 f9b530fa386943c77f3652ac36e44b7f
BLAKE2b-256 f1417c0b358e7861127d6e4a9219aeec26ee131f569fb917b51ea04f14158569

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1-cp312-abi3-macosx_11_0_arm64.whl:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyeryx-0.2.1-cp312-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pyeryx-0.2.1-cp312-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 a97bb03754986d8515df5173e901505b2591305a630eccc5f9d4ef481c45f740
MD5 cc0826349846e713eaef160a83bea435
BLAKE2b-256 63c9bda40342cd544bd0606cc88bfd48d08c976b3a3f1a553cfa658ad40701bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyeryx-0.2.1-cp312-abi3-macosx_10_12_x86_64.whl:

Publisher: python-release.yml on eryx-org/eryx

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page