# How Monty Limits Stack Depth: Function Calls and Data Structure Recursion

> Discover how Monty limits stack depth using ResourceTracker for function calls and DepthGuard for container operations, preventing recursion errors.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: internals
- Published: 2026-02-16

---

**Monty limits stack depth through two complementary mechanisms: `ResourceTracker::check_recursion_depth` guards Python function call frames with a default limit of 1000, while `DepthGuard` protects container operations like `repr` and `eq` with limits of 500 (release) or 100 (debug).**

The `pydantic/monty` Python virtual machine implements strict safeguards against stack overflow and infinite recursion. Unlike CPython's single recursion counter, Monty separates protection between **function-call recursion** (the Python stack) and **data-structure recursion** (deeply nested containers). This dual-layer approach prevents both VM crashes from deep call stacks and hangs from circular or deeply nested object representations.

## Function-Call Stack Limits in Monty

Monty tracks every Python function call through a dedicated resource management system that enforces hard limits on stack depth before new frames are allocated.

### The ResourceTracker Interface

At the core of Monty's recursion protection is the `ResourceTracker` trait defined in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs). This interface requires implementors to provide a `check_recursion_depth` method:

```rust
fn check_recursion_depth(&self, current_depth: usize) -> Result<(), ResourceError>;

```

The method receives the current stack depth (measured as the number of active call frames) and returns `Ok(())` if the call may proceed, or `Err(ResourceError::Recursion)` if the limit is exceeded.

### Default Recursion Limits and Configuration

Monty provides two tracker implementations with different enforcement strategies:

**NoLimitTracker** enforces the CPython-compatible default of **1000 recursive calls**, matching the behavior developers expect from standard Python:

```rust
const DEFAULT_RECURSION_LIMIT: usize = 1000;
if current_depth >= DEFAULT_RECURSION_LIMIT {
    return Err(ResourceError::Recursion);
}

```

In **debug builds**, Monty automatically lowers this limit to prevent stack overflow within the Rust VM itself before the Python-level check triggers.

For custom requirements, `ResourceLimits` allows programmatic configuration:

```rust
pub fn max_recursion_depth(mut self, limit: Option<usize>) -> Self {
    self.max_recursion_depth = limit;
    self
}

```

Passing `None` disables the limit entirely (not recommended for production), while specific values override the 1000-call default.

### Enforcement at the Call Site

The actual enforcement occurs in [`crates/monty/src/namespace.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/namespace.rs) within the `push_frame` method. Before allocating a new Python call frame, Monty queries the current stack length and validates it against the tracker:

```rust
let current_depth = self.stack.len() - 1;
heap.tracker().check_recursion_depth(current_depth)?;

```

The same check appears when reusing existing frames (line 200). If `check_recursion_depth` returns an error, Monty aborts the call setup and propagates a `ResourceError::Recursion`, which converts to a Python `RecursionError` at the exception boundary (see `ResourceError::into_exception` in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) lines 230-236).

## Data Structure Recursion Limits in Monty

Beyond function calls, Monty protects against deeply nested container objects that could cause stack overflow during operations like `repr()`, equality comparison, or hashing.

### DepthGuard for Container Operations

Monty uses a `DepthGuard` struct (defined in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)) to track remaining recursion budget for data-structure operations. Unlike the function-call tracker (which counts up), `DepthGuard` counts down from a maximum:

```rust
pub struct DepthGuard {
    depth_remaining: u16,
}

pub fn increase(&mut self) -> bool {
    // Returns true if depth remains, false if exhausted
}

pub fn increase_err(&mut self) -> Result<(), ResourceError> {
    // Returns Ok(()) or Err(ResourceError::Recursion)
}

pub fn decrease(&mut self) {
    // Restore the budget when exiting recursion
}

```

Container methods such as `py_repr_fmt`, `py_eq`, and `py_hash` accept a `&mut DepthGuard` parameter. Before recursing into child elements, they call `guard.increase_err()?`. If the guard returns an error, the operation aborts; otherwise, it proceeds and calls `guard.decrease()` when unwinding.

### Configurable Limits for Debug and Release Builds

Monty adjusts data-structure limits based on build configuration to balance safety and functionality:

- **Release builds**: `MAX_DATA_RECURSION_DEPTH = 500`
- **Debug builds**: `MAX_DATA_RECURSION_DEPTH = 100`

These constants appear in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) (lines 107 and 113). The lower debug limit prevents stack overflow during Rust development while still allowing reasonable nesting for testing.

When `DepthGuard` exhausts its budget, Monty handles the failure gracefully: representation methods typically truncate output with `...` rather than crashing, while comparison or hashing operations raise `RecursionError` to signal the failure.

## Practical Examples

### Python Recursion in Practice

Consider a classic recursive Fibonacci implementation:

```python
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

```

With Monty's default function-call limit of 1000, calling `fib(1500)` triggers a `RecursionError` before the Rust VM's native stack overflows. In debug builds, this protection kicks in earlier (typically around 100-200 calls) to safeguard the underlying Rust runtime.

### Configuring Limits from Rust

When embedding Monty in a Rust application, customize the recursion boundaries during VM initialization:

```rust
use monty::resource::{ResourceLimits, LimitedTracker};

let limits = ResourceLimits::new()
    .max_recursion_depth(Some(2000))  // Double the default Python limit
    .max_allocations(Some(10_000));   // Additional resource constraints

let tracker = LimitedTracker::new(limits);
// Initialize Monty VM with custom tracker

```

The `pydantic_monty` Python wrapper exposes these same parameters through the `Monty(..., limits=...)` constructor, allowing Python developers to adjust limits without touching Rust code.

## Summary

- **Function-call protection**: Monty enforces a default limit of **1000 recursive Python calls** (matching CPython) through `ResourceTracker::check_recursion_depth`, invoked in [`crates/monty/src/namespace.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/namespace.rs) before pushing new frames. Debug builds use lower limits to protect the Rust VM.

- **Data-structure protection**: Container operations use `DepthGuard` with limits of **500 (release)** or **100 (debug)** to prevent stack overflow during `repr`, `eq`, and `hash` operations on deeply nested objects.

- **Configurable limits**: Both systems support customization via `ResourceLimits::max_recursion_depth` and `DepthGuard` initialization, allowing embedding applications to tune safety margins for their specific workloads.

## Frequently Asked Questions

### What is the default recursion limit in Monty?

Monty defaults to **1000 recursive function calls**, identical to CPython's standard limit. This is enforced by the `NoLimitTracker` implementation in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs). However, Monty also imposes a separate **data-structure recursion limit** of 500 in release builds (100 in debug) for container operations like `repr()` and equality comparisons.

### How does Monty prevent stack overflow from deeply nested lists?

For data structures, Monty uses a `DepthGuard` mechanism rather than a simple counter. When performing recursive operations on containers (such as generating a string representation or checking equality), Monty passes a `DepthGuard` initialized to `MAX_DATA_RECURSION_DEPTH` (500 in release builds). Each recursive call consumes one unit from the guard; when exhausted, the operation either truncates (for `repr`) or raises `RecursionError` (for comparisons).

### Can I disable the recursion limits in Monty?

Yes, though it is not recommended for production use. You can disable function-call limits by passing `None` to `ResourceLimits::max_recursion_depth`:

```rust
let limits = ResourceLimits::new()
    .max_recursion_depth(None);  // Disables the limit

```

However, disabling these protections risks **stack overflow** in the underlying Rust runtime, which typically aborts the process rather than raising a catchable exception. Data-structure limits can be effectively disabled by setting very high `DepthGuard` values, though the API expects a `u16` limiting the maximum to 65,535.

### Where does Monty check recursion depth during function calls?

The enforcement point is in [`crates/monty/src/namespace.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/namespace.rs) within the `push_frame` method (line 164). Before allocating a new Python call frame, Monty calculates the current stack depth (`self.stack.len() - 1`) and invokes `heap.tracker().check_recursion_depth(current_depth)?`. If the tracker returns an error, the VM aborts the call setup and propagates a `ResourceError::Recursion`, which converts to a Python `RecursionError` at the exception boundary.