Skip to main content
Monty includes ty, a modern Python type checker from Astral, built directly into the binary. This allows you to catch type errors before code execution.

Basic Type Checking

Call type_check() on a Monty instance to verify types:
import pydantic_monty

# Code with a type error
m = pydantic_monty.Monty('"hello" + 1')

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e)
Output:
error[unsupported-operator]: Unsupported `+` operation
 --> main.py:1:1
  |
1 | "hello" + 1
  | -------^^^-
  |       |   |
  |       |   Has type `Literal[1]`
  |       Has type `Literal["hello"]`
  |
info: rule `unsupported-operator` is enabled by default

Enable at Construction

Pass type_check=True to enable type checking when creating a Monty instance:
import pydantic_monty

try:
    m = pydantic_monty.Monty(
        '"hello" + 1',
        type_check=True  # Type check during construction
    )
except pydantic_monty.MontyTypingError as e:
    print("Type error during construction:", e)
Type checking is disabled by default. This allows you to run code that may not type-check perfectly but works at runtime.

Type Stubs for External Functions

When your code uses external functions or input variables, provide type stubs to help the type checker:
import pydantic_monty

code = "result = fetch('https://example.com')"

# Define type stub for external function
type_stubs = """
def fetch(url: str) -> str:
    ...
"""

m = pydantic_monty.Monty(
    code,
    type_check=True,
    type_check_stubs=type_stubs  # Provide type definitions
)

Input Variables

Declare input variables in type stubs:
code = "result = x + 1"

# Without stubs, 'x' is undefined and fails type checking
# With stubs, we declare x's type
m = pydantic_monty.Monty(
    code,
    inputs=['x'],
    type_check=True,
    type_check_stubs='x = 0'  # Declare x as an int
)

Complex Type Definitions

type_definitions = """
from typing import Any

Messages = list[dict[str, Any]]

async def call_llm(prompt: str, messages: Messages) -> str | Messages:
    ...

prompt: str = ''
"""

code = """
async def agent(prompt: str, messages: Messages):
    while True:
        output = await call_llm(prompt, messages)
        if isinstance(output, str):
            return output
        messages.extend(output)

await agent(prompt, [])
"""

m = pydantic_monty.Monty(
    code,
    inputs=['prompt'],
    type_check=True,
    type_check_stubs=type_definitions
)
Type stubs are only used for type checking. They don’t affect runtime behavior or provide default values.

Type Checking Function Definitions

Monty validates function return types:
code = """
def foo() -> int:
    return "not an int"
"""

m = pydantic_monty.Monty(code)

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e)
Output:
error[invalid-return-type]: Return type does not match returned value
 --> main.py:2:14
  |
2 | def foo() -> int:
  |              --- Expected `int` because of return type
3 |     return "not an int"
  |            ^^^^^^^^^^^^ expected `int`, found `Literal["not an int"]`
  |
info: rule `invalid-return-type` is enabled by default

Undefined Variables

Type checking catches references to undefined variables:
m = pydantic_monty.Monty('print(undefined_var)')

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e)
Output:
error[unresolved-reference]: Name `undefined_var` used when not defined
 --> main.py:1:7
  |
1 | print(undefined_var)
  |       ^^^^^^^^^^^^^
  |
info: rule `unresolved-reference` is enabled by default

Display Formats

Control how type errors are displayed:

Full Format (Default)

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e.display())  # or str(e)

Concise Format

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e.display('concise'))
Output:
main.py:1:1: error[unsupported-operator] Operator `+` is not supported between objects of type `Literal["hello"]` and `Literal[1]`

Type and Message Only

try:
    m.type_check()
except pydantic_monty.MontyTypingError as e:
    print(e.display('type-msg'))
Output:
error[unsupported-operator]: Unsupported `+` operation

JavaScript Display Options

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

const m = new Monty('"hello" + 1')

try {
  m.typeCheck()
} catch (error) {
  if (error instanceof MontyTypingError) {
    // Full format (default)
    console.log(error.displayDiagnostics())
    
    // Concise format
    console.log(error.displayDiagnostics('concise'))
  }
}

Catching Type Errors

MontyTypingError is a subclass of MontyError:
import pydantic_monty

try:
    m = pydantic_monty.Monty('"hello" + 1', type_check=True)
except pydantic_monty.MontyTypingError as e:
    # Specific type error handling
    print("Type error:", e.display('concise'))
except pydantic_monty.MontyError as e:
    # General Monty error
    print("Monty error:", e)

Prefix Code in Manual Type Checking

You can provide prefix code when calling type_check() manually:
m = pydantic_monty.Monty('result = x + 1')

# Without prefix, x is undefined and fails
try:
    m.type_check()
except pydantic_monty.MontyTypingError:
    print("Failed without prefix")

# With prefix declaring x, it passes
m.type_check(prefix_code='x = 0')
print("Passed with prefix")
Use prefix_code parameter for one-off type checks. For construction-time type checking, use type_check_stubs instead.

Type Checking with Async Code

Type checking works with async functions:
code = """
async def fetch_data():
    return "data"

await fetch_data()
"""

m = pydantic_monty.Monty(code)
m.type_check()  # Passes

Best Practices

1

Use type hints

Add type hints to your function definitions for better error messages:
def add(a: int, b: int) -> int:
    return a + b
2

Provide comprehensive stubs

Include all external functions and input variables in your type stubs:
stubs = """
def external_func(x: int) -> str: ...
input_var: int = 0
"""
3

Enable early for LLM code

Use type_check=True during construction to catch errors before execution:
m = pydantic_monty.Monty(llm_generated_code, type_check=True)
4

Handle type errors gracefully

Catch MontyTypingError and provide feedback to your LLM or user:
try:
    m = pydantic_monty.Monty(code, type_check=True)
except pydantic_monty.MontyTypingError as e:
    # Send error back to LLM for correction
    print(f"Type error: {e.display('concise')}")
Type checking does not guarantee runtime correctness. It catches many common errors but cannot prevent all runtime issues (e.g., division by zero, logic errors).

When to Skip Type Checking

Type checking is disabled by default because:
  1. Dynamic code patterns: Some valid runtime patterns don’t type check
  2. Rapid prototyping: Type checking adds overhead during development
  3. Partial type coverage: Not all code paths may have complete type information
You can skip type checking and rely on runtime validation when:
  • Code is simple and well-tested
  • Performance is critical
  • You’re using dynamic patterns that are hard to type

Next Steps

External Functions

Learn how to define external function type stubs

Error Handling

Handle runtime errors and exceptions

Build docs developers (and LLMs) love