open() and file objects
Monty’s open() builtin returns a file wrapper supporting a subset of
CPython’s file API. The list below tracks every known difference from CPython.
pathlib.Path.open() is wired to the same machinery: it prepends self
as the file argument and forwards to the same internal entry point, so
every divergence listed below applies equally whether the caller uses
open(path, ...) or path.open(...). The only Path.open()-specific
quirks are in the “Path.open()” section at the bottom.
Monty never keeps a native file handle alive between OS or external
calls. open() itself yields an OsFunction::Open round-trip whose effect
(create / truncate / existence-check) the host performs and immediately
closes; every subsequent read()/write()/append() is a separate
one-shot OS call that the host opens, acts on, and closes again. The Monty
heap stores only path, mode, and small Python-visible state: no OS
handle, no buffered data, no descriptor number.
This is what makes subprocess dump() / load() safe: a session can be
serialized at a pause point and resumed later without dangling references to
host resources. The wasm in-process API exposes the same idea as
MontySnapshot. It also means external processes can observe partial state
between calls, and that there is no protection against the underlying file
being changed or removed between calls, both documented further down.
+update modes (r+,w+,a+, and theirbvariants) are rejected at parse time withValueError: update modes ('+') are not yet supported. Monty has no read-position state, so a write after a read would silently truncate the file via the one-shot OS write that backswrite().- Exclusive creation mode (
x) is rejected withValueError: exclusive creation mode is not supported; it would need a dedicated race-free mount-table operation. - The mode string is normalized to a canonical form at parse time
(
'rt'→'r','br'→'rb'); the original raw input is not preserved, andfile.modereports the canonical form. For text modes this diverges from CPython, whoseTextIOWrapperpreserves the string as passed:open(p, 'rt').modeis'rt'in CPython but'r'in Monty. Binary modes match, because CPython’s buffered classes normalize too (open(p, 'br').modeis'rb'in both).
Only file and mode are honored. The other six arguments
(buffering, encoding, errors, newline, closefd, opener) must be
at their CPython defaults; passing any non-default value raises
TypeError: '<name>' argument is not yet supported.
Two exceptions:
encoding="utf-8"(any case, also"utf8") is accepted as a documented no-op because Monty already uses UTF-8 for all text I/O.- A wrong type for
encoding/errors/newline(e.g.encoding=123) raises a typedTypeError: open() argument '<name>' must be str or None, not <type>rather than the generic “not yet supported” message.
Bytes paths are accepted but decoded as strict UTF-8, not via CPython’s
os.fsdecode / PEP 383 surrogateescape behavior. A non-UTF-8 bytes path
raises UnicodeDecodeError: can't decode bytes path as UTF-8.
This is a deliberate divergence, not a “not yet implemented” gap. PEP 383
relies on representing invalid bytes as lone surrogates (U+DC80–U+DCFF)
inside the resulting str. Rust’s String is strictly valid UTF-8 and
cannot hold lone surrogates without unsafe code or a parallel Vec<u8>
path storage type, neither of which is justified given that Monty paths
are virtual POSIX strings, not host-OS filenames. A lossy U+FFFD
replacement was also rejected because it would silently re-route an
open() call to a different (wrong) file rather than failing loudly.
If you have non-UTF-8 bytes you need to pass as a path, decode them
explicitly on the caller side (e.g. via os.fsdecode outside the sandbox)
before handing them to Monty.
The returned object is one of TextIOWrapper, BufferedReader,
BufferedWriter, or BufferedRandom depending on mode. The supported
methods and attributes are:
read()/read(-1)— read everything remaining from the current position. On the first call this performs a full-file OS read into a heap-resident buffer; subsequent reads slice the buffer in pure Monty.read(N)/read(None)— read up to N chars (text) or bytes (binary) from the current position, or everything remaining forNone. Same backing buffer asread().readline()— read up to and including the next\n, or the remainder of the buffer if the final line has no newline. Returns''/b''at EOF.readlines()— return alistof all remaining lines (each ending with\nexcept possibly the last).tell()— current position. Text-mode divergence: returns a char-index, not CPython’s opaque byte cookie. Round-trips throughseek()correctly.seek(offset, whence=0)— reposition within the buffer for readable files (loading it on demand), or within tracked logical write state for write-only files. Returns the new absolute position.write(data)— full-file or appending write.close(),flush(),readable(),writable(),seekable().__enter__()/__exit__()—with open(...) as f:works; see with.md for the shared protocol divergences.name,mode,closedattributes.encodingattribute on text files (always"utf-8").
Everything else raises AttributeError, including: truncate(),
fileno(), isatty(), detach(), buffer, raw, and the iterator
protocol (__iter__/__next__, including for line in f:).
- All reads (bare
read(), sizedread(N),readline,readlines) andseek()share a single heap-resident buffer populated on the first such call. The host serves only one full-fileReadText/ReadBytesper file; everything after is sliced in pure Monty. Memory cost: the whole file remains allocated and counts against the worker’s allocator-backedmax_memory. The buffer is never invalidated, so external modifications to the underlying file after the first read are not visible to subsequent reads. close()releases the cached buffer (matching CPython), returning its memory when no other value, such asdata = f.read(), retains it.- File I/O is rejected inside callbacks the interpreter evaluates in a
synchronous context that cannot suspend to the host — the
keyofsorted()/list.sort()/min()/max(),map()/filter()functions,iter(callable, sentinel),defaultdict’sdefault_factory, and dunder methods invoked implicitly. The first read that needs the host raisesNotImplementedError: <context>: OS function 'Path.read_text' is not yet supported in this contextwhere CPython would simply read. - A read that fails in the host leaves the file in a retry-safe state:
pending_readis cleared, the buffer stays empty, andeofis not flipped. A user-caught exception followed by a retry will re-attempt the OS load. This applies uniformly to bareread(), sizedread(N),readline(),readlines(), andseek(), and matches CPython. seekable()returnsTruefor all open Monty file wrappers, matching regular CPython files.- Text-mode
tell()returns a char index rather than CPython’s opaque byte cookie. Round-trips throughseek()work correctly (pos = tell(); seek(pos)resumes the same position) but the raw integer differs from what CPython returns for non-ASCII content. - Text-mode
seek(N)accepts any non-negative char-index. CPython restrictsTextIOWrapper.seektoseek(0),seek(0, 2), and cookies fromtell(). Monty is more permissive here. seek(-1)raisesOSError("[Errno 22] Invalid argument")matching CPython’sBufferedReader.seek. CPython’sTextIOWrapperraisesValueError("negative seek position -1")instead; Monty uses the binary-mode message in both modes.seek(0, 99)raisesValueError("whence value 99 unsupported")matching CPython’sBufferedReader. CPython’sTextIOWrapperuses a different"invalid whence ..."message; Monty does not.read(N)accepts only int orNone. TheTypeErrormessage differs from CPython (CPython:"argument should be integer or None, not 'str'"; Monty:"'str' object cannot be interpreted as an integer").- Write-only
seek()/tell()maintain logical position state, so commonwrite(); tell()andseek(0, 2)cases match CPython. Writes are still full-file or append one-shot host operations, so seeking backwards and then writing does not overwrite at that offset the way CPython’s live file descriptor would. readline(size)andreadlines(hint)are zero-argument only; passing a size/hint argument raisesTypeError. CPython accepts both and uses them to cap the returned bytes/chars.- File iteration (
for line in f) is NOT supported: it goes through theGetIteropcode, which cannot yield to the host. Usereadlines()and iterate the resulting list instead. write()to a text file requiresstr; to a binary file requiresbytes. The error messages match CPython (a bytes-like object is required, not '<type>'/write() argument must be str, not <type>).- Text I/O is whole-file UTF-8 with no error handlers and no newline
translation; line endings written to a
'w'file are preserved verbatim. io.UnsupportedOperation(raised byread()on'w'files,write()on'r'files, etc.) inherits from bothOSErrorandValueErrorfor catch purposes, soexcept OSError:andexcept ValueError:both work as in CPython.- No host file descriptor is held between calls (see “Design note: no live host file descriptors” above). The user-visible consequence is that external processes can observe partial state between writes, and Monty offers no protection against the underlying file being changed or removed between calls.
These match CPython:
'r'/'rb'on a missing file raisesFileNotFoundErrorat open time.'r'/'rb'on a directory raisesIsADirectoryErrorat open time.'w'/'wb'truncates the file immediately, before any write.'w'/'wb'creates a missing file immediately, before any write.'a'/'ab'creates a missing file immediately, preserving any existing content.
pathlib.Path.open(mode='r', ...) forwards to the same OsFunction::Open
round-trip as open() with self prepended as the file argument, so
all the rules above (mode rejection, kwarg validation, returned wrapper
types, open-time effects) apply identically. The differences:
- CPython’s
Path.open()signature lists onlymode, buffering, encoding, errors, newline(noclosefd/opener). Monty acceptsclosefd=Trueandopener=Noneat their CPythonopen()defaults as documented no-ops on this path too, and rejects non-default values with the same"'closefd' argument is not yet supported"/"'opener' argument is not yet supported"TypeErrorasopen(). CPython would instead raiseTypeError: open() got an unexpected keyword argument 'closefd'. - Passing
file=...as a keyword (which is meaningless onPath.open()becauseselfalready supplies the file) raises Monty’s “multiple values for argument ‘file’”TypeErrorrather than CPython’s “unexpected keyword argument ‘file’”. Real callers do not use this.