# How to Define and Register Custom External Functions in Monty: A Complete Guide

> Learn how to define and register custom external functions in Monty with this complete guide. Enhance your Monty workflows by easily integrating your own Python implementations.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: how-to-guide
- Published: 2026-02-16

---

**To define and register custom external functions in Monty, declare the function names when constructing the `Monty` instance, then supply the Python implementations via the `external_functions` parameter when calling `run()`, `start()`, or `create()`.**

Monty is a Python sandbox execution environment developed by Pydantic that isolates interpreted code from the host runtime. When you need to extend sandboxed code with host-side capabilities—such as database access, HTTP requests, or filesystem operations—you must define and register custom external functions that bridge the VM boundary safely.

## Understanding External Functions in Monty

Monty executes Python code in a sandbox and deliberately blocks direct access to the host runtime. When interpreted code calls a function whose name is listed in the external functions declaration, the VM pauses, yields a **FunctionCall** progress object, and requests the host to supply the actual implementation.

This two-stage process maintains security: the sandbox knows which names it may call, but the host decides what those names actually do at execution time.

## Declaring External Functions

The first step is declaring which function names the sandboxed code is permitted to call. Pass a list of strings to the `external_functions` parameter when creating a `Monty` instance:

```python
import pydantic_monty

m = pydantic_monty.Monty(
    code='my_func(42)', 
    external_functions=['my_func']   # Names that may be called

)

```

This declaration establishes the security boundary. Only functions listed here can trigger the external function bridge; any other calls remain internal to the sandbox.

## Registering Python Implementations

After declaration, you must register the actual Python callables when executing the code. Pass a dictionary mapping the declared names to functions via the `external_functions` parameter of `run()`, `start()`, or `create()`:

```python
result = m.run(
    external_functions={'my_func': my_impl}
)

```

The dictionary keys must exactly match the declared names. The values can be any callable accepting `*args` and `**kwargs`.

### Simple Synchronous Functions

For basic use cases, define a standard Python function:

```python
def greet(name: str) -> str:
    return f"Hello, {name}!"

m = pydantic_monty.Monty(
    code='greet("World")',
    external_functions=['greet']
)

result = m.run(external_functions={'greet': greet})
print(result)  # → "Hello, World!"

```

### Handling Positional and Keyword Arguments

The external function bridge automatically converts Monty arguments to Python objects. Your implementation receives both positional and keyword arguments:

```python
def combine(*args, **kwargs):
    # args = (1, 2), kwargs = {'sep': '-'}

    separator = kwargs.get('sep', ',')
    return separator.join(map(str, args))

m = pydantic_monty.Monty(
    code='combine(1, 2, sep="-")',
    external_functions=['combine']
)

print(m.run(external_functions={'combine': combine}))  # → "1-2"

```

### Grouping Functions with Classes

For complex applications, organize related external functions using classes. This pattern appears in [`examples/sql_playground/external_functions.py`](https://github.com/pydantic/monty/blob/main/examples/sql_playground/external_functions.py):

```python
from dataclasses import dataclass
from pathlib import PurePosixPath
from pydantic_monty import OSAccess

@dataclass
class ExternalFunctions:
    fs: OSAccess  # Monty-provided filesystem helper

    
    async def query_csv(self, filepath: PurePosixPath, sql: str, parameters: dict | None = None):
        """Execute SQL on a CSV stored in Monty's virtual filesystem."""
        content = self.fs.path_read_bytes(filepath)
        
        import tempfile, duckdb
        with tempfile.NamedTemporaryFile(mode='wb', suffix='.csv') as tmp:
            tmp.write(content)
            tmp.flush()
            conn = duckdb.connect(':memory:')
            conn.read_csv(tmp.name)
            result = conn.execute(sql, parameters)
            cols = [d[0] for d in result.description]
            rows = result.fetchall()
        return [dict(zip(cols, r)) for r in rows]

# Usage

extern = ExternalFunctions(fs=my_os_access)

m = pydantic_monty.Monty(
    code='query_csv("/data/file.csv", "SELECT * FROM data")',
    external_functions=['query_csv']
)

result = m.run(external_functions={'query_csv': extern.query_csv})

```

### Async External Functions with Iterative Execution

For asynchronous operations or long-running tasks, use the iterative API to pause execution and resume later:

```python
import asyncio
import pydantic_monty

async def async_fetch(url: str):
    await asyncio.sleep(0.1)  # Simulate I/O

    return f"data from {url}"

m = pydantic_monty.Monty(
    code='await fetch("https://example.com")',
    external_functions=['fetch']
)

# Start execution - pauses at external call and returns a snapshot

snapshot = m.start()

# Resolve the future and resume

final = snapshot.resume(return_value=async_fetch)
print(final)  # → "data from https://example.com"

```

## How the External Function Bridge Works

Understanding the internal mechanics helps debug complex integrations. According to the pydantic/monty source code, the bridge operates across Rust and Python layers:

1. **VM Detection**: When the Rust VM in [`monty/src/value.rs`](https://github.com/pydantic/monty/blob/main/monty/src/value.rs) encounters a call to a declared external function, it creates a `RunProgress::FunctionCall` and pauses execution.

2. **Registry Creation**: In [`crates/monty-python/src/monty_cls.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/monty_cls.rs), the `run_impl` method constructs an `ExternalFunctionRegistry` from the Python dictionary you provide (lines 69-89).

3. **Argument Conversion**: The registry in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs) (lines 19-27) converts Monty values to Python objects using `monty_to_py`, invokes your callable, then converts the result back via `py_to_monty`.

4. **GIL Management**: The VM releases the Python GIL while running sandboxed code, re-acquiring it only for the actual external function call. This preserves isolation while allowing CPU-bound Monty code to run concurrently with host-side logic.

## Error Handling and Exception Propagation

When external functions raise exceptions, Monty converts them to runtime errors inside the sandbox. As implemented in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs) (line 44), the `exc_py_to_monty` function wraps Python exceptions and re-raises them as `MontyRuntimeError` within the interpreted code, preserving the original exception type and message.

This allows sandboxed code to catch specific exceptions using standard Python error handling:

```python
def failing_function():
    raise ValueError("Something went wrong")

m = pydantic_monty.Monty(
    code='''
try:
    failing_function()
except ValueError as e:
    result = f"Caught: {e}"
''',
    external_functions=['failing_function']
)

result = m.run(external_functions={'failing_function': failing_function})

# result contains "Caught: Something went wrong"

```

## Summary

- **Declare first**: List external function names in the `external_functions` parameter when creating a `Monty` instance to establish the security boundary.
- **Register at runtime**: Supply implementations via the `external_functions` dictionary in `run()`, `start()`, or `create()`, mapping declared names to Python callables.
- **Flexible signatures**: External functions accept `*args` and `**kwargs` with automatic type conversion between Monty and Python values via `monty_to_py` and `py_to_monty`.
- **Async support**: Use the iterative API (`start()` and `resume()`) to handle asynchronous external functions or long-running operations.
- **Error propagation**: Python exceptions convert to `MontyRuntimeError` inside the sandbox while preserving original error details, allowing standard exception handling in interpreted code.

## Frequently Asked Questions

### Can I register external functions dynamically after creating the Monty instance?

No, you must declare the allowed external function names when constructing the `Monty` instance via the `external_functions` parameter. However, you can choose which Python callables to bind to those names at execution time when calling `run()` or `start()`. This two-stage approach maintains the security boundary while allowing flexible implementation swapping between runs.

### What types can be passed between Monty and external functions?

Monty automatically converts supported types between the sandbox and Python via `monty_to_py` and `py_to_monty` functions in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs). Supported types include primitives (int, float, str, bool), collections (list, dict, tuple), and dataclasses. Complex objects that cannot be serialized across the boundary will raise conversion errors.

### How do I handle exceptions raised by external functions?

When an external function raises a Python exception, Monty catches it in [`crates/monty-python/src/external.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rs) (line 44) using `exc_py_to_monty` and re-raises it as a `MontyRuntimeError` inside the sandboxed code. The original exception type and message are preserved, allowing sandboxed code to catch specific exception types using standard Python `try/except` blocks.

### Can external functions access the Monty virtual filesystem?

Yes, external functions can interact with Monty's virtual filesystem by accepting `OSAccess` objects as constructor arguments or parameters. As demonstrated in [`examples/sql_playground/external_functions.py`](https://github.com/pydantic/monty/blob/main/examples/sql_playground/external_functions.py), you can inject filesystem helpers into class-based external functions, allowing sandboxed code to read virtual files while the external function processes them using host-side libraries like DuckDB or Pandas.