Skip to content

Filesystem & OS

Mounting host directories into the sandbox, and handling the OS calls sandboxed code makes. See filesystem access for how these fit together.

MountDir

A mount point mapping a virtual path to a host directory.

The directory is opened here, and every feed this mount is passed to serves that same directory — so build one and reuse it. 'overlay' writes live in each feed’s own table and are discarded when the feed ends.

Warning: mode='read-write' writes files from untrusted code to your real filesystem.

Those files are untrusted input; do not execute them. Importing counts as executing, and the import can be indirect: with a directory on sys.path mounted, sandboxed code can write json.py, or any module not yet imported, and the next import runs it. That includes imports made by pydantic_monty itself. sys.path[0] is the script’s directory, or the cwd for python -m, python -c and the REPL.

Tools also read files without an explicit import: conftest.py, sitecustomize.py, .git/hooks/*, Makefile, .env, __pycache__.

The 'overlay' default keeps writes in memory, so nothing reaches the host filesystem. Use 'read-write' only with a directory that contains no code or config and is not on sys.path or any other execution path.

from pathlib import Path

from pydantic_monty import Monty, MountDir

with Monty() as pool, MountDir(host_path=Path('host-dir'), virtual_path='/data') as mount:
    with pool.checkout() as session:
        contents = session.feed_run("open('/data/notes.txt').read()", mount=mount)

The directory is held open from construction until the mount is closed, not for the duration of a feed — so reusing one across feeds is free, and works the same inside a with block or outside it. with (or close()) is what hands the directory back; feeds passed a closed mount raise ValueError. Without it the directory stays open until the object is collected: a file descriptor on Unix, but on Windows that blocks renaming or deleting it.

Methods

__new__
def __new__(
    cls,
    *,
    host_path: str | Path,
    virtual_path: str,
    mode: Literal['read-only', 'read-write', 'overlay'] = 'overlay',
    write_bytes_limit: int | None = None,
    memory_usage_limit: int = 100000000,
) -> MountDir

Configure a mount point; validation happens here, not at feed time.

All arguments are keyword-only: mount tools disagree on host-first (docker -v) vs virtual-first (nginx alias) ordering, so requiring names removes the ambiguity.

Returns

MountDir

Parameters

host_path : str | Path

Real host directory to expose. Opened at construction; raises if it doesn’t exist, isn’t a directory, or cannot be opened — on macOS/BSD a search-only (0o111) directory is not mountable, though Linux accepts one. Sandbox code can never see this path or reach outside it. The mount tracks the directory itself rather than its name, so renaming it on the host does not detach the mount; on Windows the open handle prevents the host renaming or deleting it at all while the mount lives. Symlinks inside it are followed only if their targets are relative — an absolute target raises PermissionError in the sandbox even when it points back into the same mount.

virtual_path : str

Absolute POSIX-style path prefix inside the sandbox (e.g. '/data'), regardless of host OS. Raises TypeError if not absolute.

mode : Literal[‘read-only’, ‘read-write’, ‘overlay’] Default: 'overlay'

'read-only' — reads only, writes raise PermissionError; 'read-write' — writes through to the host directory, where the files persist after the feed (see the warning above); 'overlay' (default) — reads fall through to the host, writes are captured in memory per feed and discarded when it ends.

write_bytes_limit : int | None Default: None

Cap on cumulative bytes written through the mount within one feed; exceeding it raises OSError in the sandbox. None (default) means unlimited.

memory_usage_limit : int Default: 100000000

Per-mount budget in bytes (default 100 MB, matches DEFAULT_MEMORY_USAGE_LIMIT in rust) shared by retained overlay data and transient filesystem results; an operation that would exceed it raises MemoryError in the sandbox.

close
def close() -> None

Release the open host directory. Idempotent.

Feeds passed this mount afterwards raise ValueError; the attributes above keep answering. Only Windows needs this — it refuses to rename or delete a directory while a handle to it is open — but MountDir also works as a context manager, which closes on exit.

Returns

None

OSAccess

Bases: AbstractOS

In-memory virtual filesystem for sandboxed Monty execution.

OSAccess provides a complete virtual filesystem that Monty code can interact with via pathlib.Path methods. Files exist only in memory (when using MemoryFile) and cannot access the real filesystem. Environment access is isolated to the provided environ mapping. date.today() and datetime.now() default to the host clock; override those methods in a subclass if you need a deterministic or virtual clock.

Attributes

files

List of AbstractFile objects registered with this filesystem.

Type: list[AbstractFile]

environ

Dictionary of environment variables accessible via os.getenv().

Type: dict[str, str]

Methods

__init__
def __init__(
    files: Sequence[AbstractFile] | None = None,
    environ: dict[str, str] | None = None,
    *,
    root_dir: str | PurePosixPath = '/',
)

Create a virtual filesystem with the given files.

Parameters

files : Sequence[AbstractFile] | None Default: None

Files to register in the virtual filesystem. Use MemoryFile for sandboxed in-memory files, or CallbackFile for custom logic (with security caveats - see its docstring).

environ : dict[str, str] | None Default: None

Environment variables accessible to Monty code via os.getenv(). Isolated from the real environment.

root_dir : str | PurePosixPath Default: '/'

Base directory for normalizing relative file paths. Relative paths in files will be prefixed with this. Default is ’/’.

Raises
  • AssertionError — If root_dir is not an absolute path.
  • ValueError — If a file path conflicts with another file (e.g., trying to create a file inside another file’s path).
path_open
def path_open(path: PurePosixPath, mode: str) -> MontyFileHandle

Perform the open(path, mode) open-time effect against the in-memory tree.

  • 'r' / 'rb': verify the file exists and is not a directory; raise FileNotFoundError / IsADirectoryError if not.
  • 'w' / 'wb': truncate (or create empty) via _write_file.
  • 'a' / 'ab': create the file if missing; leave existing content untouched. Raises IsADirectoryError if the path is a directory.

The returned MontyFileHandle carries the canonicalized mode ('rt''r', 'r+b''rb+') and becomes the first argument of any subsequent read/write/append OS calls Monty issues for this file.

The handle is constructed before any side effect so that a malformed mode raises ValueError without touching the filesystem. Direct callers (not routed through Monty, which pre-validates) could otherwise pass e.g. 'wxyz' and silently trigger the truncate/create branch before the eventual mode-parse failure.

Returns

MontyFileHandle

AbstractOS

Bases: ABC

Abstract base class for implementing virtual filesystems and host OS access.

Subclass this and implement the abstract methods to provide a custom filesystem and selected host-backed operations that Monty code can interact with via pathlib.Path, os, date.today(), and datetime.now().

Pass an instance as the os parameter to Monty.run().

Methods

__call__
def __call__(
    function_name: OsFunction,
    args: tuple[Any, ...],
    kwargs: dict[str, Any] | None = None,
) -> Any

Adapter used by Monty’s os= callback surface.

Monty calls __call__ directly, so this method stays as the public callable entrypoint. Override dispatch() when you want to customize routing or return NOT_HANDLED.

Returns

Any — The OS operation result, or NOT_HANDLED to let Monty apply its Any — standard unhandled-operation behavior.

dispatch
def dispatch(
    function_name: OsFunction,
    args: tuple[Any, ...],
    kwargs: dict[str, Any] | None = None,
) -> Any

Dispatch an OS operation to the appropriate method.

This handles Monty’s built-in pathlib.Path, os, and host clock operations. Subclasses can override it for custom behavior or return NOT_HANDLED to delegate back to Monty’s default fallback errors.

Returns

Any — The result of the OS operation.

Parameters

function_name : OsFunction

The OS operation being called (e.g., ‘Path.exists’).

args : tuple[Any, …]

The arguments passed to the method.

kwargs : dict[str, Any] | None Default: None

The keyword arguments passed to the method.

path_exists

@abstractmethod

def path_exists(path: PurePosixPath) -> bool

Check if a path exists.

Returns

bool — True if the path exists, False otherwise.

Parameters

path : PurePosixPath

The path to check.

path_is_file

@abstractmethod

def path_is_file(path: PurePosixPath) -> bool

Check if a path is a regular file.

Returns

bool — True if the path is a regular file, False otherwise.

Parameters

path : PurePosixPath

The path to check.

path_is_dir

@abstractmethod

def path_is_dir(path: PurePosixPath) -> bool

Check if a path is a directory.

Returns

bool — True if the path is a directory, False otherwise.

Parameters

path : PurePosixPath

The path to check.

@abstractmethod

def path_is_symlink(path: PurePosixPath) -> bool

Check if a path is a symbolic link.

Returns

bool — True if the path is a symbolic link, False otherwise.

Parameters

path : PurePosixPath

The path to check.

path_open
def path_open(path: PurePosixPath, mode: str) -> MontyFileHandle

Perform the open-time effect for open(path, mode).

Monty issues this OS call from the open() builtin. The handler is responsible for the side-effect that matches mode and for returning a MontyFileHandle Monty can wrap into an _io.* file object:

  • 'r' / 'r+' (text/binary): verify the file exists and is not a directory; raise FileNotFoundError / IsADirectoryError if not.
  • 'w' / 'w+': truncate the file (creating it if missing).
  • 'a' / 'a+': create the file if missing, leaving existing content untouched.

The returned MontyFileHandle becomes the first argument of any subsequent Path.read_text / Path.write_text / Path.append_text (and bytes variants) OS calls; dispatch() normalizes it back to the underlying PurePosixPath so the existing path_* handlers continue to work without modification.

Returns

MontyFileHandle

path_read_text

@abstractmethod

def path_read_text(path: PurePosixPath | MontyFileHandle) -> str

Read the contents of a file as text.

Returns

str — The file contents as a string.

Parameters

path : PurePosixPath | MontyFileHandle

The path to the file, either as a PurePosixPath (from pathlib.Path methods) or a MontyFileHandle (after the file was opened via open()). Use path_from_arg() to collapse both shapes if you don’t need the extra handle metadata.

Raises
  • FileNotFoundError — If the file does not exist.
  • IsADirectoryError — If the path is a directory.
path_read_bytes

@abstractmethod

def path_read_bytes(path: PurePosixPath | MontyFileHandle) -> bytes

Read the contents of a file as bytes.

Returns

bytes — The file contents as bytes.

Parameters

path : PurePosixPath | MontyFileHandle

The path to the file, either as a PurePosixPath (from pathlib.Path methods) or a MontyFileHandle (after the file was opened via open()). Use path_from_arg() to collapse both shapes if you don’t need the extra handle metadata.

Raises
  • FileNotFoundError — If the file does not exist.
  • IsADirectoryError — If the path is a directory.
path_write_text

@abstractmethod

def path_write_text(path: PurePosixPath | MontyFileHandle, data: str) -> int

Write text data to a file.

Returns

int — The number of characters written.

Parameters

path : PurePosixPath | MontyFileHandle

The path to the file, either as a PurePosixPath (from pathlib.Path methods) or a MontyFileHandle (after the file was opened via open()). Use path_from_arg() to collapse both shapes if you don’t need the extra handle metadata.

data : str

The text content to write.

Raises
  • FileNotFoundError — If the parent directory does not exist.
  • IsADirectoryError — If the path is a directory.
path_write_bytes

@abstractmethod

def path_write_bytes(path: PurePosixPath | MontyFileHandle, data: bytes) -> int

Write binary data to a file.

Returns

int — The number of bytes written.

Parameters

path : PurePosixPath | MontyFileHandle

The path to the file, either as a PurePosixPath (from pathlib.Path methods) or a MontyFileHandle (after the file was opened via open()). Use path_from_arg() to collapse both shapes if you don’t need the extra handle metadata.

data : bytes

The binary content to write.

Raises
  • FileNotFoundError — If the parent directory does not exist.
  • IsADirectoryError — If the path is a directory.
path_append_text
def path_append_text(path: PurePosixPath | MontyFileHandle, data: str) -> int

Append text data to a file and return the number of characters written.

Accepts either a PurePosixPath or a MontyFileHandle (see path_read_text for details).

Returns

int

path_append_bytes
def path_append_bytes(path: PurePosixPath | MontyFileHandle, data: bytes) -> int

Append binary data to a file and return the number of bytes written.

Accepts either a PurePosixPath or a MontyFileHandle (see path_read_text for details).

Returns

int

path_mkdir

@abstractmethod

def path_mkdir(path: PurePosixPath, parents: bool, exist_ok: bool) -> None

Create a directory.

Returns

None

Parameters

path : PurePosixPath

The path of the directory to create.

parents : bool

If True, create parent directories as needed.

exist_ok : bool

If True, don’t raise an error if the directory exists.

Raises
  • FileNotFoundError — If parents is False and parent directory doesn’t exist.
  • FileExistsError — If exist_ok is False and the directory already exists.

@abstractmethod

def path_unlink(path: PurePosixPath) -> None

Remove a file.

Returns

None

Parameters

path : PurePosixPath

The path to the file to remove.

Raises
  • FileNotFoundError — If the file does not exist.
  • IsADirectoryError — If the path is a directory.
path_rmdir

@abstractmethod

def path_rmdir(path: PurePosixPath) -> None

Remove an empty directory.

Returns

None

Parameters

path : PurePosixPath

The path to the directory to remove.

Raises
  • FileNotFoundError — If the directory does not exist.
  • NotADirectoryError — If the path is not a directory.
  • OSError — If the directory is not empty.
path_iterdir

@abstractmethod

def path_iterdir(path: PurePosixPath) -> list[PurePosixPath]

List the contents of a directory.

Returns

list[PurePosixPath] — A list of full paths (as PurePosixPath) for entries in the directory.

Parameters

path : PurePosixPath

The path to the directory.

Raises
  • FileNotFoundError — If the directory does not exist.
  • NotADirectoryError — If the path is not a directory.
path_stat

@abstractmethod

def path_stat(path: PurePosixPath) -> StatResult

Get file status information.

Use file_stat(), dir_stat(), or symlink_stat() helpers to create the return value.

Returns

StatResult — A StatResult with file metadata.

Parameters

path : PurePosixPath

The path to stat.

Raises
  • FileNotFoundError — If the path does not exist.
path_rename

@abstractmethod

def path_rename(path: PurePosixPath, target: PurePosixPath) -> None

Rename a file or directory.

Returns

None

Parameters

path : PurePosixPath

The current path.

target : PurePosixPath

The new path.

Raises
  • FileNotFoundError — If the source path does not exist.
  • FileExistsError — If the target already exists (platform-dependent).
path_resolve

@abstractmethod

def path_resolve(path: PurePosixPath) -> str

Resolve a path to an absolute path, resolving any symlinks.

Returns

str — The resolved absolute path with symlinks resolved.

Parameters

path : PurePosixPath

The path to resolve.

path_absolute

@abstractmethod

def path_absolute(path: PurePosixPath) -> str

Convert a path to an absolute path without resolving symlinks.

Returns

str — The absolute path.

Parameters

path : PurePosixPath

The path to convert.

getenv

@abstractmethod

def getenv(key: str, default: str | None = None) -> str | None

Get an environment variable value.

Returns

str | None — The value of the environment variable, or default if not set.

Parameters

key : str

The name of the environment variable.

default : str | None Default: None

The value to return if the environment variable is not set.

get_environ

@abstractmethod

def get_environ() -> dict[str, str]

Get the entire environment as a dictionary.

Returns

dict[str, str] — A dictionary containing all environment variables.

date_today
def date_today() -> datetime.date

Return today’s date for Monty’s date.today() host callback.

Override this when the sandbox should observe a virtual or fixed clock. The default implementation proxies to the host Python process.

Returns

datetime.date

datetime_now
def datetime_now(tz: datetime.tzinfo | None = None) -> datetime.datetime

Return the current datetime for Monty’s datetime.now(tz=...) callback.

Override this when the sandbox should observe a virtual or fixed clock. The default implementation proxies to the host Python process and passes any provided timezone through to datetime.datetime.now().

Returns

datetime.datetime

AbstractFile

Bases: Protocol

Protocol defining the interface for files used with OSAccess.

This protocol allows custom file implementations to be used with OSAccess. The built-in implementations are:

  • MemoryFile: Stores content in memory (recommended for sandboxed execution)
  • CallbackFile: Delegates to custom callbacks (use with caution - see its docstring)

Attributes

path

The virtual path of the file within the OSAccess filesystem.

Type: PurePosixPath

name

The filename (basename) extracted from path.

Type: str

permissions

Unix-style permission bits (e.g., 0o644).

Type: int

deleted

Whether the file has been marked as deleted.

Type: bool

Methods

read_content
def read_content() -> str | bytes

Read and return the file’s content.

Returns

str | bytes

write_content
def write_content(content: str | bytes) -> None

Write content to the file.

Returns

None

delete
def delete() -> None

Mark the file as deleted.

Returns

None

MemoryFile

An in-memory virtual file for use with OSAccess.

This is the recommended file type for sandboxed Monty execution. Content is stored entirely in Python memory with no access to the real filesystem.

When Monty code reads from this file, it receives the stored content. When Monty code writes to this file, the content attribute is updated.

Example::

from pydantic_monty import Monty, OSAccess, MemoryFile

fs = OSAccess( [ MemoryFile(‘/config.json’, ’{“debug”: true}’), MemoryFile(‘/data.bin’, b’\x00\x01\x02’), ] )

result = Monty(''' from pathlib import Path Path(‘/config.json’).read_text() ''').run(os=fs)

result == ’{“debug”: true}‘

Attributes

path

The virtual path of the file within the OSAccess filesystem.

Type: PurePosixPath

name

The filename (basename) extracted from path.

Type: str

content

The file content (str for text, bytes for binary).

Type: str | bytes

permissions

Unix-style permission bits (default: 0o644).

Type: int

deleted

Whether the file has been marked as deleted.

Type: bool

Methods

__init__
def __init__(
    path: str | PurePosixPath,
    content: str | bytes,
    *,
    permissions: int = 420,
) -> None

Create an in-memory virtual file.

Returns

None

Parameters

path : str | PurePosixPath

The virtual path for this file in the OSAccess filesystem.

content : str | bytes

The initial file content (str for text, bytes for binary).

permissions : int Default: 420

Unix-style permission bits (default: 0o644).

read_content
def read_content() -> str | bytes

Return the stored content.

Returns

str | bytes

write_content
def write_content(content: str | bytes) -> None

Update the stored content.

Returns

None

delete
def delete() -> None

Mark the file as deleted.

Returns

None

CallbackFile

A virtual file backed by custom read/write callbacks.

This class allows you to create files whose content is dynamically generated or persisted through custom logic. When Monty code reads or writes to this file, the provided callbacks are invoked.

Attributes

path

The virtual path of the file within the OSAccess filesystem.

Type: PurePosixPath

name

The filename (basename) extracted from path.

Type: str

read

Callback invoked when the file is read. Receives the path and must return str or bytes.

Type: Callable[[PurePosixPath], str | bytes]

write

Callback invoked when the file is written. Receives the path and content (str or bytes).

Type: Callable[[PurePosixPath, str | bytes], None]

permissions

Unix-style permission bits (default: 0o644).

Type: int

deleted

Whether the file has been marked as deleted.

Type: bool

Methods

__init__
def __init__(
    path: str | PurePosixPath,
    read: Callable[[PurePosixPath], str | bytes],
    write: Callable[[PurePosixPath, str | bytes], None],
    *,
    permissions: int = 420,
) -> None

Create a callback-backed virtual file.

Returns

None

Parameters

path : str | PurePosixPath

The virtual path for this file in the OSAccess filesystem.

read : Callable[[PurePosixPath], str | bytes]

Callback to generate content when the file is read.

write : Callable[[PurePosixPath, str | bytes], None]

Callback to handle content when the file is written.

permissions : int Default: 420

Unix-style permission bits (default: 0o644).

read_content
def read_content() -> str | bytes

Read content by invoking the read callback.

Returns

str | bytes

write_content
def write_content(content: str | bytes) -> None

Write content by invoking the write callback.

Returns

None

delete
def delete() -> None

Mark the file as deleted.

Returns

None

StatResult

Bases: NamedTuple

Equivalent to os.stat_result.

Attributes

st_mode

protection bits

Type: int

st_ino

inode

Type: int

st_dev

device

Type: int

number of hard links

Type: int

st_uid

user ID of owner

Type: int

st_gid

group ID of owner

Type: int

st_size

total size, in bytes

Type: int

st_atime

time of last access

Type: float

st_mtime

time of last modification

Type: float

st_ctime

time of last change

Type: float

Methods

file_stat

@classmethod

def file_stat(cls, size: int, mode: int = 420, mtime: float | None = None) -> Self

Creates a stat_result namedtuple for a regular file.

Use this when responding to Path.stat() OS calls.

Returns

Self

Parameters

size : int

File size in bytes

mode : int Default: 420

File permissions as octal (e.g., 0o644) or full mode with file type

mtime : float | None Default: None

Modification time as Unix timestamp, defaults to Now.

dir_stat

@classmethod

def dir_stat(cls, mode: int = 493, mtime: float | None = None) -> Self

Creates a stat_result namedtuple for a directory.

Use this when responding to Path.stat() OS calls on directories.

Returns

Self — A namedtuple with stat_result fields

Parameters

mode : int Default: 493

Directory permissions as octal (e.g., 0o755) or full mode with file type

mtime : float | None Default: None

Modification time as Unix timestamp, defaults to Now.

MontyFileHandle

Host-side handle to a file opened inside a Monty sandbox.

Plain data holder — Monty never gives the host a live OS file descriptor. Exposed to callbacks (e.g. as the first argument of an open result or a read/write request) so they can route on path and branch on mode/binary/readable/writable without re-parsing the mode string.

Construct one from a Python open OS handler to return a handle back to Monty: MontyFileHandle('/data/foo.txt', 'r'). The mode is canonicalized at construction ('rt''r', 'r+b''rb+').

Attributes

path

Virtual sandbox path of the open file (always POSIX-style, never a host path).

Type: str

mode

Canonical Python open() mode string for this file (e.g. 'r', 'rb+', 'w').

Type: str

position

Current position for sized/line/seek operations.

Char index in text mode, byte index in binary mode. 0 for a freshly opened file.

Type: int

binary

True if the mode opens the file in binary form ('rb', 'wb', …).

Type: bool

readable

True if the mode permits read() ('r', 'r+', 'w+', 'a+', and binary variants).

Type: bool

writable

True if the mode permits write() ('w', 'a', 'r+', 'w+', 'a+', and binary variants).

Type: bool

Methods

__new__
def __new__(cls, path: str, mode: str, *, position: int = 0) -> MontyFileHandle

Construct a MontyFileHandle to return from an open OS callback.

Returns

MontyFileHandle

Parameters

path : str

Virtual sandbox path of the opened file (POSIX-style).

mode : str

Python open() mode string. Parsed and canonicalized at construction, so 'rt' becomes 'r' and 'r+b' becomes 'rb+'. Raises ValueError for malformed or unsupported modes (e.g. 'x').

position : int Default: 0

Initial position for sized/line/seek operations (char index in text mode, byte index in binary mode). Almost always 0 for a freshly opened file.

OsHandler

OS-call handler shared by feed_run / feed_start.

Type: TypeAlias Default: Callable[[OsFunction, tuple[Any, ...], dict[str, Any]], Any] | AbstractOS

OsFunction

Default: Literal['Path.exists', 'Path.is_file', 'Path.is_dir', 'Path.is_symlink', 'open', 'Path.read_text', 'Path.read_bytes', 'Path.write_text', 'Path.write_bytes', 'Path.append_text', 'Path.append_bytes', 'Path.mkdir', 'Path.unlink', 'Path.rmdir', 'Path.iterdir', 'Path.stat', 'Path.rename', 'Path.resolve', 'Path.absolute', 'os.getenv', 'os.environ', 'date.today', 'datetime.now']

NOT_HANDLED

Default: object()