Skip to main content
Monty provides comprehensive error handling with detailed tracebacks, exception type preservation, and multiple error display formats.

Error Types

Monty has three main error types, all subclasses of MontyError:

MontySyntaxError

Raised when code has syntax errors during parsing:
import pydantic_monty

try:
    m = pydantic_monty.Monty('def')  # Invalid syntax
except pydantic_monty.MontySyntaxError as e:
    print(str(e))  # "Expected an identifier at byte range 3..3"
    inner = e.exception()
    print(type(inner))  # <class 'SyntaxError'>

MontyRuntimeError

Raised when code encounters a runtime error during execution:
try:
    m = pydantic_monty.Monty('1 / 0')
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    inner = e.exception()
    print(type(inner))  # <class 'ZeroDivisionError'>

MontyTypingError

Raised when type checking detects type errors:
try:
    m = pydantic_monty.Monty('"hello" + 1', type_check=True)
except pydantic_monty.MontyTypingError as e:
    print(e.display('concise'))
All Monty errors inherit from MontyError, so you can catch all Monty-specific errors with a single except clause.

Catching All Monty Errors

import pydantic_monty

try:
    m = pydantic_monty.Monty(code, type_check=True)
    result = m.run()
except pydantic_monty.MontyError as e:
    # Handles all Monty error types
    print(f"Error: {e}")

Getting the Inner Exception

For MontySyntaxError and MontyRuntimeError, you can access the underlying Python exception:
try:
    m = pydantic_monty.Monty('undefined_variable')
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    inner = e.exception()
    print(type(inner))  # <class 'NameError'>
    print(str(inner))   # "name 'undefined_variable' is not defined"
    print(isinstance(inner, NameError))  # True

Common Runtime Exceptions

Monty preserves standard Python exception types:
m = pydantic_monty.Monty('1 / 0')
try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    assert isinstance(e.exception(), ZeroDivisionError)

Tracebacks

Monty provides full traceback information for runtime errors:
code = """
def inner():
    raise ValueError('error')

def outer():
    inner()

outer()
"""

m = pydantic_monty.Monty(code)
try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    print(e.display())
Output:
Traceback (most recent call last):
  File "main.py", line 7, in <module>
    outer()
    ~~~~~~~
  File "main.py", line 5, in outer
    inner()
    ~~~~~~~
  File "main.py", line 2, in inner
    raise ValueError('error')
ValueError: error

Accessing Traceback Frames

You can access individual traceback frames programmatically:
try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    frames = e.traceback()
    for frame in frames:
        print(f"{frame.filename}:{frame.line} in {frame.function_name}")
        print(f"  {frame.source_line}")

Frame Properties

Each frame has these properties:
frame.filename       # 'main.py'
frame.line          # Line number (1-indexed)
frame.column        # Column number (1-indexed)
frame.end_line      # End line number
frame.end_column    # End column number
frame.function_name # Function name or '<module>'
frame.source_line   # The actual source code line
You can also get frame data as a dict:
frame_dict = frame.dict()
# {
#     'filename': 'main.py',
#     'line': 5,
#     'column': 1,
#     'end_line': 5,
#     'end_column': 6,
#     'function_name': '<module>',
#     'source_line': 'foo()'
# }

Display Formats

Control how errors are displayed:

Full Traceback (Default)

try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    print(e.display())  # Full traceback with frames
    # or
    print(e)  # Same as display()

Type and Message

try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    print(e.display('type-msg'))  # "ValueError: bad value"

Message Only

try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    print(e.display('msg'))  # "bad value"
str(error) returns the same as error.display('type-msg') for easy printing.

JavaScript Error Handling

import { Monty, MontySyntaxError, MontyRuntimeError, MontyTypingError } from '@pydantic/monty'

try {
  const m = new Monty('1 / 0')
  m.run()
} catch (error) {
  if (error instanceof MontySyntaxError) {
    console.log('Syntax error:', error.message)
  } else if (error instanceof MontyRuntimeError) {
    console.log('Runtime error:', error.message)
    console.log('Traceback:', error.traceback())
  } else if (error instanceof MontyTypingError) {
    console.log('Type error:', error.displayDiagnostics())
  }
}

Exception Handling Within Monty Code

Monty code can catch and handle exceptions using try/except:
code = """
try:
    1 / 0
except ZeroDivisionError as e:
    result = 'caught'
result
"""

m = pydantic_monty.Monty(code)
print(m.run())  # 'caught'

Exception Hierarchies

Monty respects Python exception hierarchies:
code = """
try:
    raise ZeroDivisionError('math error')
except ArithmeticError:  # Parent class catches child
    caught = 'parent'
except ZeroDivisionError:
    caught = 'child'
caught
"""

m = pydantic_monty.Monty(code)
print(m.run())  # 'parent'
Supported exception hierarchies:
  • ArithmeticErrorZeroDivisionError, OverflowError
  • LookupErrorKeyError, IndexError
  • RuntimeErrorNotImplementedError, RecursionError

Exceptions from External Functions

Exceptions raised by external functions propagate to Monty code:
def fail():
    raise ValueError('external error')

code = """
try:
    fail()
except ValueError:
    caught = True
caught
"""

m = pydantic_monty.Monty(code)
result = m.run(external_functions={'fail': fail})
print(result)  # True
Exception types are preserved across the boundary. A ValueError raised by an external function is a ValueError in Monty code.

Uncaught External Exceptions

If Monty code doesn’t catch an exception from an external function, it propagates to the host:
def fail():
    raise RuntimeError('uncaught')

m = pydantic_monty.Monty('fail()')

try:
    m.run(external_functions={'fail': fail})
except pydantic_monty.MontyRuntimeError as e:
    inner = e.exception()
    print(type(inner))  # <class 'RuntimeError'>
    print(str(inner))   # 'uncaught'

Exceptions During Iterative Execution

You can resume execution with an exception:
code = """
try:
    result = external_func()
except ValueError:
    caught = True
caught
"""

m = pydantic_monty.Monty(code)
progress = m.start()

# Resume with an exception instead of a return value
result = progress.resume(exception=ValueError('test error'))
print(result.output)  # True

Uncaught Exception in Resume

code = 'external_func()'
m = pydantic_monty.Monty(code)
progress = m.start()

try:
    # Exception not caught by Monty code
    progress.resume(exception=ValueError('uncaught'))
except pydantic_monty.MontyRuntimeError as e:
    inner = e.exception()
    print(type(inner))  # <class 'ValueError'>

Best Practices

1

Catch specific errors first

Handle specific errors before catching general MontyError:
try:
    m.run()
except pydantic_monty.MontySyntaxError:
    # Handle syntax errors
    pass
except pydantic_monty.MontyRuntimeError:
    # Handle runtime errors
    pass
except pydantic_monty.MontyError:
    # Catch-all for other Monty errors
    pass
2

Log tracebacks for debugging

Always log the full traceback for runtime errors:
try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    logger.error(f"Runtime error: {e.display()}")
3

Provide helpful error messages

When raising exceptions from external functions, provide clear messages:
def fetch_data(url: str):
    if not url.startswith('https://'):
        raise ValueError(f"URL must use HTTPS: {url}")
    # ...
4

Handle errors in Monty code when possible

Encourage your agent to use try/except in generated code:
code = """
try:
    result = risky_operation()
except ValueError as e:
    result = default_value
result
"""
Always catch MontyError or its subclasses. Don’t let Monty exceptions crash your application.

Error Messages for LLMs

When providing error feedback to LLMs, use concise formats:
try:
    m = pydantic_monty.Monty(llm_generated_code, type_check=True)
    result = m.run()
except pydantic_monty.MontyTypingError as e:
    # Send concise type error back to LLM
    feedback = e.display('concise')
    print(f"Type error: {feedback}")
except pydantic_monty.MontyRuntimeError as e:
    # Send type and message back to LLM
    feedback = e.display('type-msg')
    print(f"Runtime error: {feedback}")

Testing Error Handling

Test that your code handles errors correctly:
import pytest
import pydantic_monty

def test_handles_zero_division():
    m = pydantic_monty.Monty('1 / 0')
    
    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:
        m.run()
    
    inner = exc_info.value.exception()
    assert isinstance(inner, ZeroDivisionError)

def test_handles_missing_function():
    m = pydantic_monty.Monty('missing_func()')
    
    with pytest.raises(pydantic_monty.MontyRuntimeError) as exc_info:
        m.run()
    
    inner = exc_info.value.exception()
    assert isinstance(inner, NameError)
    assert "'missing_func'" in str(inner)

Repr for Debugging

All error types have helpful repr() implementations:
try:
    m.run()
except pydantic_monty.MontyRuntimeError as e:
    print(repr(e))
    # MontyRuntimeError(ValueError: test message)

Next Steps

Type Checking

Catch errors before runtime with type checking

External Functions

Learn how external function errors propagate

Build docs developers (and LLMs) love