Built-in functions
Monty implements a subset of CPython’s builtins. Referencing any name not
listed here raises NameError at runtime; there is no fallback to a host
Python.
abs, all, any, bin, chr, divmod, enumerate, eval, exec, filter,
format, getattr, hasattr, hash, hex, id, isinstance, iter, len,
locals, map, max, min, next, oct, open, ord, pow, print, repr,
reversed, round, setattr, sorted, sum, type, zip.
eval, exec and locals are described in eval_exec.md.
bool, bytes, dict, float, frozenset, int, list, range,
set, slice, str, tuple. Exception classes (ValueError,
TypeError, etc.) are also names in the builtin namespace.
These raise NameError:
- Code objects and imports:
compile,__import__. - Namespace introspection:
globals,vars,dir. - Interactive:
input,breakpoint,help. - Decorators / descriptors:
classmethod,staticmethod,property,super. (@propertyon functions is not recognized; use a method.) - Construction / coercion:
bytearray,complex,memoryview,object,ascii. - Other:
callable,delattr,issubclass,aiter,anext.
super() is the biggest practical omission: with no class inheritance either
(see classes.md), there is no inheritance mechanism at all.
- No
__class__on builtin values —[].__class__,list.__class__andlist[int].__class__raiseAttributeError; only instances of Monty classes carry it (see classes.md). Usetype(x). - Builtin methods are call-only — reading one without calling it raises
AttributeError, so[1].append,'a'.upper,{}.get,dict.fromkeysandlist.__class_getitem__cannot be assigned, passed as a callback or reached throughgetattr. Call them directly (list.__class_getitem__(int)). hash(x)— Monty hashesstr,bytes,floatand every container with its own algorithm, so the values differ from CPython’s. Onlybooland smallintagree: aninthashes to itself, which is what CPython does whileabs(x) < 2**61 - 1, but CPython reduces modulo2**61 - 1from there up (hash(2**62)is2in CPython,4611686018427387904in Monty) and Monty hashes aninttoo large for ani64differently again. Monty’s hashes are stable within a run and across runs of the same build (there is no hash randomisation), but never persist one or compare one against a CPython hash.sys.hash_infois not exposed, so the parameters CPython publishes are unavailable (see sys.md).reprof a dict being mutated by its own elements — Monty iterates the live entries like CPython, but deletion compacts Monty’s dense entry storage where CPython leaves a tombstone in place: a key deleted from inside a user__repr__running during that dict’s repr shifts later entries down, so the entry after the deleted one can be skipped from the output where CPython would still print it. Insertions during repr match CPython (appended and printed), as do list (live length, mid-repr pops truncate / appends extend),set,collections.dequeandcollections.Counter(all snapshot, like CPython).- copying a dict or rebuilding a set re-hashes the keys — CPython copies the
hash table, so
d.copy(),dict(d),{**d},defaultdict.copy(),Counter.copy(),set(s)andfrozenset(s)never call a key’s__hash__. Monty re-inserts each element, so a key with a custom__hash__sees it called again — observable through a counter or other side effect, and a__hash__that raises makes the copy fail where CPython’s succeeds. Onlyset.copy()clones the storage directly and matches CPython.copy.copyinherits this for dicts (see copy.md). - dict/set lookups under a mutating
__eq__— like CPython, a lookup (in,d[k],set.remove, …) whose user__eq__mutates the container keeps probing rather than raising, and a mutation that only adds colliding keys never makes it repeat a comparison. Monty re-reads the colliding candidates after each comparison where CPython walks the live probe chain, so an__eq__that moves the entry being compared restarts the probe and re-compares the other candidates from scratch — extra__eq__calls CPython would not make (CPython restarts and re-compares too, but only after a resize or when that entry’s own slot changed). An__eq__that adds a colliding key on every comparison never finishes in either engine; under a duration limit Monty raisesTimeoutError. No mutation pattern can panic or corrupt either engine. - Set algebra under a mutating
__eq__—-,&,^and their method forms walk one of the two sets while user__eq__code can run. Monty raisesRuntimeError: Set changed size during iterationif that code adds or removes an element from the set being walked, where CPython carries on over its live table and returns a result. The operand Monty is not walking is snapshotted before the operation begins, so mutating that one is never observed at all —s | tandt.update(s)see all ofs’s original elements even when__eq__clearsspartway through, where CPython’s merge stops early. A mutation that empties the set being probed rather than walked is a resize to neither engine, but they still answer from different sides of it: withs’s own__eq__clearings,s.isdisjoint(t)isFalsein Monty, which keeps the comparison that matched, andTruein CPython, which restarts the probe and finds the set empty. - Dict-view set operators re-hash the view’s own keys —
d.keys() - s,|,^,isdisjointand the reflected forms collect those keys through a live, resize-checked walk that calls__hash__on each one. CPython probes with each key’s stored hash and mostly does not call it at all. So a__hash__— or a colliding__eq__— that resizes the dict raisesRuntimeError: dictionary changed size during iterationin Monty where CPython completes, as ind.keys() - s; CPython raises too wherever its own walk observes the change, as ins - d.keys()andd.items() - s.d.keys() & sdiverges the other way: Monty always walks the other operand and probes the live dict, so the view’s keys are never hashed and the intersection completes, while CPython walks the view whenever the dict is no larger than the other operand — hashing each of its keys into that operand — and so raises where Monty returns a result. The operand that is not the view is snapshotted before the operation begins, so mutating that one is never observed. Absent mutation every result agrees. Set-to-set operators do not re-hash at all, and dict-view equality (d.keys() == s) raises exactly where CPython does. enumerate,zip,map,filterandreversedare eager, not lazy — each drains its source and returns alist, sotype(enumerate(x)).__name__is'list'rather than'enumerate'. Observable several ways: a side-effecting callable runs for every item at the call itself rather than as the result is consumed; the whole result is held in memory at once, so an infinite iterator (e.g.map(f, itertools.count())) never returns and runs until a resource limit trips; the result can be indexed and re-iterated, which CPython forbids; and mutating the source from inside the loop body is never observed, so containers that detect mutation during iteration (dict,set,collections.deque) will not raise when looped over via one of these.zipand multi-iterablemapstop at the shortest input, so pairing an infinite iterable with a finite or empty one stays bounded. A plainfor x in containeris lazy and does detect mutation. See itertools.md.- Arity-error wording for some str/bytes methods — a handful of
keyword-accepting methods (e.g.
str.split,str.rsplitand thebytesequivalents) report too-many-arguments assplit expected at most 2 arguments, got 3, where CPython 3.14’s Argument Clinic pre-counts positionals plus kwargs and sayssplit() takes at most 2 arguments (3 given). Methods audited against CPython (encode,decode,expandtabs,splitlines,replace, …) already match; the remainder need a per-functionat_most_totalaudit. getattr(obj, name)— if the resolved attribute would be an async coroutine, external function, or OS call, raisesTypeError: "getattr(): attribute is not a simple value"rather than returning a bound method object. Use direct attribute access (obj.name(...)) for these.int(x, base=10)— string/bytes parsing accepts ASCII digits only; CPython also accepts non-ASCII Unicode decimal digits (int('١٢')== 12), which Monty rejects withinvalid literal for int() with base 10.bytes(source)— an iterable of ints is not supported: CPython’sbytes([65, 66])==b'AB', Monty raisesTypeError: cannot convert 'list' object to bytes. The int / str-with-encoding / bytes source forms all work. A count abovei64(bytes(2**70)) raises the same shape ofTypeError(cannot convert 'int' object to bytes), not CPython’sOverflowError: cannot fit 'int' into an index-sized integer.slice—start,stopandstepare readable, butslice.indices(length)is not implemented and raisesAttributeError: 'slice' object has no attribute 'indices'.isinstance(obj, T)—Tmust be a built-in type (int,str,list, …), a built-in exception class, a sandbox-defined class (see classes.md), a|union of those (see typing.md), or a tuple of those. Passing a host-supplied dataclass / namedtuple as the second argument raisesTypeError.iter()— see iter.md for iterator anditer(callable, sentinel)divergences.pow(base, exp, mod)— the three-argument form requires all integers and rejects negative exponents withValueErrorinstead of computing a modular inverse. Non-modular exponents whose result cannot be materialized raiseOverflowError(see resource_limits.md).pow(base, exp)and**with a negative float base and a fractional exponent — givesnanwhere CPython returns acomplex(Monty has no complex type). Overflow raisesOverflowErrorlike CPython, always worded(34, 'Numerical result out of range')(glibc’sstrerror(ERANGE); CPython on macOS and Windows says(34, 'Result too large')), andexc.argsis that text as one string rather than CPython’s(34, '...')tuple.sorted(iterable, *, key=None, reverse=False)—keyandreversemust be passed by keyword; positional forms raiseTypeError.round(n, ndigits)—ndigitsvalues outside the i64 range are clamped by sign. For floats this matches CPython (which clamps toPy_ssize_t); for an intnwith a hugely negativendigits, CPython tries to materialise10**-ndigitsand dies withMemoryErrorwhere Monty returns0immediately.print— writes via the host print callback.file=,flush=are not honoured;sep=andend=are.- Identity of host-supplied callables — host functions passed in as inputs
(
MontyObject::function(...)) lose their host object identity at the sandbox boundary. Live external functions are identified by lookup name, so distinct host callables with the same name shareis, equality,id(), andhash()results. Once the last sandbox reference is dropped, a later conversion of that name may create a new function object. - Type objects across the host boundary — a
typeobject (a class, not an instance) round-trips in both directions.- Sandbox → host (external/OS-call argument, or a
.run()return value): the type is reconstructed as the corresponding host class. Genuine builtins (int,str,type,bytes,list,dict,property, …) resolve to the real builtin; Monty’s modeled stdlib types map to their host stdlib class:datetime/date/timedelta/timezone→datetime.*,re.Pattern/re.Match→re.*, the binary/text file types →io.*. Thepathlib.Pathclass maps topathlib.PurePosixPath, consistent with how Path instances round-trip, and instantiable on every host OS. A type with no faithful host class (e.g. an internal function or cell type) cannot be reconstructed and surfaces as anAttributeErrorfrom the host call. - Host → sandbox (input, or an external-call return value): the same recognized
builtins and modeled stdlib types are preserved as type objects, so
isinstance(x, the_type)works inside the sandbox. Recognition is by type-object identity, not class name/module, so a class that forges__name__/__module__to impersonate a builtin is not treated as one. Everypathlibpath class collapses toPurePosixPath(it re-emerges asPurePosixPath). A host class Monty does not model (e.g. a user-defined class) is not preserved as a type; it degrades to a callable, appearing inside the sandbox as afunctionrather than atype.
- Sandbox → host (external/OS-call argument, or a