Back to Monty

Python language / parser

limitations/language.md

0.0.2110.6 KB
Original Source

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.

Statements rejected at parse time

  • class definitions — 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 than def, a simple name [: 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 nested def/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, @property and any decorator on a def inside a class body are unavailable. See classes.md.
  • async with statements — not yet supported
  • yield / yield from expressions — no generator functions. Generator expressions ((x for x in ...)) parse but currently materialize to a list rather than a lazy iterator (this is a known temporary divergence; see iter__generator_expr_type.py).
  • match statements — structural pattern matching is not supported.
  • del statements — neither del x nor del d[k] parse.
  • try* / except* exception groups — PEP 654 syntax rejected.
  • type aliases (PEP 695 type Foo = int).
  • async for loops and async comprehensions.
  • Wildcard imports (from m import *) — raises ImportError: "Wildcard imports (\from ... import *`) are not supported"`.

Expressions rejected at parse time

  • Complex number literals (1j, 2+3j) — NotImplementedError: The monty syntax parser does not yet support complex constants.
  • Template strings (t-strings) — PEP 750.

Starred unpacking

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.

Source nesting depth

  • 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-item with — 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.

Imports

  • Only the bundled stdlib modules listed in modules.md can be imported. Importing anything else raises ModuleNotFoundError.
  • Relative imports (from . import x) raise ImportError: "attempted relative import with no known parent package" — there is no package system.
  • __import__ is not defined.

__future__ imports

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) raises NotImplementedError: "The monty syntax parser does not yet support the 'barry_as_FLUFL' future feature". CPython accepts it, making <> the inequality operator and != a SyntaxError; Monty parses neither differently, so the import is rejected rather than silently ignored.
  • Aliasing is rejected. from __future__ import annotations as ann raises NotImplementedError: "The monty syntax parser does not yet support aliasing a \future` feature". CPython binds annto afuture._Featureobject; a no-op would bind nothing and surface as aNameError` far 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 to from __future__ import ...) raises ModuleNotFoundError — there is no __future__ module object.

Module-level dunder variables

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.

NameMonty valueCPython (script run)
__name__'__main__''__main__'
__debug__TrueTrue
__doc__NoneNone or docstring str
__spec__NoneNone
__package__NoneNone
__annotations__empty dictNameError (no annotations)

In Monty __doc__ is always None — 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.

Other module dunders CPython defines (__loader__, __file__, __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. __file__ is omitted so no host path can leak into the sandbox.

Function objects

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 is the ceiling on 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.

Ordering comparisons

<, <=, >, >= 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')CPythonMonty
(x,) == (x,), [x] == [x]TrueFalse
x in [x]TrueFalse
[x].count(x)10
[x].index(x)0ValueError
[1, x] < [1, x, 3]TrueFalse

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.

What does work

  • 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/!a conversions, and format specs.