---
title: Python
description: Choose the interpreter that runs python3 inside the workspace, monty (sandboxed, default), wasi (sandboxed full CPython), or the host interpreter.
icon: /images/pydantic-logo.svg
---

Shell lines like `python3 script.py` need an interpreter. Mirage calls that
interpreter a **runtime**, and you pick it per workspace. Three runtimes
ship today:

| Runtime | Engine | Filesystem | Default |
| --- | --- | --- | --- |
| `monty` | [Pydantic Monty](https://github.com/pydantic/monty), a sandboxed Python interpreter written in Rust | Workspace mounts only, bridged through Mirage | Yes |
| `wasi` | Full CPython compiled to WebAssembly, run in-process on [wasmtime](https://wasmtime.dev) | Workspace mounts only, bridged through Mirage; no host filesystem, no network | No |
| `local` | The host `python` interpreter, as a subprocess | The host filesystem | No |

## Monty (default)

Monty is a minimal, secure Python interpreter built for running
AI-generated code: microsecond startup, no host filesystem, environment,
or network access. Mirage wires Monty's OS interface to the workspace
dispatch, so code running under `python3` sees the workspace mounts, and
nothing else:

```python
from mirage import MountMode, Workspace
from mirage.resource.ram import RAMResource

ws = Workspace({"/data": RAMResource()}, mode=MountMode.EXEC)
await ws.execute("echo hello > /data/a.txt")

# Reads a virtualized file; /etc/passwd would raise FileNotFoundError.
io = await ws.execute(
    "python3 -c \"print(open('/data/a.txt').read().strip())\"")
```

Reads, writes, `pathlib` traversal, and `os.environ` all route through the
mounts. A `python3` write is immediately visible to `cat`, and works the
same whether the mount is RAM, Redis, S3, or a virtual backend like
MongoDB's `database.json`.

Monty requires the `monty` extra:

```bash
pip install mirage-ai[monty]
```

### Concurrency

Runs execute on Monty's own worker pool and are fully async: the event
loop stays free, and cancelling a run halts the interpreter. One
ceiling to know about when a server runs many I/O-heavy scripts at
once: each run waiting on workspace I/O holds one pool worker (the
Python binding only takes synchronous OS callbacks,
[pydantic/monty#560](https://github.com/pydantic/monty/issues/560)),
and the pool defaults to the machine's core count. Set
`TOKIO_WORKER_THREADS` to lift it; waiting workers cost kilobytes of
stack and no CPU, so 100 parallel I/O-bound scripts on one core is a
`TOKIO_WORKER_THREADS=100` away.

Without it, `python3` exits with code 127 and an install hint; every other
command keeps working.

### Differences from CPython

Monty implements a Python subset. Notably:

- Command-line arguments are the `argv` global (`argv[0]` is the script
  name); `sys.argv` does not exist.
- No `sys.stdin`: piping into `python3` needs the `wasi` or `local` runtime.
- No third-party imports (`numpy`, `requests`, ...).
- File handles are not iterable (`for line in f` raises `TypeError`);
  `f.read()`, `f.readline()`, and `f.readlines()` all work.
- `os.environ` reflects the session env only, never the host's.

## WASI

The `wasi` runtime runs a real CPython, compiled to WebAssembly, inside
your process on wasmtime. It fills monty's gaps (classes, the complete
stdlib, `sys.argv`, `sys.stdin`) while staying sandboxed: host files
and network sockets are invisible, and the run sees the interpreter's
own build directory plus the workspace mounts.

Mirage intercepts the sandbox's filesystem calls and bridges them
through the workspace dispatch, so `open('/data/f.txt')` inside the
code reads and writes the mount live — RAM, Redis, S3, or a virtual
backend — with the same cache, write modes, and per-session mount
narrowing as `cat`. Guest writes are immediately visible to every
command, a read-only mount (or a session narrowed to read) answers
`PermissionError` at `open()`, and none of this needs FUSE, a config
key, or any setup. The interpreter's own build directory is served
read-only from the host; a mount at `/` coexists with it (mount
prefixes win for their subtrees, the build serves the rest).

It needs the `wasi` extra plus a CPython WASI build (download and
unzip a release from
[cpython-wasi-build](https://github.com/brettcannon/cpython-wasi-build/releases)):

```bash
pip install mirage-ai[wasi]
```

Point mirage at the build directory with the `home` option on the
runtime entry (or the MIRAGE_WASI_HOME environment variable):

```python
from mirage.runtime.python import WasiRuntime

ws = Workspace({"/data": RAMResource()},
               mode=MountMode.EXEC,
               runtimes=[WasiRuntime(home="/path/to/unzipped/build"), "vfs"])
```

```yaml
runtimes:
  - name: wasi
    home: /path/to/unzipped/build
  - vfs
```

The first run compiles `python.wasm` (a few hundred milliseconds) and
caches the compilation as `python.cwasm` next to it; runs after that
boot in milliseconds. The build directory is read-only inside the
sandbox: guest writes land in the workspace or answer `PermissionError`,
never in the bundle.

## Local

The `local` runtime runs a host interpreter as a subprocess: full
CPython, host filesystem, host environment. Use it when the code needs
stdin, third-party packages, or host files:

```python
ws = Workspace({"/data": RAMResource()},
               mode=MountMode.EXEC,
               runtimes=["local", "vfs"])
```

It defaults to the interpreter running mirage; point the `home` option
on the runtime entry (or the MIRAGE_LOCAL_HOME environment variable)
at another binary, e.g. a project venv whose packages the code needs:

```yaml
runtimes:
  - name: local
    home: /path/to/venv/bin/python
  - vfs
```

Note that `local` code does **not** see workspace mounts; it sees the real
host filesystem.

## Selecting in YAML

Workspace config files take a top-level `runtimes` list: the ordered
world of runtimes, each entry a name or a mapping with the name plus
constructor options flat on the entry. The first entry that captures a
command binds it; `vfs` is the in-process catch-all:

```yaml
runtimes:
  - monty         # or: wasi, local
  - name: wasi    # options live flat on the entry
    home: /opt/cpython-wasi
  - vfs

mounts:
  /data:
    resource: ram
    command_safeguards:
      python3:
        timeout_seconds: 30
```

Each runtime can have its own option block under its name. The `home`
option locates the runtime's interpreter or distribution, in the
spirit of JAVA_HOME: a build directory for `wasi`, an interpreter path
for `local`, a distribution URL for `pyodide` (TypeScript); it also
falls back to a MIRAGE_&lt;RUNTIME&gt;_HOME environment variable. Only
the selected runtime consumes its block and it rejects option keys it
does not know, so one config stays portable across runtimes and
languages. `monty` embeds its interpreter and has no options yet.

## Resource limits

`python3` is a command like any other: the same `command_safeguards`
blocks that guard `cat` or `grep` guard it, enforced at the same
central point. A run that exceeds `timeout_seconds` answers with exit
124 and `python3: timed out after Ns` on stderr, exactly like any
other command; `max_bytes` and `max_lines` cap its output the same
way. There is no python3-specific limit surface.

Firing the guard also cancels the run, and every runtime honors
cancellation: monty halts its interpreter, wasi traps the run
through wasmtime's epoch interruption, and local kills the
subprocess. A timed-out `python3` run is reclaimed at the deadline,
not left burning in the background.
