# Classes

Sandboxed Python code in Monty can define simple classes. A `class`
statement with instance methods, `__init__`, `__eq__`, `__repr__`/`__str__`,
and class variables works. The class body has a real scope (like CPython's
class-body code object), so class variables may be arbitrary expressions
and may reference earlier class variables:

```python
class Foo:
    count = 0

    def __init__(self, a: int) -> None:
        self.a = a

    def bar(self) -> int:
        return self.a * 2

    def __repr__(self) -> str:
        return f'Foo(a={self.a})'
```

See `test_cases/class__basic.py` and `test_cases/class__repr.py`.

The host can also construct dataclass and namedtuple values (using the
`MontyObject` API) and pass them in; those are a separate mechanism whose
methods dispatch back to the host (see `test_cases/dataclass__basic.py`).

## Supported surface

Per the `limitations/` convention this file documents only *divergences* from
CPython; the supported surface is summarized here just to bound what the
divergences below apply to. Working, CPython-matching features: instance
methods, `__init__` (full parameter shapes), instance and class attribute
get/set (including `setattr(Foo, ...)` and function-attributes-become-methods),
bound methods, class variables (arbitrary expressions, evaluated in a real
suspendable class-body scope), **class decorators** (`@deco class Foo`),
`__repr__`/`__str__`/`__enter__`/`__exit__`/`__eq__`/`__hash__` dispatch,
`obj.__class__`, `Foo.__name__`, `Foo.__doc__`/`obj.__doc__`,
`Foo.__annotations__` (ordered; values stringized and provisional — see
[typing.md](typing.md)), `type(obj)`/`isinstance(obj, Foo)`, and the 3-arg
`type()` constructor. The
`__enter__`/`__exit__` divergences are in [with.md](with.md). Everything else
below is where Monty differs from or does not implement CPython behaviour.

## Dynamic class creation — `type(name, bases, dict)`

The 3-arg `type()` form creates classes at runtime with CPython's validation
order and error wording, but with these divergences:

- **`bases` must be the empty tuple `()`.** Any non-empty bases tuple — even
  `(object,)` — raises `TypeError: type() bases are not supported` (the
  runtime counterpart of the parse-time `class Foo(Bar)` rejection; no
  inheritance).
- **Keywords are always rejected.** CPython forwards extra keywords to
  `__init_subclass__`; Monty has no `__init_subclass__`, but the error
  message matches what `object.__init_subclass__` produces
  (`A.__init_subclass__() takes no keyword arguments`).
- Only `__doc__` is synthesized into the namespace when absent (as `None`,
  matching CPython). CPython also sets `__module__`, `__qualname__`,
  `__dict__`, `__weakref__`, etc. — those attributes raise `AttributeError`
  in Monty, as for compiled classes.
- **Non-string namespace keys raise `TypeError`**
  (`non-string key (int) in the namespace of class 'A'`). CPython accepts
  them with only a `RuntimeWarning`; Monty has no warnings machinery, so it
  raises rather than silently accepting.

## Divergences from CPython

- **Default `repr`** (no user `__repr__`) is `<Foo object at 0x..>` using the
  **bare** class name, where CPython uses the qualified name
  `<module.Foo object at 0x..>`.
- **`__init__`/method argument-count errors** name the method without the
  class qualifier — e.g. `__init__() missing 1 required positional argument:
  'y'`, where CPython says `Foo.__init__() missing ...`.
- **`type(obj)`** returns the class object (so identity works), but its own
  `repr` is `<class 'Foo'>` with the bare name (CPython qualifies it).
- **The class object is not itself a `type` instance.** The bare name `type`
  resolves to the builtin `type` *function*, not a type object, so
  `type(Foo) is type` is `False` (CPython: `True`) and `isinstance(Foo, type)`
  raises `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a
  union` (CPython: `True`). There is no metaclass.
- **Bound methods report `function`, not `method`.** `type(obj.method)` is
  `<class 'function'>` where CPython says `<class 'method'>` — Monty has no
  dedicated `method` type.
- **Ordering comparisons on instances raise, but a user `__lt__`/`__gt__`/… is
  not dispatched.** `a < b` on instances of a class with no comparison dunders
  raises `TypeError: '<' not supported between instances of 'Foo' and 'Foo'`
  (matching CPython). A class that *defines* `__lt__` etc. still raises — those
  dunders are not dispatched (see the not-dispatched dunder list below).
- **`__repr__`/`__str__` cannot suspend**: they are run to completion
  synchronously, so a `__repr__`/`__str__` that calls an external/OS function
  raises rather than yielding to the host. `__init__` and regular methods
  *can* suspend on external/OS calls.
- **Only a plain-function `__init__` can suspend.** When `__init__` is bound to
  something else (a builtin, another class, a bound method, ...), it is called
  with CPython's descriptor-binding semantics (no `self` prepended unless it is
  a plain function) and CPython's `None`-return contract is enforced — but it
  runs to completion synchronously, so it cannot yield to the host, and an
  external-function `__init__` raises `NotImplementedError` rather than
  suspending.
- **`__eq__`/`__hash__` cannot suspend**: like `__repr__`/`__str__` they run to
  completion synchronously, so one that calls an external/OS function raises
  rather than yielding to the host. An exception raised by `__eq__` terminates
  the run instead of being catchable by a `try` around the comparison.
- **Ordering dunders are still not dispatched** — see the entry above.
  Instances are always truthy (no `__bool__`/`__len__` dispatch).
- **Bound methods compare and hash by identity**: each `obj.method` access
  creates a fresh object, so `obj.method == obj.method` is `False` and two
  accesses hash differently. CPython compares/hashes bound methods by
  `(instance, func)`, making separate accesses equal.
- **Bound-method `repr`** is the bare `<bound method>`; CPython renders
  `<bound method Foo.m of <__main__.Foo object at 0x..>>`.
- **Assigning `Foo.__name__`** stores an ordinary class member: unlike CPython
  (where `type.__name__` is a metaclass descriptor whose setter renames the
  class), it does not rename the class, so `Foo.__name__` reads and `repr(Foo)`
  keep the original name while instances see the member.
- **Assigning `obj.__class__`** stores an ordinary instance attribute rather
  than reassigning the object's class. `obj.__class__ = X` then reads back `X`,
  but `type(obj)` and `isinstance` still report the original class — an
  internally inconsistent object. CPython either reassigns the class (for a
  compatible class) or raises `TypeError: __class__ must be set to a class, not
  '...' object`.
- **Recursive/deep `__repr__`/`__str__` raises `RecursionError` earlier than
  CPython.** A `__repr__` (or `__str__`) that reprs `self`, or a deep chain of
  instances whose reprs nest (e.g. a long linked list), re-enters the
  interpreter on the native Rust call stack once per nesting level (unlike
  ordinary Python-level recursion, which lives on a heap-allocated frame stack
  and is bounded at 1000 by the normal recursion limit). To avoid a native
  stack overflow (which would abort the process — fatal for the
  in-process/wasm API, which shares the host process), this native re-entry is
  capped independently at a much lower, fixed depth, raising a catchable
  `RecursionError` once exceeded. The practical effect: infinite `__repr__`
  recursion now raises `RecursionError` (matching CPython's outcome, though
  not its exact depth), but a deep-but-finite chain that CPython's default
  1000-frame limit would still successfully render may raise `RecursionError`
  in Monty where CPython succeeds — a deliberate divergence traded for avoiding
  native stack overflow. The same cap also applies to synchronous callback
  evaluation such as `map()`, `filter()`, `sorted()`/`list.sort(key=...)`,
  `min()`/`max(key=...)`, and exotic `__init__` recursion (see
  `limitations/resource_limits.md`'s "Recursion" section).
- **Comprehensions in the class body** can see class variables, because Monty
  inlines comprehensions into the enclosing scope. In CPython a comprehension
  has its own scope that skips the class scope, so only the *leftmost iterable*
  is evaluated in class scope and the body cannot see class variables
  (`[n + offset for n in nums]` referencing a class variable `offset` raises
  `NameError` in CPython but succeeds in Monty).
- **Same-name collision is rejected, not resolved.** When an enclosing-function
  local and a class variable share a name *and* a method captures the enclosing
  one, CPython keeps the two distinct (a class-dict entry vs. a closure cell).
  Monty maps one name to a single slot and so cannot represent both; it raises
  `NotImplementedError` at compile time ("class member 'x' that shadows a
  captured variable of the same name from an enclosing scope") rather than
  miscompiling. Distinct names work fine.

## Crossing the host boundary (`pydantic_monty` / `@pydantic/monty`)

TODO: change dataclasses to `class` and use that.

A user-defined **class object or instance has no faithful host representation**.
When one is returned to a host caller as a run's result value, it is converted
to its `repr()` **string**, not a proxy or a value that preserves attributes:

```python
result = session.feed_run('class A:\n    x = 1\nA()')
# result is the str '<A object at 0x..>', NOT an object with `.x`
```

`A` (the class) and `A()` (an instance) both surface as their repr text (e.g.
`"<class 'A'>"` and `"<A object at 0x..>"`), so the host cannot read
attributes, call methods, or reconstruct the object. This is unlike the values
Monty *does* round-trip structurally (numbers, str/bytes, list/tuple/dict/set,
datetimes, and host-supplied dataclasses/namedtuples, which dispatch back to the
host). To return class data to the host, convert it inside the sandbox first —
e.g. return a `dict` of the fields.

## What does NOT exist for user code

- `class Foo(Bar): ...` — no inheritance, no MRO, no `super()` (rejected at
  parse time: "class inheritance and metaclasses"; the runtime equivalent
  `type('Foo', (Bar,), {})` raises `TypeError`, see above).
- Metaclasses, `__init_subclass__`, `__set_name__`, and any other
  metaclass-driven namespace customization.
- `__slots__`, descriptors (`__get__` / `__set__` / `__delete__`).
- Abstract base classes (`abc.ABC`, `@abstractmethod`).
- Method decorators — `@classmethod`, `@staticmethod`, `@property`, and any
  decorator on a `def` inside a class body (rejected at parse time). Decorators
  on classes and on non-method functions are supported.
- **Classes are barely introspectable**: `__dict__`, `__bases__` and `dir()`
  are all unavailable (`cls.__name__` and `cls.__annotations__` work — the
  latter with stringized values, see [typing.md](typing.md)). A class decorator
  can therefore discover fields and nothing else.
- **Tracebacks from decorator application point at the whole `class` statement**
  (a span from the first decorator through the body, with the body elided as
  `...<N lines>...`), where CPython pins the individual decorator that raised.
  Every decorator in a stack reports that same location; only the callee frame
  identifies which one raised.
- Dunder protocols other than `__init__`, `__repr__`, `__str__`,
  `__enter__`, `__exit__`, `__iter__`, `__next__`, `__contains__`, `__eq__`,
  and `__hash__`: `__new__`, `__call__`, `__getitem__`, `__setitem__`,
  `__add__`, `__ne__`, `__bool__`, etc. are not dispatched for user-defined
  instances. `__ne__` is always the negation of `__eq__`, as CPython derives it
  by default, so a custom `__ne__` is ignored.
- `__iter__` / `__next__` / `__contains__` **are** dispatched, but like
  `__repr__`/`__str__` they run synchronously, so one that calls an external or
  OS function cannot suspend and raises `NotImplementedError`. Two related
  protocols are still not dispatched, so a class relying on either is not
  iterable:
  - the legacy `__getitem__`-only fallback — CPython iterates a class defining
    `__getitem__` but not `__iter__` from index 0 until `IndexError`, while
    Monty reports it as not iterable. (Note `monty -t` accepts `iter(obj)` for
    such a class, so this fails only at runtime — see [iter.md](iter.md).)
  - `__reversed__` — so `reversed(obj)` on any user instance raises
    `TypeError: '{cls}' object is not reversible`. That matches CPython for a
    class defining neither `__reversed__` nor `__len__` + `__getitem__`, and
    diverges for one that does.
- `__next__` is looked up on the class only, never the instance `__dict__`, and
  a `StopIteration` raised anywhere inside it ends the iteration — including one
  that propagates out of a nested call, where CPython's PEP 479 protections
  apply only to generators, which Monty does not have.
- **A `__contains__` returning a user instance is always `True`.** The result is
  coerced by Monty's truthiness, which reports every instance as truthy (see
  above), where CPython's `PyObject_IsTrue` consults the returned object's
  `__bool__`/`__len__`. Every other return type coerces as CPython does.
- Attribute-access hooks are **never** dispatched: `__getattr__`,
  `__getattribute__`, `__setattr__`, `__delattr__`, and `__del__`. A missing
  attribute always raises the default `AttributeError` even when the class
  defines `__getattr__`, and attribute writes always go straight to the
  instance `__dict__`.
- Introspection attributes other than `__name__`, `__doc__`, `__annotations__`
  and `obj.__class__`: `Foo.__dict__`, `obj.__dict__`, `Foo.__bases__`,
  `Foo.__mro__`, `Foo.__qualname__`, `Foo.__module__`, and explicit
  `obj.__repr__()` / `obj.__str__()` calls when the class defines none — all
  raise `AttributeError`.
- Class-body statements other than a `def`, a simple `name [: T] = <expr>`
  variable assignment, `pass`, `...`, or a docstring — e.g. `if`/`for`/`while`
  in the class body, or tuple/multiple assignment targets (rejected at parse
  time).
- Assignment expressions (`:=`) that bind in the class-body scope — in
  class-variable values, method parameter defaults, and lambda parameter
  defaults (rejected at parse time). In CPython the walrus target becomes a
  class member (`class C: x = (y := 5)` gives `C.y`); Monty's class-namespace
  assembly only records directly-assigned names, so the syntax is reserved
  rather than silently dropping the binding. A walrus inside a lambda *body*
  (`f = lambda: (z := 1)`) binds in the lambda's own scope and works. A walrus
  in a comprehension in the class body is also rejected (CPython rejects that
  too, but as a `SyntaxError` with different wording). A walrus in an
  *annotation* (`x: (y := int) = 5`) runs in Monty — annotation expressions are
  captured as source text (stringized) and never evaluated, so the walrus never
  binds — where CPython raises `SyntaxError`. This one follows from annotations
  never being evaluated, so it would change if they ever are (see
  [typing.md](typing.md)).
- `del obj.attr` (the `del` statement is unsupported generally).

## `FrozenInstanceError`

Raised when assigning to a field of a frozen host-supplied dataclass.
Subclass of `AttributeError` — `except AttributeError:` catches it, as in
CPython's `dataclasses` module. (User-defined classes in the sandbox are
never frozen.)
