pydantic-monty 0.0.19


pip install pydantic-monty

  Latest version

Released: Jul 24, 2026

Project Links

Meta
Requires Python: >=3.10

Classifiers

Development Status
  • 3 - Alpha

Programming Language
  • Python :: Implementation

Intended Audience
  • Developers
  • Information Technology
  • System Administrators

License
  • OSI Approved :: MIT License

Operating System
  • Unix
  • POSIX :: Linux

Environment
  • MacOS X

Topic
  • Software Development :: Libraries :: Python Modules
  • Internet

pydantic-monty

Python bindings for the Monty sandboxed Python interpreter.

Execution always happens in a pool of monty worker subprocesses: a monty process can never be made fully crash-proof against memory errors (stack overflows, allocator aborts) triggered by adversarial input, so crash isolation is built in. A crashed worker raises MontyCrashedError and is replaced transparently — your process is never at risk.

Installation

pip install pydantic-monty

This installs the monty worker binary via the pydantic-monty-runtime dependency (the same way uv and ruff ship their binaries).

Usage

Basic execution

from pydantic_monty import Monty

with Monty() as pool:
    with pool.checkout() as session:
        print(session.feed_run('1 + 2'))
        #> 3

Monty() is a pool of workers; pool.checkout() dedicates one worker to a REPL session. Session state persists across feed_run calls:

from pydantic_monty import Monty

with Monty() as pool:
    with pool.checkout() as session:
        session.feed_run('x = 40')
        print(session.feed_run('x + 2'))
        #> 42

Async

AsyncMonty is the asyncio counterpart: worker I/O runs off the event loop, and external functions may be coroutines.

import asyncio

from pydantic_monty import AsyncMonty


async def fetch(url: str) -> str:
    await asyncio.sleep(0.01)
    return f'contents of {url}'


async def main():
    async with AsyncMonty() as pool:
        async with pool.checkout() as session:
            result = await session.feed_run(
                "await fetch('https://example.com')",
                external_lookup={'fetch': fetch},
            )
    print(result)
    #> contents of https://example.com


asyncio.run(main())

Input variables and external lookup

from pydantic_monty import Monty

with Monty() as pool:
    with pool.checkout() as session:
        result = session.feed_run(
            'double(x) + y',
            inputs={'x': 5, 'y': 1},
            external_lookup={'double': lambda x: x * 2},
        )
    print(result)
    #> 11

Snapshots: pausing and resuming execution

feed_start is the suspendable counterpart of feed_run: instead of driving a snippet to completion, it hands control back at each external call, OS call, name lookup, or future resolution as a snapshot. You answer with snapshot.resume(...), which returns the next snapshot or a MontyComplete.

from pydantic_monty import FunctionSnapshot, Monty, MontyComplete

with Monty() as pool:
    with pool.checkout() as session:
        snapshot = session.feed_start('greet(name) + "!"', inputs={'name': 'Ada'})
        assert isinstance(snapshot, FunctionSnapshot)
        print(snapshot.function_name, snapshot.args)
        #> greet ('Ada',)
        result = snapshot.resume({'return_value': 'hello Ada'})
        assert isinstance(result, MontyComplete)
        print(result.output)
        #> hello Ada!

To iterate a snippet to completion without answering each suspension by hand, pass an external_lookup (and/or os) to feed_start and drive with snapshot.resume_auto(), which resolves each external call and name lookup from them automatically — the same resolution feed_run performs, but one step at a time so you can inspect or dump() each snapshot along the way:

from pydantic_monty import Monty, MontyComplete

with Monty() as pool:
    with pool.checkout() as session:
        snapshot = session.feed_start(
            'greet(name) + "!"',
            inputs={'name': 'Ada'},
            external_lookup={'greet': lambda n: f'hello {n}'},
        )
        while not isinstance(snapshot, MontyComplete):
            snapshot = snapshot.resume_auto()
        print(snapshot.output)
        #> hello Ada!

On AsyncMonty, external_lookup callables may be coroutine functions and resume_auto is awaitable (snapshot = await snapshot.resume_auto()); a coroutine external is awaited concurrently and settled via an AsyncFutureSnapshot.

snapshot.dump() serializes the paused worker to bytes; a fresh session's load_snapshot restores it and returns the snapshot to resume. This lets you checkpoint execution and continue it later, even in a different process:

from pydantic_monty import FunctionSnapshot, Monty, MontyComplete

with Monty() as pool:
    with pool.checkout() as session:
        snapshot = session.feed_start(
            'fetch(url)', inputs={'url': 'https://example.com'}
        )
        blob = snapshot.dump()

    # later — restore into a fresh session and resume
    with pool.checkout() as session:
        snapshot = session.load_snapshot(blob)
        assert isinstance(snapshot, FunctionSnapshot)
        result = snapshot.resume({'return_value': 'page contents'})
        assert isinstance(result, MontyComplete)
        print(result.output)
        #> page contents

If the paused feed used filesystem mounts, re-supply the same ones to load_snapshot(blob, mount=...) — their host paths are not stored in the dump.

session.dump() between feeds serializes an idle session instead; restore it with session.load_session(blob) (which returns None) and keep feeding. Both load_session and load_snapshot are valid only on a fresh session, before any feed; using the wrong one for a dump's kind raises. AsyncMonty sessions expose the same feed_start / load_session / load_snapshot, with awaitable resume(...).

Resource limits

Limits are enforced inside the worker; the pool's request_timeout is a host-side backstop that kills a hung worker outright. max_duration_secs limits cumulative execution time — the clock runs only while the interpreter executes, never while suspended waiting on the host, and accumulates across feeds. The worker reports its execution time on every protocol turn, and sessions with the limit are additionally killed duration_limit_grace (1s, not currently configurable from Python) after the remaining budget expires, covering hangs the in-sandbox limit cannot catch (its check only runs at interpreter checkpoints).

from pydantic_monty import Monty, MontyRuntimeError

with Monty(request_timeout=10) as pool:
    with pool.checkout(limits={'max_duration_secs': 0.1}) as session:
        try:
            session.feed_run('while True:\n    pass')
        except MontyRuntimeError as exc:
            print(exc.display(format='type-msg').split(':')[0])
            #> TimeoutError

Type checking

Monty bundles ty: each fed snippet can be type-checked inside the worker before it runs, with successfully executed snippets accumulating into the checking context.

from pydantic_monty import Monty, MontyTypingError

with Monty() as pool:
    with pool.checkout(type_check=True) as session:
        try:
            session.feed_run("x: int = 'not an int'")
        except MontyTypingError as exc:
            print('invalid-assignment' in exc.display())
            #> True

Crash isolation

from pydantic_monty import Monty, MontyCrashedError

hostile_code = '...'

with Monty() as pool:
    with pool.checkout() as session:
        try:
            session.feed_run(hostile_code)  # even a segfault is contained
        except MontyCrashedError:
            ...  # the worker died; the pool already replaced it

See limitations/pool-architecture.md in the repository for the behavioural details of subprocess execution (host-side mounts, buffered print callbacks, session dumps).

Wheel compatibility matrix

Platform CPython 3.10 CPython 3.11 CPython 3.12 CPython 3.13 CPython 3.14
macosx_10_12_x86_64
macosx_11_0_arm64
manylinux_2_28_aarch64
manylinux_2_28_armv7l
manylinux_2_28_i686
manylinux_2_28_ppc64le
manylinux_2_28_s390x
manylinux_2_28_x86_64
musllinux_1_1_aarch64
musllinux_1_1_x86_64
win32
win_amd64

Files in release

pydantic_monty-0.0.19-cp310-cp310-macosx_10_12_x86_64.whl (2.4MiB)
pydantic_monty-0.0.19-cp310-cp310-macosx_11_0_arm64.whl (2.2MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_aarch64.whl (2.2MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_armv7l.whl (1.9MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_i686.whl (2.1MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_ppc64le.whl (2.2MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_s390x.whl (2.1MiB)
pydantic_monty-0.0.19-cp310-cp310-manylinux_2_28_x86_64.whl (2.3MiB)
pydantic_monty-0.0.19-cp310-cp310-musllinux_1_1_aarch64.whl (2.4MiB)
pydantic_monty-0.0.19-cp310-cp310-musllinux_1_1_x86_64.whl (2.6MiB)
pydantic_monty-0.0.19-cp310-cp310-win32.whl (1.8MiB)
pydantic_monty-0.0.19-cp310-cp310-win_amd64.whl (2.0MiB)
pydantic_monty-0.0.19-cp311-cp311-macosx_10_12_x86_64.whl (2.4MiB)
pydantic_monty-0.0.19-cp311-cp311-macosx_11_0_arm64.whl (2.1MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_aarch64.whl (2.2MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_armv7l.whl (1.9MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_i686.whl (2.1MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_ppc64le.whl (2.2MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_s390x.whl (2.1MiB)
pydantic_monty-0.0.19-cp311-cp311-manylinux_2_28_x86_64.whl (2.3MiB)
pydantic_monty-0.0.19-cp311-cp311-musllinux_1_1_aarch64.whl (2.4MiB)
pydantic_monty-0.0.19-cp311-cp311-musllinux_1_1_x86_64.whl (2.6MiB)
pydantic_monty-0.0.19-cp311-cp311-win32.whl (1.8MiB)
pydantic_monty-0.0.19-cp311-cp311-win_amd64.whl (2.0MiB)
pydantic_monty-0.0.19-cp312-cp312-macosx_10_12_x86_64.whl (2.4MiB)
pydantic_monty-0.0.19-cp312-cp312-macosx_11_0_arm64.whl (2.1MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_aarch64.whl (2.2MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_armv7l.whl (1.9MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_i686.whl (2.1MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_ppc64le.whl (2.2MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_s390x.whl (2.1MiB)
pydantic_monty-0.0.19-cp312-cp312-manylinux_2_28_x86_64.whl (2.2MiB)
pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_aarch64.whl (2.4MiB)
pydantic_monty-0.0.19-cp312-cp312-musllinux_1_1_x86_64.whl (2.6MiB)
pydantic_monty-0.0.19-cp312-cp312-win32.whl (1.8MiB)
pydantic_monty-0.0.19-cp312-cp312-win_amd64.whl (2.0MiB)
pydantic_monty-0.0.19-cp313-cp313-macosx_10_12_x86_64.whl (2.4MiB)
pydantic_monty-0.0.19-cp313-cp313-macosx_11_0_arm64.whl (2.1MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_aarch64.whl (2.2MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_armv7l.whl (1.9MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_i686.whl (2.1MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_ppc64le.whl (2.2MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_s390x.whl (2.1MiB)
pydantic_monty-0.0.19-cp313-cp313-manylinux_2_28_x86_64.whl (2.2MiB)
pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_aarch64.whl (2.4MiB)
pydantic_monty-0.0.19-cp313-cp313-musllinux_1_1_x86_64.whl (2.6MiB)
pydantic_monty-0.0.19-cp313-cp313-win32.whl (1.8MiB)
pydantic_monty-0.0.19-cp313-cp313-win_amd64.whl (2.0MiB)
pydantic_monty-0.0.19-cp314-cp314-macosx_10_12_x86_64.whl (2.4MiB)
pydantic_monty-0.0.19-cp314-cp314-macosx_11_0_arm64.whl (2.1MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_aarch64.whl (2.2MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_armv7l.whl (1.9MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_i686.whl (2.1MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_ppc64le.whl (2.2MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_s390x.whl (2.1MiB)
pydantic_monty-0.0.19-cp314-cp314-manylinux_2_28_x86_64.whl (2.3MiB)
pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_aarch64.whl (2.4MiB)
pydantic_monty-0.0.19-cp314-cp314-musllinux_1_1_x86_64.whl (2.6MiB)
pydantic_monty-0.0.19-cp314-cp314-win32.whl (1.8MiB)
pydantic_monty-0.0.19-cp314-cp314-win_amd64.whl (2.0MiB)
pydantic_monty-0.0.19.tar.gz (1.4MiB)
Extras: None
Dependencies:
typing-extensions (>=4.5)
pydantic-monty-runtime (==0.0.19)