Skip to content

Python API

This page is generated from the package's docstrings and signatures by uv run python tests/harness/api.py --write, and a check fails when it differs from the live surface. Edit the docstrings, not this page.

What is promised

The public API is the names below: what wasi_python and wasi_python.tools export (each module's __all__), the wasi-python command (see Command line), and the environment variables WASI_PYTHON_ARTIFACTS and WASI_PYTHON_CACHE. Everything else is internal and may change in any release: every other name, and the modules runner, mounts and cli. The project's tests record this surface and fail on any change to it that was not made on purpose.

The promise starts at 0.1.0. Versions are 0.MINOR.PATCH. A patch release changes no public name, signature, field, default or command option. A minor release may add names, keyword-only arguments whose default keeps today's behaviour, dataclass fields placed last with a default (or keyword-only), Errno members, stats() keys, command options and JSON keys. It removes or changes something only after a deprecation: a DeprecationWarning, a docstring that begins "Deprecated since 0.N: use X instead." and a CHANGELOG line, all at once. A thing deprecated in 0.N still works in 0.N and 0.N+1; 0.N+2 is the first release that may remove it. Two exceptions need no deprecation: a tighter default limit, in a minor release, and a security fix, in any release; each is named in the CHANGELOG. Exceptions are promised by type, never by message. Not promised: messages, reprs, help text, wasmtime's trap names and error types, timings, fuel counts, the .cwasm cache, and the guest's package versions. The tool protocol is versioned by PROTOCOL. At 1.0 the same rules hold with "major" in place of "minor".

Reference

wasi_python

wasi_python.Artifacts

@dataclass(frozen=True)
class Artifacts:
    snapshot: Path
    plain: Path
    stdlib_zip: Path
    manifest: Path

The artifact set's four files, made by artifacts(), or by you for run(artifacts=…).

Attributes:

  • snapshot: The snapshot binary, python-snapshot.wasm: the reactor with the stdlib's bootstrap imported (no package).
  • plain: The plain reactor, python.wasm, without the snapshot.
  • stdlib_zip: The stdlib zip, python-stdlib.zip, which every guest sees read-only at /usr/local/lib.
  • manifest: The MANIFEST: every pinned source and recipe hash the build used.

wasi_python.ArtifactsMissing

class ArtifactsMissing(RuntimeError):
    ...

Raised when there is no usable artifact set; the message says how to get one.

wasi_python.Completed

@dataclass(frozen=True)
class Completed:
    exit_code: int | None
    stdout: bytes
    stderr: bytes
    trapped: str | None
    fuel_used: int | None
    truncated: tuple[str, ...] = ()
    files: FileRecord | None = None
    tools: ToolRecord | None = None

How one program ended: what run returns.

Attributes:

  • exit_code: The program's exit status: its return value, or the code it passed to exit. None when it trapped.
  • stdout: What the guest wrote to standard output, up to the output limit.
  • stderr: What the guest wrote to standard error, up to the output limit.
  • trapped: None when the program ended by itself. Otherwise one of four fixed strings, "out of fuel", "deadline", "cancelled" or "out of file calls", or wasmtime's words: a trap's plain name ("stack overflow"), or an error's first line. The four strings are promised; wasmtime's words are not.
  • fuel_used: The guest's instructions spent, when a fuel cap was given and no deadline hit; else None.
  • truncated: The streams ("stdout", "stderr") whose output passed the limit.
  • files: For a call with mounts, its provider calls, faults and refused writes; else None.
  • tools: For a call with tools, every frame counted and the first 1,000 listed; else None.

wasi_python.Defaults

@dataclass(frozen=True)
class Defaults:
    memory_bytes: int | None
    deadline_s: float | None
    output_bytes: int | None
    async_workers: int = <factory>
    file_calls: int | None = 10000
    tool_calls: int | None = 1000
    tool_request_bytes: int | None = 16777216
    open_files: int | None = 1024
    session_idle_s: float | None = 900.0
    session_lifetime_s: float | None = 3600.0
    session_shutdown_s: float = 5.0

The limits a call has when it names none: defaults() returns the ones in force, and set_defaults() replaces them.

Attributes:

  • memory_bytes: The most linear memory a guest may hold, in bytes.
  • deadline_s: The wall-clock seconds a call may run, enforced by epoch interruption, which costs nothing per instruction.
  • output_bytes: The most bytes kept per stream; bytes past it are dropped and the stream is named in truncated.
  • async_workers: How many guests run_async's own pool runs at once; by default the CPU count, at most 8.
  • file_calls: The most provider calls a call with mounts makes; then it ends "out of file calls".
  • tool_calls: The most tool-protocol frames a call with tools answers; past it the guest gets ToolBudgetExceeded.
  • tool_request_bytes: The most bytes of tool-protocol requests a call with tools reads, counted from each frame's length before it is read; a frame that would pass it is refused unread, and the guest gets ToolBudgetExceeded.
  • open_files: The most files and folders a guest holds open at once, in grants, the root and mounts alike; past it an open answers EMFILE and the guest runs on.
  • session_idle_s: The longest a Session may rest between turns before it ends "idle".
  • session_lifetime_s: How long after its opening a Session's turns may run; then it ends "lifetime".
  • session_shutdown_s: The seconds a Session's ending has to run Python's shutdown (atexit handlers, then finalization); past them the session is simply stopped. 0 skips the shutdown; it cannot be None.

wasi_python.Errno

class Errno(IntEnum):
    EACCES = 2
    EBUSY = 10
    ECANCELED = 11
    EDQUOT = 19
    EEXIST = 20
    EFBIG = 22
    EINVAL = 28
    EIO = 29
    EISDIR = 31
    ENAMETOOLONG = 37
    ENOENT = 44
    ENOSPC = 51
    ENOSYS = 52
    ENOTDIR = 54
    ENOTEMPTY = 55
    ENOTSUP = 58
    EPERM = 63
    EROFS = 69
    ESTALE = 72
    ETIMEDOUT = 73
    EXDEV = 75

An answer a provider gives instead of a value: WASI preview1's numbers, which the guest sees unchanged, each as the OSError of that errno. The set is closed: no success, nothing that makes the guest's C library retry, and nothing that describes the guest's own arguments.

wasi_python.FileFault

@dataclass(frozen=True)
class FileFault:
    method: str
    path: str
    error: str

One provider call that raised an exception that is not an answer, or answered outside its protocol.

Attributes:

  • method: The provider's method, such as "read".
  • path: The path it was given; a rename's source.
  • error: The exception's type and first line, or "bad answer: ".

wasi_python.FileProvider

class FileProvider:
    def stat(self, path: str) -> Stat | Errno
    def list(self, path: str) -> Iterable[tuple[str, str]] | Errno
    def read(self, path: str) -> bytes | bytearray | memoryview | Errno

What a mount's provider answers, for type checkers: a read-only mount's three methods.

Methods:

  • FileProvider.stat: What the path is: a Stat, whose kind for / must be "dir", or an Errno.
  • path: An absolute path within the mount.
  • FileProvider.list: A folder's entries, as (name, kind) pairs, or an Errno.
  • path: An absolute path within the mount.
  • FileProvider.read: A file's whole content, or an Errno: EISDIR when the path itself is a folder, and ENOTDIR when a parent is a file.
  • path: An absolute path within the mount.

wasi_python.FileRecord

@dataclass(frozen=True)
class FileRecord:
    calls: Mapping[str, int]
    faults: tuple[FileFault, ...] = ()
    refused: tuple[RefusedWrite, ...] = ()
    refused_total: int = 0

A call's provider calls, counted. No times, and no exception objects, so a result holds no frame of the call.

Attributes:

  • calls: Each method's count, all eight methods, zeros included.
  • faults: The first 1,000 faults.
  • refused: The first 1,000 refused writes.
  • refused_total: How many writes were refused in all, so a cut list says so.

wasi_python.Grant

@dataclass(frozen=True)
class Grant:
    host: str | os.PathLike[str]
    guest: str
    writable: bool = False  # keyword-only

A host folder the guest sees at a guest path, read-only unless writable=True.

Attributes:

  • host: The host folder, a str or os.PathLike[str].
  • guest: Where the guest sees it: an absolute guest path, not / and not the runtime's own /usr/local/lib.
  • writable: Whether the guest may write there. Keyword-only.

wasi_python.Mount

@dataclass(frozen=True)
class Mount:
    provider: FileProvider
    guest: str
    writable: bool = False  # keyword-only

A provider whose files the guest sees at a guest path, read-only unless writable=True.

Attributes:

  • provider: The provider: a FileProvider, or a WritableFileProvider for a writable mount.
  • guest: Where the guest sees it: an absolute guest path, or / for the guest's whole filesystem (not with root=).
  • writable: Whether the guest may write there; it needs the provider's write half. Keyword-only.

wasi_python.RefusedWrite

@dataclass(frozen=True)
class RefusedWrite:
    mount: str
    method: str
    paths: tuple[str, ...]
    errno: Errno
    at: str
    cause: str

One write-half call the provider did not accept, or a write-back the runner owed and did not make.

Attributes:

  • mount: The mount's guest path.
  • method: The provider's method, such as "write".
  • paths: The provider path, or (src, dst) for a rename.
  • errno: The provider's answer, or EIO for a fault or the limit.
  • at: When: "open", "call", "close", "sync" or "exit".
  • cause: Why: "refused", "fault" or "limit".

wasi_python.Session

class Session:
    def __init__(
        self,
        *,
        root: str | os.PathLike[str] | bool | None = False,
        grants: Iterable[Grant] = (),
        mounts: Iterable[Mount] = (),
        import_path: Iterable[str] = (),
        env: Mapping[str, str] | None = None,
        host: Callable[[bytes], bytes | bytearray | memoryview] | None = None,
        snapshot: bool = True,
        fuel: int | None = None,
        deadline_s: float | None = <default>,
        memory_bytes: int | None = <default>,
        output_bytes: int | None = <default>,
        cache_dir: str | os.PathLike[str] | None | bool = None,
        artifacts: Artifacts | None = None,
        max_file_calls: int | None = <default>,
        tools: Iterable[Tool | Callable[..., Any]] | None = None,
        gate: Callable[[ToolCall], Any] | None = None,
        max_tool_calls: int | None = <default>,
        max_tool_request_bytes: int | None = <default>,
        max_open_files: int | None = <default>,
        idle_s: float | None = <default>,
        lifetime_s: float | None = <default>,
        shutdown_s: float = <default>,
    ) -> None
    def __enter__(self) -> Session
    def __exit__(self, *exc: object) -> None
    @property
    def ended(self) -> str | None
    @property
    def shutdown_outcome(self) -> str | None
    def run(
        self,
        code: str,
        *,
        fuel: int | None = <default>,
        deadline_s: float | None = <default>,
        output_bytes: int | None = <default>,
        cancel: threading.Event | None = None,
    ) -> Completed
    async def run_async(
        self,
        code: str,
        *,
        fuel: int | None = <default>,
        deadline_s: float | None = <default>,
        output_bytes: int | None = <default>,
        executor: concurrent.futures.Executor | None = None,
    ) -> Completed
    def close(self) -> None

One instance of the runtime kept across several calls, each a turn: variables, functions and imports made in one turn are there in the next.

Methods:

  • Session.__init__: Open a session: check every argument, find the artifact set, make the folders and the store, and instantiate the guest. No guest code runs until the first turn.
  • root: The host folder the guest sees as /, writable, made if missing. False or None (the default) gives no / and nothing writable; True gives a fresh private folder, removed when the session ends.
  • grants: Host folders the guest sees at their guest paths, each a Grant, read-only unless it says writable=True. The stdlib zip is always granted, read-only, at /usr/local/lib.
  • mounts: Providers whose files the guest sees at their guest paths, each a Mount, served from your code, read-only unless it says writable=True.
  • import_path: Guest paths put on the guest's sys.path after the stdlib zip, in order: each a folder or a zip file inside a read-only grant. The root, a writable grant or a mount may not serve one, no writable grant or mount may lie under one, and no writable grant or root may lie inside the grant serving one.
  • env: Environment variables laid over the guest's own: TMPDIR=/tmp, PYTHONTZPATH= empty (zoneinfo reads the zip's tzdata), and a HOME made fresh for the session where /tmp is writable, else an empty one the guest cannot write.
  • host: The door's host function: called with each request's bytes, it returns the response's. It must answer synchronously: an async one raises TypeError. Excludes tools.
  • snapshot: True runs the snapshot binary, with the stdlib's bootstrap imported (no package); False the plain reactor.
  • fuel: Each turn's cap on the guest's executed instructions, unless the turn passes its own; past it the turn ends "out of fuel", which ends the session. A session given a number is metered; None (the default) is unmetered, and its turns report no fuel.
  • deadline_s: Each turn's wall-clock seconds, unless the turn passes its own; past them the turn ends "deadline". Default: defaults().deadline_s, 60. None means no limit but the lifetime's.
  • memory_bytes: The most linear memory the guest may hold, in bytes, for the whole session. Default: defaults().memory_bytes, 1 GiB. None means no limit.
  • output_bytes: The most bytes kept per stream in each turn, unless the turn passes its own; the rest are dropped and the stream named in truncated. Default: defaults().output_bytes, 16 MiB. None means no limit.
  • cache_dir: Where compiled modules are kept as .cwasm files, a folder only you and the runner write: never one a guest is granted, in this or another call. None or True uses $WASI_PYTHON_CACHE, else the user's cache folder; False keeps them in this process only, never on disk.
  • artifacts: The artifact set to run; None finds it with artifacts() and checks it against its SHA256SUMS first. A set you pass is used as you give it.
  • max_file_calls: The most provider calls each turn with mounts makes; then it ends "out of file calls". Default: defaults().file_calls, 10,000. None means no limit.
  • tools: Host functions offered to the guest through the tool protocol (from host import tools), each a wasi_python.tools.Tool or a plain function, which goes through tool(). A tool may be async: under run it is awaited on one loop of the session's own for all its turns, under run_async on the event loop that awaits it. Fixed for the session. Excludes host.
  • gate: Called with a ToolCall before each tool runs. It allows by returning None or True, and refuses by returning False or raising ToolDenied. It may be async, as a tool may. Needs tools.
  • max_tool_calls: The most tool-protocol frames each turn answers; the guest gets ToolBudgetExceeded for the rest. Default: defaults().tool_calls, 1,000. None means no limit. Needs tools.
  • max_tool_request_bytes: The most bytes of tool-protocol requests each turn reads, counted from each frame's length before it is read; a frame that would pass it is refused unread. Default: defaults().tool_request_bytes, 16 MiB. None means no limit. Needs tools.
  • max_open_files: The most files and folders the guest holds open at once, in grants, the root and mounts alike, counted across the session's turns and its ending, since its store keeps them; past it an open answers EMFILE and the guest runs on. 0 refuses every open, the stdlib's included. It protects your process only within the process's own soft descriptor limit. Default: defaults().open_files, 1,024. None means no limit.
  • idle_s: The longest the session may rest between turns; then it ends "idle". Default: defaults().session_idle_s, 900. None means no bound.
  • lifetime_s: How long after the opening the session's turns may run: no turn starts at or after it, and each turn's deadline is capped at what is left; then it ends "lifetime". Default: defaults().session_lifetime_s, 3,600. None means no bound.
  • shutdown_s: The seconds the session's ending has to run Python's shutdown, capped so it ends by the opening plus the lifetime plus this; past it the session is stopped. Default: defaults().session_shutdown_s, 5. 0 skips the shutdown; None is refused.
  • Session.__enter__: Enter a with block; the session is closed when the block is left.
  • Session.__exit__: Close the session as the with block is left, however it is left.
  • exc: The exception leaving the block, if any; it is never suppressed.
  • Session.ended: None while the session can run turns; once it cannot, why: "closed", "idle", "lifetime", "quit" (a turn called exit() or quit()), "exit" (a turn called os._exit), "error" (a turn raised in your process), or the stopped turn's trapped. Set once, and never changed.
  • Session.shutdown_outcome: None until the session's ending has finished; then "finished" (Python's shutdown ran to its end), "stopped" (its budget ran out, or it trapped), or "skipped" (no shutdown ran: the session was stopped mid-turn, no turn ever ran, shutdown_s was 0, or, on Linux, wasmtime's trap handlers were replaced after the first run). close() waits for the ending.
  • Session.run: Run one turn of code in the session and return how it ended.
  • code: The turn's source, a str.
  • fuel: This turn's cap on executed instructions in a metered session; None sets no cap. Default: the session's.
  • deadline_s: This turn's wall-clock seconds; None means no limit but the lifetime's. Default: the session's.
  • output_bytes: The most bytes kept per stream in this turn. Default: the session's.
  • cancel: A threading.Event. Once it is set and the guest runs, the turn stops within 5 ticks of 10 ms while it computes and ends "cancelled", which ends the session; set before the guest runs, the turn runs nothing and the session goes on.
  • Session.run_async: run from async code, on a worker thread, so the event loop is never blocked.
  • code: The turn's source, a str.
  • fuel: This turn's cap on executed instructions in a metered session; None sets no cap. Default: the session's.
  • deadline_s: This turn's wall-clock seconds; None means no limit but the lifetime's. Default: the session's.
  • output_bytes: The most bytes kept per stream in this turn. Default: the session's.
  • executor: Where the turn runs: a concurrent.futures.Executor that runs callables on threads of this process. None uses the runner's own pool.
  • Session.close: End the session, run its ending, and return once its store is closed.

wasi_python.SessionEnded

class SessionEnded(RuntimeError):
    ...

A session has ended, so it runs no more turns.

wasi_python.Stat

@dataclass(frozen=True)
class Stat:
    kind: str
    size: int = 0
    mtime_ns: int = 0

What a provider's stat answers for a path.

Attributes:

  • kind: "file" or "dir".
  • size: The size in bytes, 0 to 2**63 - 1. A bool is refused.
  • mtime_ns: The modification time in nanoseconds, 0 to 2**64 - 1. A bool is refused.

wasi_python.WritableFileProvider

class WritableFileProvider(FileProvider):
    def write(
        self,
        path: str,
        data: bytes,
    ) -> None | Errno
    def mkdir(self, path: str) -> None | Errno
    def remove(self, path: str) -> None | Errno
    def rmdir(self, path: str) -> None | Errno
    def rename(
        self,
        src: str,
        dst: str,
    ) -> None | Errno

What a writable mount's provider answers, for type checkers: the read half and the write half.

Methods:

  • WritableFileProvider.write: Replace a file's whole content, creating the file if it is missing.
  • path: An absolute path within the mount.
  • data: The file's new content, a copy the provider may keep.
  • WritableFileProvider.mkdir: Make a folder.
  • path: An absolute path within the mount.
  • WritableFileProvider.remove: Remove a file; EISDIR for a folder.
  • path: An absolute path within the mount.
  • WritableFileProvider.rmdir: Remove a folder; ENOTEMPTY for one that holds anything.
  • path: An absolute path within the mount.
  • WritableFileProvider.rename: Move a file or folder within the mount, by POSIX's rule for replacing dst.
  • src: The absolute path, within the mount, of what moves.
  • dst: The absolute path, within the mount, it moves to.

wasi_python.artifact_dir

def artifact_dir() -> Path

The folder holding the artifact set.

Raises:

  • ArtifactsMissing: None of the three is there.

wasi_python.artifacts

def artifacts() -> Artifacts

The artifact set's paths, in the folder artifact_dir() names.

Raises:

  • ArtifactsMissing: There is no artifact set, or its folder has no MANIFEST; the message says how to get one.

wasi_python.defaults

def defaults() -> Defaults

The Defaults a call reads now: the limits it has for each one it does not name.

wasi_python.run

def run(
    argv: Iterable[str],
    *,
    root: str | os.PathLike[str] | bool | None = False,
    grants: Iterable[Grant] = (),
    mounts: Iterable[Mount] = (),
    import_path: Iterable[str] = (),
    env: Mapping[str, str] | None = None,
    stdin: bytes | bytearray | memoryview | None = None,
    host: Callable[[bytes], bytes | bytearray | memoryview] | None = None,
    snapshot: bool = True,
    fuel: int | None = None,
    deadline_s: float | None = <default>,
    memory_bytes: int | None = <default>,
    output_bytes: int | None = <default>,
    cache_dir: str | os.PathLike[str] | None | bool = None,
    artifacts: Artifacts | None = None,
    cancel: threading.Event | None = None,
    max_file_calls: int | None = <default>,
    tools: Iterable[Tool | Callable[..., Any]] | None = None,
    gate: Callable[[ToolCall], Any] | None = None,
    max_tool_calls: int | None = <default>,
    max_tool_request_bytes: int | None = <default>,
    max_open_files: int | None = <default>,
) -> Completed

Run one program in a fresh instance of the runtime and return how it ended.

Args:

  • argv: The guest's arguments after the program's name, as for python: ["-c", code], ["script.py"] or ["-m", "module"].
  • root: The host folder the guest sees as /, writable, made if missing. False or None (the default) gives no / and nothing writable; True gives a fresh private folder, removed after the call.
  • grants: Host folders the guest sees at their guest paths, each a Grant, read-only unless it says writable=True. The stdlib zip is always granted, read-only, at /usr/local/lib.
  • mounts: Providers whose files the guest sees at their guest paths, each a Mount, served from your code, read-only unless it says writable=True.
  • import_path: Guest paths put on the guest's sys.path after the stdlib zip, in order: each a folder or a zip file inside a read-only grant. The root, a writable grant or a mount may not serve one, no writable grant or mount may lie under one, and no writable grant or root may lie inside the grant serving one.
  • env: Environment variables laid over the guest's own: TMPDIR=/tmp, PYTHONTZPATH= empty (zoneinfo reads the zip's tzdata), and a HOME made fresh for the call where /tmp is writable, else an empty one the guest cannot write.
  • stdin: The guest's standard input, whole; None is empty.
  • host: The door's host function: called with each request's bytes, on the thread that made the call, it returns the response's. It must answer synchronously: an async one raises TypeError. Excludes tools.
  • snapshot: True runs the snapshot binary, with the stdlib's bootstrap imported (no package); False the plain reactor.
  • fuel: A cap on the guest's executed instructions; past it the call ends "out of fuel". None (the default) sets no cap.
  • deadline_s: Wall-clock seconds; past them the call ends "deadline". Default: defaults().deadline_s, 60. None means no limit.
  • memory_bytes: The most linear memory the guest may hold, in bytes. Default: defaults().memory_bytes, 1 GiB. None means no limit.
  • output_bytes: The most bytes kept per stream; the rest are dropped and the stream named in truncated. Default: defaults().output_bytes, 16 MiB. None means no limit.
  • cache_dir: Where compiled modules are kept as .cwasm files, a folder only you and the runner write: never one a guest is granted, in this or another call. None or True uses $WASI_PYTHON_CACHE, else the user's cache folder; False keeps them in this process only, never on disk.
  • artifacts: The artifact set to run; None finds it with artifacts() and checks it against its SHA256SUMS first. A set you pass is used as you give it.
  • cancel: A threading.Event. Once it is set and the guest runs, the guest stops within 5 ticks of 10 ms while it computes, and the call ends "cancelled".
  • max_file_calls: The most provider calls a call with mounts makes; then it ends "out of file calls". Default: defaults().file_calls, 10,000. None means no limit.
  • tools: Host functions offered to the guest through the tool protocol (from host import tools), each a wasi_python.tools.Tool or a plain function, which goes through tool(). A tool may be async (an async def, or a function returning an awaitable): it is awaited on the event loop that awaits run_async, or, under run, on a loop of the call's own; it never starts once its call has been answered, and is cancelled when its call ends. Excludes host.
  • gate: Called with a ToolCall before each tool runs. It allows by returning None or True, and refuses by returning False or raising ToolDenied. It may be async, as a tool may. Needs tools.
  • max_tool_calls: The most tool-protocol frames a call answers; the guest gets ToolBudgetExceeded for the rest. Default: defaults().tool_calls, 1,000. None means no limit. Needs tools.
  • max_tool_request_bytes: The most bytes of tool-protocol requests a call reads, counted from each frame's length before it is read, whatever its answer; a frame that would pass it is refused unread, and the guest gets ToolBudgetExceeded and runs on. Default: defaults().tool_request_bytes, 16 MiB. None means no limit. Needs tools.
  • max_open_files: The most files and folders the guest holds open at once, in grants, the root and mounts alike; past it an open answers EMFILE and the guest runs on. 0 refuses every open, the stdlib's included. It protects your process only within the process's own soft descriptor limit. Default: defaults().open_files, 1,024. None means no limit.

Raises:

  • TypeError: An argument has the wrong type; raised before any work.
  • ValueError: An argument is out of range; raised before any work.
  • ArtifactsMissing: There is no usable artifact set.
  • ImportError: wasmtime-py cannot load; its own error is chained as the cause.
  • OSError: A host filesystem call failed while the root, the private folders, the zip's copy or the cache was prepared (its subclasses, such as FileExistsError, included).
  • RuntimeError: This process was forked after it ran a guest; a folder or the zip's copy changed between the runner's check and its use; on Linux, a handler installed or removed after the first run (faulthandler, signal.signal, a native crash reporter) replaced wasmtime's trap handlers, so a guest trap would end the process; the guest's thread could not start, or has less stack than its 8 MiB wasm stack needs, or its stack cannot be read on this platform; this is CPython 3.14.0 on a C library other than glibc; or wasmtime-py cannot set its async stack size; raised before the guest runs.
  • wasmtime.WasmtimeError: Today, for a folder removed between the runner's check and wasmtime's own open. The type is wasmtime-py's and is not promised.
  • KeyboardInterrupt: Ctrl-C.
  • BaseException: One that is not an Exception, raised by your own host, provider, tool or gate, propagates.

wasi_python.run_async

async def run_async(
    argv: Iterable[str],
    *,
    root: str | os.PathLike[str] | bool | None = False,
    grants: Iterable[Grant] = (),
    mounts: Iterable[Mount] = (),
    import_path: Iterable[str] = (),
    env: Mapping[str, str] | None = None,
    stdin: bytes | bytearray | memoryview | None = None,
    host: Callable[[bytes], bytes | bytearray | memoryview] | None = None,
    snapshot: bool = True,
    fuel: int | None = None,
    deadline_s: float | None = <default>,
    memory_bytes: int | None = <default>,
    output_bytes: int | None = <default>,
    cache_dir: str | os.PathLike[str] | None | bool = None,
    artifacts: Artifacts | None = None,
    executor: concurrent.futures.Executor | None = None,
    max_file_calls: int | None = <default>,
    tools: Iterable[Tool | Callable[..., Any]] | None = None,
    gate: Callable[[ToolCall], Any] | None = None,
    max_tool_calls: int | None = <default>,
    max_tool_request_bytes: int | None = <default>,
    max_open_files: int | None = <default>,
) -> Completed

run from async code, on a worker thread, so the event loop is never blocked.

run's arguments except cancel, plus:

  • executor: Where the call runs: a concurrent.futures.Executor that runs callables on threads of this process (a process or interpreter pool is refused). None (the default) uses the runner's own pool of defaults().async_workers threads.

Raises:

  • TypeError: An argument has the wrong type; raised before any work.
  • ValueError: An argument is out of range; raised before any work.
  • ArtifactsMissing: There is no usable artifact set.
  • ImportError: wasmtime-py cannot load; its own error is chained as the cause.
  • OSError: A host filesystem call failed while the root, the private folders, the zip's copy or the cache was prepared (its subclasses, such as FileExistsError, included).
  • RuntimeError: This process was forked after it ran a guest; the interpreter is shutting down; a folder or the zip's copy changed between the runner's check and its use; on Linux, a handler installed or removed after the first run replaced wasmtime's trap handlers; or the guest's thread is refused (as run).
  • wasmtime.WasmtimeError: Today, for a folder removed between the runner's check and wasmtime's own open. The type is wasmtime-py's and is not promised.
  • asyncio.CancelledError: The awaiting task was cancelled; the guest was stopped and its worker freed first.
  • BaseException: One that is not an Exception, raised by your own host, provider, tool or gate, propagates.

wasi_python.set_defaults

def set_defaults(new: Defaults) -> Defaults

Replace the Defaults that calls read, and return the previous ones.

Args:

  • new: The limits calls read from now on, already checked by Defaults.

Raises:

  • TypeError: new is not a Defaults.

wasi_python.stats

def stats() -> dict[str, int]

Counts kept by this process, a diagnostic.

wasi_python.tools

wasi_python.tools.PROTOCOL

PROTOCOL = 1

PROTOCOL is the number of the protocol this host speaks. The rules for its frames, the schema subset and the bounds are the protocol's own, not the host Python's, so an answer is the same on every host; they change only with PROTOCOL (README.md, "The tool protocol").

wasi_python.tools.Tool

@dataclass(frozen=True, eq=False)
class Tool:
    name: str
    description: str
    fn: Callable[..., Any]
    validates_input: bool = False
    timeout_s: float | None = None
    read_only: bool = False
    destructive: bool = False
    idempotent: bool = False
    open_world: bool = False
    @property
    def input_schema(self) -> dict
    @property
    def output_schema(self) -> dict | None
    def descriptor(self) -> dict
    def __call__(
        self,
        *args: Any,
        **kwargs: Any,
    ) -> Any

A host function offered to the guest, made only by tool(). Immutable; compared and hashed by identity. Calling it calls the function.

Attributes:

  • name: The name the guest calls it by.
  • description: What the guest is told it does.
  • fn: The function it calls.
  • validates_input: Whether schema keywords outside the protocol's subset are let through, unchecked, for a tool that checks its own input.
  • timeout_s: The longest one call may run, in seconds; None for no bound beyond the run's own.
  • read_only: A label shown to the guest as MCP's readOnlyHint; it grants nothing.
  • destructive: A label shown as MCP's destructiveHint; it grants nothing.
  • idempotent: A label shown as MCP's idempotentHint; it grants nothing.
  • open_world: A label shown as MCP's openWorldHint; it grants nothing.

Methods:

  • Tool.input_schema: A copy of the vetted input schema.
  • Tool.output_schema: A copy of the output schema, or None. It is shown, never validated.
  • Tool.descriptor: The tool in MCP's field names: name, description, inputSchema, and outputSchema and annotations when there are any. A fresh copy each time.
  • Tool.__call__: Call the function with these arguments, on this thread, unchecked.

wasi_python.tools.ToolCall

@dataclass(frozen=True)
class ToolCall:
    seq: int
    tool: Tool
    arguments: dict[str, Any]
    request_bytes: int

What a gate sees of one tool call.

Attributes:

  • seq: The frame's number in this call, from 1.
  • tool: The tool the guest called.
  • arguments: A deep copy of the validated and decoded arguments; the tool gets its own.
  • request_bytes: The frame's length in bytes.

wasi_python.tools.ToolDenied

class ToolDenied(ToolError):
    ...

Raised by a gate or a capability to refuse a call; its message crosses (denied).

wasi_python.tools.ToolError

class ToolError(Exception):
    ...

Raised by a tool to fail with a message the guest reads (tool_error), or by a gate to refuse with one (denied). Any other exception crosses only as "(internal error)".

wasi_python.tools.ToolOutcome

@dataclass(frozen=True)
class ToolOutcome:
    seq: int
    op: str | None
    tool: str | None
    outcome: str
    message: str | None
    request_bytes: int
    response_bytes: int
    fault: str | None = None
    abandoned: bool = False

One frame, as the record lists it. Nothing in it measures time.

Attributes:

  • seq: The frame's number in this call, from 1.
  • op: "list", "call", or None when the frame was not read.
  • tool: The tool's name, or the name the guest gave, cut to 64 characters.
  • outcome: "ok", or one of the protocol's kinds of failure (README.md, "The tool protocol").
  • message: What the guest was told on a failure.
  • request_bytes: The frame's length in bytes.
  • response_bytes: The answer's length in bytes.
  • fault: An exception not meant for the guest: its type and first line.
  • abandoned: Whether the tool still ran when the guest was answered.

wasi_python.tools.ToolRecord

@dataclass(frozen=True)
class ToolRecord:
    frames: int
    outcomes: tuple[ToolOutcome, ...]
    unlisted: int
    abandoned: int = 0

Every frame of one call counted, the first 1,000 listed.

Attributes:

  • frames: Every frame the guest sent.
  • outcomes: The first 1,000 frames, each a ToolOutcome.
  • unlisted: How many frames are counted but not listed.
  • abandoned: Every call answered while its tool ran on, listed or not, so a guest cannot hide a tool still running with your authority behind 1,000 cheap frames.

wasi_python.tools.stubs

def stubs(tools: Iterable[Tool | Callable[..., Any]]) -> str

Python stubs for a prompt: each tool as a def with the signature and docstring the guest's function carries, under a first line from host import tools.

Args:

  • tools: The tools, each a Tool or a plain function, which goes through tool().

Raises:

  • TypeError: A plain function tool() refuses.
  • ValueError: A plain function tool() refuses.

wasi_python.tools.tool

def tool(
    fn: Callable[..., Any] | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    input_schema: Any = None,
    validates_input: bool = False,
    timeout_s: float | None = None,
    read_only: bool = False,
    destructive: bool = False,
    idempotent: bool = False,
    open_world: bool = False,
) -> Any
@overload
def tool(
    fn: Callable[..., Any],
    /,
    *,
    name: str | None = None,
    description: str | None = None,
    input_schema: Any = None,
    validates_input: bool = False,
    timeout_s: float | None = None,
    read_only: bool = False,
    destructive: bool = False,
    idempotent: bool = False,
    open_world: bool = False,
) -> Tool
@overload
def tool(
    fn: None = None,
    /,
    *,
    name: str | None = None,
    description: str | None = None,
    input_schema: Any = None,
    validates_input: bool = False,
    timeout_s: float | None = None,
    read_only: bool = False,
    destructive: bool = False,
    idempotent: bool = False,
    open_world: bool = False,
) -> Callable[[Callable[..., Any]], Tool]

Describe a function as a tool: @tool, @tool(...) with arguments, or tool(fn, ...).

Args:

  • fn: The function. Without it, tool(...) returns a decorator.
  • name: The name the guest calls it by; by default the function's own.
  • description: What the guest is told it does; by default the docstring up to its first section heading.
  • input_schema: The input schema instead of the derived one: a dict whose root is an object, or anything with model_json_schema().
  • validates_input: Let schema keywords outside the protocol's subset through, unchecked, for a tool that checks its own input.
  • timeout_s: The longest one call may run, in seconds; None for no bound beyond the run's own.
  • read_only: Shown to the guest as MCP's readOnlyHint.
  • destructive: Shown to the guest as MCP's destructiveHint.
  • idempotent: Shown to the guest as MCP's idempotentHint.
  • open_world: Shown to the guest as MCP's openWorldHint.

Raises:

  • TypeError: An argument has the wrong type, the function is an async generator function, or a type hint has no schema.
  • ValueError: A name or a schema the protocol does not allow.

The command

wasi-python run [OPTIONS] [--] ARG... (also python -m wasi_python):

option value action
--env KEY[=VALUE] append
--stdin PATH store
--deadline-s SECONDS store
--memory-bytes BYTES store
--output-bytes BYTES store
--fuel N store
--max-open-files N store
--no-snapshot store_true
--grant GUEST=HOST append
--grant-writable GUEST=HOST append
--import-path GUEST append
--root HOST once
--private-root store_true
--no-root store_true

Exit codes: 0, a result; 1, run raised; 2, a usage error; 130, interrupted.

One line of JSON: on a result, exit_code, fuel_used, stderr, stderr_base64, stdout, stdout_base64, trapped, truncated (a stream that is not UTF-8 comes as its _base64 key); on exit 1, error, kind.