assert
Monty deliberately diverges from CPython on assert failure messages: a
failed assert raises an AssertionError carrying a pytest-style
introspected message, so sandboxed code (and hosts feeding errors back to
users or LLMs) can see the values involved instead of a blank
AssertionError.
assert 2 == 5raisesAssertionError('assert 2 == 5'); CPython raisesAssertionError()with emptystr(e)and emptye.args.- The message is visible everywhere the exception is:
str(e),e.args[0], tracebacks, and host-side error objects. - Applies when the test is a single binary comparison with one of
==,!=,<,<=,>,>=,is,is not,in,not in: both operands’repr()s are substituted. Operands are evaluated exactly once, so side effects are not duplicated. - Any other failing test shows the falsy value’s repr instead:
assert []→assert [],assert None→assert None,assert 0→assert 0.Falseitself adds no information, soassert Falseraises a plain message-lessAssertionError, exactly like CPython. - Chained comparisons (
assert 1 < 2 > 3) andnotexpressions produceFalsewhen they fail, so they carry no introspected message.andandorreturn an operand rather than coercing it tobool, so they show that final falsy operand unless it is literallyFalse:assert 1 and []showsassert [].
assert 1 == 2, 'my message'raisesAssertionError('my message\nassert 1 == 2'); CPython raisesAssertionError('my message').e.args[0]therefore contains the combined string, not the original message object. Non-strmessages are rendered withstr()(assert [], 123→123\nassert []); CPython stores the object itself ine.args.- When the test value is literally
Falseno detail is appended, soassert False, 'msg'raisesAssertionError('msg'), the same as CPython apart from thestr()rendering of non-strmessages. - A message that stringifies to the empty string is treated as absent, so only
the detail is shown:
assert 1 == 2, ''raisesAssertionError('assert 1 == 2')(CPython raisesAssertionError('')), andassert False, ''raises a message-lessAssertionError.
- At most 120 bytes of each operand’s repr are retained, cut on a character
boundary. A truncated repr gets a three-byte
…suffix in addition to that limit. The retained-byte limit is configurable per session; see “Opt-out for embedders” below. - A failing assert calls
repr()on its operands, which CPython never does, so 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 and their__repr__s (and any side effects) don’t run at all. The temporary repr buffer and its formatting loop are not charged to theResourceTracker. - If an operand’s
__repr__(or an explicit message’s__str__) raises a catchable Python exception, that part is dropped: a bare assert falls back to a message-lessAssertionError, 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.md. The retained repr length can also be customized (an int >= 1, in bytes; 0 means “off”, not “retain no bytes”):
- Rust: pass
CompileOptions { assert_message_annotations: AssertMessageAnnotations::Off }(or::MaxBytes(n), aNonZeroU32;::from_max_bytes(n)maps 0 toOff) toMontyRun::neworMontyRepl::new. - Python:
pool.checkout(assert_message_annotations=False)(or=n). - JavaScript:
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.