Python language / parser
Monty parses Python source with Ruff’s parser but rejects several constructs
at parse time. Anything listed below raises NotImplementedError (prefixed
with “The monty syntax parser does not yet support ”) at compile time, before
any code runs.
classdefinitions — simple classes are supported (instance methods,__init__/__repr__/__str__, class variables of arbitrary expressions). Rejected at parse time: base classes / metaclasses (class Foo(Bar):) and class-body statements other thandef, a simplename [: T] = <expr>assignment,pass, or a docstring. There is no inheritance and no general dunder protocol. See classes.md.- Decorators (
@deco) — supported on classes and on top-level or nesteddef/async def, taking any callable in scope, evaluated in the enclosing scope and applied bottom-up. Rejected at parse time on methods, so@classmethod,@staticmethod,@propertyand any decorator on adefinside a class body are unavailable. See classes.md. async withstatements — not yet supported.yield/yield fromexpressions — no generator functions. Generator expressions ((x for x in ...)) parse but currently materialize to alistrather than a lazy iterator, a known temporary divergence; seeiter__generator_expr_type.py.matchstatements — structural pattern matching is not supported.delstatements — neitherdel xnordel d[k]parse.try*/except*exception groups — PEP 654 syntax rejected.typealiases (PEP 695type Foo = int).async forloops and async comprehensions.- Wildcard imports (
from m import *) — raisesNotImplementedError: "Wildcard imports (`from ... import *`) are not supported".
- Complex number literals (
1j,2+3j) —NotImplementedError: The monty syntax parser does not yet support complex constants. - Template strings (t-strings) — PEP 750.
Anything Monty can iterate may follow a *, matching CPython: [*xs],
(*xs,), {*xs}, f(*xs), a, b = xs and a, *b = xs all accept whatever
list(xs) accepts.
One message divergence: passing a non-iterable to a call, f(*1), reports
TypeError: Value after * must be an iterable, not int, the same wording as a
list literal. CPython instead names the callable by its module-qualified
__qualname__: __main__.f() argument after * must be an iterable, not int,
and correspondingly __main__.C.m(), __main__.<lambda>() or
__main__.outer.<locals>.inner(). Monty has neither function __qualname__
nor module-qualified names (see the class-name note in collections.md), so
it reports the generic form. Every other unpacking form matches CPython
exactly, except for the comprehension target restriction in
comprehensions.md.
- AST nesting is capped at 200 levels (30 in debug builds); exceeding it raises
SyntaxError: Source is too deeply nested. - The budget is shared across every nesting-producing construct (parens, calls, subscripts, attribute chains, operators,
comprehensions, control-flow blocks,
with, etc.), including the synthetic nesting from a flat multi-itemwith; see with.md. - The message differs from CPython, which uses construct-specific wording (
too many nested parentheses,too many statically nested blocks, …). - Class-body annotations count against the budget even though they are stringized rather than evaluated (see typing.md), as do class-variable values and method parameter defaults; all three are walked before being parsed. CPython imposes no comparable limit on a stringized annotation.
- A source longer than
CompileOptions::source_scan_thresholdbytes (4 KiB by default) is first scanned token by token and rejected with the sameSyntaxErrorwhen its estimated parser nesting exceeds the cap. The scan bounds how much native stack the parser grows on a long source, memory the sandbox allocator does not see; a shorter source cannot nest deeper than its length, so it skips the scan. - The scan counts open brackets, indented blocks, prefix operators,
**right operands, lambda bodies,elsebranches, f-string format specs and the+and-ofcasecomplex-literal patterns, so one expression holding more than 200 of those open at once, with no comma, newline or lower-precedence operator between them, is rejected even where CPython would accept it. A long literal such as[-1, -2, ...]releases the count at every comma. A logical line starting with a variable namedcasecounts its binary+and-the same way. - The scan also enters string literals, because the type checker parses a forward-reference annotation such as
x: "list[int]"from the text between the quotes; a string longer than 200 bytes holding more than 200 unbalanced brackets is rejected the same way even when it is plain data. - Only Rust hosts can change the threshold, with
CompileOptions { source_scan_threshold: n, ..CompileOptions::default() }(0scans every source,usize::MAXnever scans); Python and JavaScript sessions use the default.
- Only the bundled stdlib modules listed in modules.md can be
imported. Importing anything else raises
ModuleNotFoundError. - Relative imports (
from . import x) raiseImportError: "attempted relative import with no known parent package"; there is no package system. __import__is not defined.
from __future__ import ... is a compiler directive, not a real import: it
binds nothing and is accepted as a no-op. Of CPython’s ten features, eight
became mandatory in Python 3.7 or earlier and so are inert there too, and
annotations is a no-op here because Monty already stringizes annotations
(see typing.md). Divergences:
barry_as_FLUFL(PEP 401) raisesNotImplementedError: "The monty syntax parser does not yet support the 'barry_as_FLUFL' future feature". CPython accepts it, making<>the inequality operator and!=aSyntaxError; Monty parses neither differently, so the import is rejected rather than silently ignored.- Aliasing is rejected.
from __future__ import annotations as annraisesNotImplementedError: "The monty syntax parser does not yet support aliasing a `__future__` feature". CPython bindsannto a__future__._Featureobject; a no-op would bind nothing and surface as aNameErrorfar from the import, so it is rejected at the import instead. - Position is not enforced. CPython requires
__future__imports to precede all other statements (SyntaxError: "from __future__ imports must occur at the beginning of the file"); Monty accepts them anywhere. import __future__(as opposed tofrom __future__ import ...) raisesModuleNotFoundError; there is no__future__module object.
Monty has no module object and no globals() dict, but it exposes a fixed set
of module-level dunders so common idioms (e.g. if __name__ == '__main__':)
work. They are resolved on read; there is no real namespace entry behind them,
so the values built per read (__file__, __annotations__) are fresh objects
each time and __file__ is __file__ is False where CPython gives True.
| Name | Monty value | CPython (script run) |
|---|---|---|
__name__ | '__main__' | '__main__' |
__debug__ | True | True |
__doc__ | None | None or docstring str |
__spec__ | None | None |
__package__ | None | None |
__annotations__ | empty dict | NameError (no annotations) |
In Monty __doc__ is always None, since module docstrings are never
extracted, and __annotations__ is always an empty dict because
module-level annotations are not stored (see typing.md); CPython 3.14
instead raises NameError when a module has no annotations (PEP 649).
These names are read-only: assigning one at module or global scope (including
via global __name__ inside a function, and augmented assignment like
__name__ += ...) is rejected at compile time with
NotImplementedError: cannot reassign read-only module attribute '<name>'.
CPython instead allows rebinding most of them (it is how you set a module
docstring), and rejects only __debug__, with a SyntaxError.
Binding one of these names as a function local is allowed (it is an
ordinary local in a separate namespace), matching CPython, except __debug__,
which CPython rejects everywhere with SyntaxError but Monty permits as a
local.
__file__ is the final path component of the session’s script name placed
under the virtual working directory the feed started in: /main.py by
default, /data/main.py when the feed’s first mount is /data. CPython makes
the script path absolute as given, so python src/app.py reports
/host/cwd/src/app.py; Monty keeps only app.py, because the script name is a
host-side label that may be a host path and no host directory may leak into the
sandbox. The monty CLI passes its file argument as the script name, so
monty /abs/script.py reports /script.py (or /data/script.py under a
/data mount) and monty -c reports /<string> where CPython raises
NameError. Like the other dunders it is read-only, where CPython allows
rebinding it.
Other module dunders CPython defines (__loader__, __builtins__,
__cached__, __dict__) are not exposed; reading them falls through to the host
name lookup and ultimately raises NameError if unresolved. __loader__ is
omitted because CPython always binds it to a loader object (never None), so
exposing None would diverge on type, and a real loader is neither available
nor safe to surface in the sandbox.
A function exposes no attributes: __name__, __doc__, __qualname__ and
__module__ all raise AttributeError: 'function' object has no attribute '<name>', and new ones cannot be set —
fn.tag = True raises
AttributeError: 'function' object has no attribute 'tag' and no __dict__ for setting new attributes. CPython supports
all of these.
This bounds what a decorator can do: it can call, wrap, store or replace the
function it receives, but cannot ask the function about itself, so
functools.wraps-style metadata copying, registries keyed by fn.__name__, and
attribute tagging for later discovery all have no equivalent.
<, <=, >, >= on operands with no defined ordering raise
TypeError: '<' not supported between instances of '{a}' and '{b}', matching
CPython (int vs str, None vs None, user-class instances without comparison
dunders, etc.). Lists and tuples order lexicographically as in CPython. A NaN
operand is unordered rather than incomparable, so float('nan') < 1 (and
every operator/direction, including two NaNs) returns False without raising,
also matching CPython, and likewise inside sorted/min/max.
One message divergence: when a list or tuple compares unequal only because
an inner element pair is unorderable (e.g. (1, 2) < (1, 'a')), Monty names
the outer container types ('tuple' and 'tuple') where CPython names the inner
element pair ('int' and 'str'). Both raise TypeError; only the message text
differs.
One value divergence: whenever CPython compares elements inside a container it
shortcuts equality by object identity (x is x ⇒ equal) before falling back
to ==. Monty has no object identity for immediate floats, so it asks ==, and
a NaN is never equal to itself. Every container operation built on element
comparison therefore differs when the same NaN object appears on both sides:
with x = float('nan') | CPython | Monty |
|---|---|---|
(x,) == (x,), [x] == [x] | True | False |
x in [x] | True | False |
[x].count(x) | 1 | 0 |
[x].index(x) | 0 | ValueError |
[1, x] < [1, x, 3] | True | False |
NaN is the only practical way to reach this, being the one built-in value
unequal to itself. Distinct NaN objects agree on both
([1, float('nan')] < [1, float('nan'), 3] is False either way), as does a
direct x == x (False on both). Named tuples inherit all of this from tuple.
- Functions (
def,async def), nested functions, closures, and decorators on them, but not on methods (see above). - List / dict / set comprehensions; generator comprehensions degrade to lists (see above).
try/except/else/finally,raise ... from ....for/while/if/elif/else,break,continue,pass,assert,global,nonlocal,return.import x,import x.y,from x import y, z as w.- f-strings including
=debug specifier,!r/!s/!aconversions, and format specs.