limitations/assert.md
assertMonty deliberately diverges from CPython on assert failure messages: failed
asserts raise an AssertionError carrying a pytest-style introspected message,
so sandboxed code (and hosts feeding errors back to users/LLMs) can see the
values involved instead of a blank AssertionError.
assert carries a message (CPython raises an empty AssertionError)assert 2 == 5 raises AssertionError('assert 2 == 5'); CPython raises
AssertionError() with empty str(e) and empty e.args.str(e), e.args[0],
tracebacks, and host-side error objects.==, !=, <, <=, >, >=, is, is not, in, not in:
both operands' repr()s are substituted. Operands are evaluated exactly
once — side effects are not duplicated.assert [] → assert [], assert None → assert None,
assert 0 → assert 0 — except False itself, which adds no information:
assert False raises a plain message-less AssertionError, exactly like
CPython.assert 1 < 2 > 3) and not expressions produce
False when they fail, so they carry no introspected message. and and or
return an operand rather than coercing it to bool, so they show that final
falsy operand unless it is literally False: assert 1 and [] shows
assert [].assert test, msg appends the detail on a new lineassert 1 == 2, 'my message' raises
AssertionError('my message\nassert 1 == 2'); CPython raises
AssertionError('my message').e.args[0] contains the combined string, not the original
message object. Non-str messages are rendered with str()
(assert [], 123 → 123\nassert []); CPython stores the object
itself in e.args.False no detail is appended, so
assert False, 'msg' raises AssertionError('msg') — the same as CPython
apart from the str() rendering of non-str messages.assert 1 == 2, '' raises AssertionError('assert 1 == 2')
(CPython raises AssertionError('')), and assert False, '' raises a
message-less AssertionError.… suffix in addition to that
limit. The retained-byte limit is configurable per session; see "Opt-out for
embedders" below.repr() on its operands, which CPython never does:
user __repr__ side effects run. Rendering is streamed and stops at the
truncation cap, so parts of a container beyond the cap are never repr'd —
their __repr__s (and any side effects) don't run at all. The temporary repr
buffer and its formatting loop are not charged to the ResourceTracker.__repr__ (or an explicit message's __str__) raises a
catchable Python exception, that part is dropped: a bare assert falls back to
a message-less AssertionError, while an explicit-message assert keeps
whichever of message/detail rendered successfully. Terminal internal and
resource errors propagate instead.Introspected annotations can be disabled per session, restoring CPython's empty message for bare asserts. This does not remove Monty's other exception constructor differences: with annotations disabled, an explicit assert message must still be a string; see exceptions. The retained repr length can also be customized (an int >= 1, in bytes; 0 means "off", not "retain no bytes"):
CompileOptions { assert_message_annotations: AssertMessageAnnotations::Off } (or ::MaxBytes(n), a NonZeroU32;
::from_max_bytes(n) maps 0 to Off) to MontyRun::new or
MontyRepl::new.pool.checkout(assert_message_annotations=False) (or =n).pool.checkout({ assertMessageAnnotations: false }) (or : n;
both the native and wasm-worker pools).Monty's Rust, Python, and JavaScript surfaces default to messages on. The
CPython compatibility worker ignores this option and always uses CPython's
plain AssertionError behavior.