How Monty Type Checking Works with Python Type Hints: A Complete Guide

Monty performs optional static type checking using a Ruff-based engine that validates Python type hints before execution, raising MontyTypingError with detailed diagnostics when violations are detected.

Monty is a Python interpreter that can optionally enforce type safety through built-in static analysis. By integrating a Ruff-based type checker, Monty validates Python type hints against your code before runtime, catching type errors early while maintaining compatibility with standard typing module constructs.

How Monty Validates Python Type Hints

Runtime Typing Module Support

Monty ships with a minimal typing module implementation located in crates/monty/src/modules/typing.rs. This module provides standard marker objects like Any, List, Optional, and TYPE_CHECKING (always set to False) so that from typing import List works at runtime. These markers serve as runtime placeholders only and do not trigger analysis themselves.

Explicit Type Checking Invocation

Type checking occurs only when explicitly requested. When creating a Monty instance, you can set type_check=True in the constructor or call the type_check() method later. This invokes the Ruff-based type checker from the monty_type_checking crate. The public API is defined in pydantic_monty/_monty.pyi, which exposes the type_check method and MontyTypingError exception.

Type Checking Implementation Details

The core implementation resides in monty_type_checking/src/type_check.rs. The process works as follows:

  1. Virtual File System Creation: The checker creates a temporary in-memory file system containing the source code and optional stub declarations.
  2. Stub Injection: If provided, stub code is injected as a leading from <stub> import * statement. This allows declaring variable types or external function signatures without affecting runtime execution.
  3. Ruff Analysis: The ruff library's check_types routine analyzes the file and returns diagnostic information.
  4. Error Adjustment: Line numbers in diagnostics are adjusted to account for injected stub code, ensuring error locations match the original user code.
  5. Exception Raising: If errors exist, Monty raises MontyTypingError, a subclass of MontyError. The exception provides a .display(format, color) method supporting formats like full, concise, and json.

Architecture and Data Flow

Monty's type checking system consists of five integrated components:

Step Component Responsibility
A typing module (crates/monty/src/modules/typing.rs) Supplies runtime marker objects (TYPE_CHECKING = False)
B Python wrapper (pydantic_monty/_monty.pyi) Exposes Monty.__new__, type_check(), and MontyTypingError
C monty_type_checking crate (src/type_check.rs) Creates virtual file system, runs Ruff checker, adjusts diagnostics
D MontyTypingError (_monty.pyi) Wraps diagnostics with customizable display formatting
E Test suite (crates/monty-python/tests/test_type_check.py) Validates constructor flags, stub handling, and output formats

The data flow proceeds as follows:


User code → Monty constructor (type_check=True)
                ↓
   monty_type_checking::type_check()
                ↓
        ruff::check_types()
                ↓
   Diagnostics → MontyTypingError (if errors) → Python exception

Practical Usage Examples

Enabling Type Checking at Construction

To validate code immediately upon loading, pass type_check=True when creating a Monty instance:

import pydantic_monty

# This raises MontyTypingError because "hello" + 1 is a type error

try:
    pydantic_monty.Monty('"hello" + 1', type_check=True)
except pydantic_monty.MontyTypingError as exc:
    print(exc.display(format='concise'))

The constructor validates the code before any execution occurs, ensuring type safety from the start.

Running Type Checks After Construction

You can also defer type checking until after instantiation using the type_check() method:

m = pydantic_monty.Monty('x + 1')
m.type_check()  # Raises MontyTypingError if type violations exist

This method is defined in pydantic_monty/_monty.pyi and invokes the full Ruff-based analysis pipeline.

Providing Stub Declarations

For code that relies on external variables or functions, use the type_check_stubs parameter to inject type declarations without affecting runtime:

stubs = """
x: int
def fetch(url: str) -> str: ...
"""

m = pydantic_monty.Monty(
    'result = fetch(url) + str(x)',
    type_check=True,
    type_check_stubs=stubs,
)

The stub code is injected as a virtual from <stub> import * statement and automatically stripped from diagnostic line numbers, ensuring error locations match your original source.

Customizing Diagnostic Output

MontyTypingError provides flexible formatting options for diagnostics:

try:
    m.type_check()
except pydantic_monty.MontyTypingError as exc:
    # Available formats: 'full', 'concise', 'json', etc.

    print(exc.display(format='json', color=True))

The display method supports multiple output formats and colorization, making it easy to integrate Monty's type checking into various development workflows.

Summary

  • Monty provides optional static type checking for Python code using a Ruff-based engine that validates type hints before execution.
  • The system relies on a minimal runtime typing module (crates/monty/src/modules/typing.rs) to support standard imports without triggering analysis.
  • Type checking is invoked via the type_check=True constructor flag or the type_check() method, both defined in pydantic_monty/_monty.pyi.
  • The implementation in monty_type_checking/src/type_check.rs creates a virtual file system, injects optional stubs, runs ruff::check_types(), and adjusts line numbers to match original source.
  • Errors are reported through MontyTypingError, which offers customizable display formats including full, concise, and json.
  • Stub declarations via type_check_stubs allow external type definitions without affecting runtime execution.

Frequently Asked Questions

How does Monty handle the typing module at runtime?

Monty includes a minimal implementation of the typing module in crates/monty/src/modules/typing.rs that provides standard marker objects like Any, List, and Optional. The TYPE_CHECKING constant is always set to False. These markers serve as runtime placeholders only and do not perform any static analysis themselves.

Can I use type checking without executing the code?

Yes. When you pass type_check=True to the Monty constructor, the type checker runs before any code execution begins. If type errors are detected, Monty raises MontyTypingError immediately, preventing the program from running. This ensures that only type-correct programs execute when type checking is enabled.

What formats are available for type error diagnostics?

MontyTypingError supports multiple output formats through its .display() method. Available formats include full for complete diagnostic information, concise for abbreviated output, and json for machine-readable structured data. You can also enable color output by passing color=True to the display method.

How do stub declarations work in Monty's type checker?

Stub declarations allow you to provide type signatures for external variables or functions without affecting runtime execution. You pass stub code via the type_check_stubs parameter when creating a Monty instance. The type checker injects this code as a virtual import and automatically adjusts diagnostic line numbers to match your original source code, excluding the injected stub lines.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →