How Monty's Built-in Type Checking Works: A Deep Dive into the Rust Implementation
Monty's built-in type checking leverages Ruff's ty crates through an in-memory Salsa database to analyze Python code across Rust, Python, JavaScript, and CLI interfaces without touching the host filesystem.
Monty is a Python-to-everything compiler developed by Pydantic that ships with a fast, accurate type-checking subsystem. This article explores how Monty's built-in type checking works under the hood, powered by Ruff's semantic analysis engine and exposed through a unified API across multiple language bindings.
Architecture Overview
The type-checking implementation lives in the monty-type-checking crate and relies on Ruff's ty crates (ty_python_semantic, ty_module_resolver, etc.). At its core, the system uses an in-memory Salsa database to manage incremental analysis, ensuring that no real filesystem operations are required during type checking.
The In-Memory Salsa Database
The foundation of Monty's type checking is the MemoryDb struct defined in crates/monty-type-checking/src/db.rs. This database implements the Salsa traits required by Ruff's analysis stack:
SourceDb– Provides access to the virtual file system (system,vendored,files)Db– Stores language-level settings including lint rules, analysis settings, and Python versionModuleResolverDb– Handles search-path resolution for imports
The database uses a TestSystem and the vendored monty-typeshed files, creating a completely sandboxed environment isolated from the host filesystem.
Type Checking Workflow
When type_check() is invoked in crates/monty-type-checking/src/type_check.rs, the system executes a six-step pipeline:
1. Register the Virtual Root
let src_root = SystemPathBuf::from("/");
db.files().try_add_root(&db, &src_root, FileRootKind::Project);
This establishes the virtual filesystem root where all source files will reside.
2. Write Source Files
let main_path = src_root.join(python_source.path);
db.write_file(&main_path, python_source.source_code)?;
If a stub file is provided, Monty writes it first, then injects a from <stub> import * line at the top of the main source file to ensure the type checker sees the type annotations.
3. Initialize the Program
Program::from_settings(
&db,
ProgramSettings {
python_version: PythonVersionWithSource {
version: db.python_version(),
source: PythonVersionSource::default(),
},
python_platform: PythonPlatform::default(),
search_paths,
},
);
Creating the Program is mandatory—it forces the ty database to initialize its internal caches. Without this step, check_types would panic.
4. Execute Type Analysis
let main_file = system_path_to_file(&db, &main_path)?;
let mut diagnostics = check_types(&db, main_file);
The check_types function returns a Vec<Diagnostic> from Ruff's diagnostic engine.
5. Filter and Adjust Diagnostics
Monty applies a filter to remove spurious "await outside of async function" diagnostics that the current ty version emits for Monty-specific code patterns.
If a stub import was injected, adjust_annotation_span corrects line numbers by subtracting the offset length, but only for spans belonging to the original main file.
6. Package Results
Remaining diagnostics are wrapped in a TypeCheckingDiagnostics struct that holds:
- The raw
Vec<Diagnostic> - An
Arc<Mutex<MemoryDb>>for rendering - Formatting preferences (
DiagnosticFormat, color flags)
This struct implements Display and Debug to produce output identical to Ruff's textual format.
Public API and Integration Points
The primary entry point is the type_check function exposed in crates/monty-type-checking/src/lib.rs:
pub fn type_check(
python_source: &SourceFile<'_>,
stubs_file: Option<&SourceFile<'_>>,
) -> Result<Option<TypeCheckingDiagnostics>, String>
Return values indicate:
Ok(None)– Source type-checks cleanlyOk(Some(diagnostics))– One or more typing errors detectedErr(String)– Unexpected internal error (e.g., virtual filesystem I/O)
Integration Across Front-Ends
| Crate / File | Integration Method |
|---|---|
monty-python – src/monty_cls.rs |
Invoked when type_check=True is passed; translates Python strings into SourceFile and raises MontyTypingError |
monty-js – src/monty_cls.rs |
Exposes typeCheck option on JS Monty class; throws MontyTypingError exception |
monty-cli – src/main.rs |
Direct Rust API call via --type-check flag; prints diagnostics to stdout |
Tests – crates/monty-type-checking/tests/main.rs |
Exercises success paths, error detection, stub handling, and formatting |
Usage Examples
Direct Rust Usage
use monty_type_checking::{SourceFile, type_check};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let src = SourceFile::new(
r#"
def greet(name: str) -> str:
return f"Hello, {name}"
greet(42) # <-- type error
"#,
"example.py",
);
match type_check(&src, None)? {
None => println!("✅ No type errors"),
Some(diag) => println!("❌ Type errors:\n{diag}"),
}
Ok(())
}
Output format matches Ruff's diagnostic style:
TypeCheckingDiagnostics:
example.py:6:8: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
Python API
from pydantic_monty import Monty
code = """
def greet(name: str) -> str:
return f"Hello, {name}"
greet(42)
"""
m = Monty(code, type_check=True)
try:
m.run()
except MontyTypingError as exc:
print(exc) # Same diagnostics as Rust API
JavaScript API
import { Monty, MontyTypingError } from "@pydantic/monty";
const code = `
def greet(name: str) -> str:
return f"Hello, {name}"
greet(42)
`;
const m = new Monty(code, { typeCheck: true });
try {
m.run();
} catch (e) {
if (e instanceof MontyTypingError) {
console.error(e.message); // Formatted diagnostics
}
}
CLI Usage
$ monty run example.py --type-check
TypeCheckingDiagnostics:
example.py:6:8: error: Argument 1 to "greet" has incompatible type "int"; expected "str" [arg-type]
Summary
- Monty's built-in type checking leverages Ruff's
tycrates (ty_python_semantic,ty_module_resolver) through themonty-type-checkingcrate. - The system uses an in-memory Salsa database (
MemoryDb) defined incrates/monty-type-checking/src/db.rsto provide a sandboxed virtual filesystem. - The type-checking pipeline involves registering virtual roots, writing source files, initializing a
Program, executingcheck_types, and filtering/adjusting diagnostics. - Results are packaged in
TypeCheckingDiagnosticswhich implementsDisplayto produce Ruff-compatible error formatting. - The
type_checkfunction incrates/monty-type-checking/src/lib.rsserves as the unified entry point for Python (monty-python), JavaScript (monty-js), CLI (monty-cli), and direct Rust usage.
Frequently Asked Questions
How does Monty's type checker differ from running Ruff directly?
Monty embeds Ruff's ty crates internally rather than shelling out to a separate process. This allows Monty to run type checking in a completely sandboxed, in-memory environment using MemoryDb without touching the host filesystem. The integration also enables seamless error propagation across Rust, Python, and JavaScript APIs with consistent diagnostic formatting.
Can I use stub files (.pyi) with Monty's type checker?
Yes. Monty supports stub files through the optional stubs_file parameter in the type_check function. When provided, Monty writes the stub to the virtual filesystem first, then injects a from <stub> import * line at the top of the main source file. The system automatically adjusts diagnostic line numbers to account for this injected import line.
What happens if the type checker encounters an internal error?
If the type checker encounters an unexpected internal error—such as a failure in the virtual filesystem operations—the type_check function returns Err(String) with a descriptive message. Successful type checking returns Ok(None) for clean code or Ok(Some(TypeCheckingDiagnostics)) when typing errors are detected. This three-state return pattern allows callers to distinguish between "no errors," "type errors," and "system failures."
Is Monty's type checking compatible with all Python versions?
Monty uses the ProgramSettings struct to configure Python version and platform settings when initializing the type checker. The database stores language-level settings including the target Python version, which is passed to Ruff's ty engine. While the specific supported versions depend on the underlying ty crates, Monty's architecture allows version configuration through the Db trait implementation in crates/monty-type-checking/src/db.rs.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →