# How Monty Handles Reference Counting for Memory Management: A Deep Dive into the Rust Implementation

> Explore Monty's reference counting for memory management. Discover how it uses explicit inc_ref and dec_ref operations for deterministic object lifetime control within the Rust implementation.

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

---

**Monty implements manual, deterministic reference counting where every heap-allocated Python object stores a `refcount` field in the `Heap` structure, with explicit `inc_ref` and `dec_ref` operations managing object lifetimes through `clone_with_heap` and `drop_with_heap` helper methods.**

Monty, the high-performance Python implementation from pydantic/monty, uses a sophisticated reference counting system for memory management that provides deterministic object lifetimes without garbage collection pauses. Unlike typical Rust programs that rely solely on ownership and borrowing, Monty implements manual reference counting to support Python's dynamic semantics where multiple variables can reference the same object. This article examines the core mechanisms, source files, and safety guarantees that make Monty's approach both efficient and memory-safe.

## Core Architecture of Monty's Reference Counting

Monty's memory management rests on three tightly-coupled components that work together to track every heap allocation.

### The Heap Structure in [`heap.rs`](https://github.com/pydantic/monty/blob/main/heap.rs)

The `Heap` struct in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) serves as the central allocator and reference count tracker. It stores every heap-allocated value as a `HeapValue` entry containing a manual reference counter:

```rust
pub struct HeapValue {
    refcount: usize,          // <-- the manual reference counter
    data: Option<HeapData>,   // actual Python object payload
    hash_state: HashState,    // cached hash for hashable types
}

```

When a value is created, the heap assigns it a fresh `HeapId` and initializes `refcount` to 1. The `Heap` provides two fundamental operations for reference counting: `inc_ref` and `dec_ref`.

### Value References in [`value.rs`](https://github.com/pydantic/monty/blob/main/value.rs)

The `Value` enum in [`crates/monty/src/value.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/value.rs) represents all Python values. Heap objects are always represented as `Value::Ref(HeapId)`, which contains an index into the `Heap`. All container types—lists, dictionaries, sets—store `Value::Ref` variants to point to their contents.

This design means that copying a `Value` does not automatically copy the underlying object; instead, it requires explicit reference count management through helper methods.

## Reference Counting Operations in Detail

Monty's reference counting relies on explicit operations rather than implicit compiler-generated code. This section examines the critical methods that manage object lifetimes.

### Incrementing References with `inc_ref`

The `Heap::inc_ref` method in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) (lines 1075-1087) increments the reference counter for a given `HeapId`:

```rust
pub fn inc_ref(&mut self, id: HeapId) {
    let value = self.entries
        .get_mut(id.index())
        .expect("Heap::inc_ref: slot missing")
        .as_mut()
        .expect("Heap::inc_ref: object already freed");
    value.refcount += 1;
}

```

This operation is invoked whenever a new variable needs to reference an existing object, such as during assignment or when passing arguments to functions.

### Decrementing and Deallocating with `dec_ref`

The `Heap::dec_ref` method in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) (lines 1090-1115) handles both decrementing the counter and deallocating objects when the count reaches zero:

```rust
pub fn dec_ref(&mut self, id: HeapId) {
    let slot = self.entries.get_mut(id.index()).expect("Heap::dec_ref: slot missing");
    let entry = slot.as_mut().expect("Heap::dec_ref: object already freed");
    if entry.refcount > 1 {
        entry.refcount -= 1;
    } else if let Some(value) = slot.take() {
        self.free_list.push(id);
        // … free children recursively …
    }
}

```

When the reference count drops to zero, the method removes the entry from the heap, returns the slot to a free-list for reuse, and recursively decrements references held by the object's children.

### Safe Cloning via `clone_with_heap`

The `Value::clone_with_heap` method in [`crates/monty/src/value.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/value.rs) (lines 1918-1948) provides the safe way to duplicate a heap reference:

```rust
pub fn clone_with_heap(&self, heap: &mut Heap<impl ResourceTracker>) -> Self {
    match self {
        Self::Ref(id) => {
            heap.inc_ref(*id);
            Self::Ref(*id)
        }
        other => other.clone_immediate(),
    }
}

```

This method distinguishes between heap-allocated values (which need reference count increments) and immediate values like integers (which can be copied directly).

### Explicit Dropping with `drop_with_heap`

The `Value::drop_with_heap` method in [`crates/monty/src/value.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/value.rs) (lines 1655-1669) ensures proper cleanup when a value is discarded:

```rust
pub fn drop_with_heap(self, heap: &mut Heap<impl ResourceTracker>) {
    if let Self::Ref(id) = self {
        heap.dec_ref(id);
    }
}

```

This method must be called whenever a `Value` goes out of scope or is overwritten, such as when variables are reassigned in Python namespaces.

## Container-Specific Reference Management

Mutable containers in Monty own the references they contain, requiring careful coordination during operations that add or remove elements.

### Lists and the Two-Phase Extend Pattern

The `List` type in [`crates/monty/src/types/list.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/types/list.rs) demonstrates ownership-transfer semantics. When pushing items, the caller retains responsibility for reference counting:

```rust
pub fn push(&mut self, item: Value, heap: &mut Heap<impl ResourceTracker>) {
    // The caller already incremented item’s refcount if it was a Ref.
    self.items.push(item);
}

```

For operations like `extend` that clone existing elements, Monty uses a two-phase pattern (lines 360-380) to avoid borrow checker conflicts:

```rust
// Phase 1 – shallow copy
let cloned_items = self.items.iter()
    .map(|v| v.copy_for_extend())
    .collect::<Vec<_>>();
// Phase 2 – increment refs
for v in &cloned_items {
    if let Value::Ref(id) = v {
        heap.inc_ref(*id);
    }
}

```

This pattern separates the collection phase (which doesn't require mutable heap access) from the reference incrementing phase (which does), preventing borrow checker errors while maintaining correct reference counts.

### Dictionaries and Sets

Similar patterns appear in [`crates/monty/src/types/dict.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/types/dict.rs) (lines 360-770) and [`crates/monty/src/types/set.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/types/set.rs). These containers provide helper methods like `inc_refs_for_entries` to bulk-increment reference counts when inserting multiple items, ensuring that keys and values maintain correct ownership semantics during complex operations.

## VM Integration and Safety Guarantees

The bytecode virtual machine in [`crates/monty/src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/mod.rs) and [`crates/monty/src/bytecode/vm/collections.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/bytecode/vm/collections.rs) integrates tightly with the reference counting system. When pushing values onto the operand stack, the VM assumes ownership of existing references without additional increments. When discarding stack slots, it invokes `Value::drop_with_heap` to decrement counts.

This design provides several safety guarantees:

- **No implicit leaks**: Every `Value::Ref` must be cloned via `clone_with_heap` or explicitly transferred. The compiler-enforced `drop_with_heap` ensures every reference is released.
- **Panic on misuse**: Attempting to clone a reference using standard `clone` methods panics, making accidental reference sharing obvious during development.
- **Deterministic deallocation**: When `dec_ref` reaches zero, the system immediately walks the object graph and frees resources, ensuring external handles (like file descriptors) close predictably.

## Summary

- Monty implements **manual reference counting** in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) using `HeapValue` structs with explicit `refcount` fields.
- The `Heap` provides `inc_ref` and `dec_ref` methods (lines 1075-1115) to manage object lifetimes deterministically.
- `Value::Ref` variants in [`crates/monty/src/value.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/value.rs) represent heap objects, requiring `clone_with_heap` and `drop_with_heap` for safe duplication and disposal.
- Containers use **ownership-transfer semantics** and **two-phase copying** (e.g., `copy_for_extend` followed by `inc_ref`) to avoid borrow checker conflicts while maintaining correct counts.
- The VM integration ensures **no implicit leaks**, **panic on misuse**, and **deterministic deallocation** of Python objects.

## Frequently Asked Questions

### How does Monty's reference counting differ from CPython's implementation?

While both systems use deterministic reference counting, Monty implements its counting in Rust with explicit `clone_with_heap` and `drop_with_heap` methods that integrate with Rust's borrow checker. CPython uses raw pointer manipulation and macro-based reference counting in C. Monty's approach provides memory safety guarantees through Rust's type system, panicking on incorrect clone operations rather than allowing use-after-free errors.

### Why does Monty use manual reference counting instead of Rust's ownership system alone?

Python's semantics require that multiple variables can reference the same object simultaneously, with mutations visible through all references. Rust's ownership system prohibits this aliasing by default. Monty uses manual reference counting to implement Python's shared mutability model safely, tracking when the last reference disappears so the system can reclaim memory while allowing the flexible reference patterns Python developers expect.

### What happens when a reference count reaches zero?

When `Heap::dec_ref` detects that `refcount` has reached zero (lines 1090-1115 in [`heap.rs`](https://github.com/pydantic/monty/blob/main/heap.rs)), it removes the `HeapValue` from the entries vector, pushes the slot ID onto a free-list for reuse, and recursively decrements references held by the object's children. This immediate deallocation ensures deterministic resource cleanup, preventing memory leaks and ensuring that external resources like file handles close predictably when no longer referenced.

### How does Monty handle reference cycles?

The current implementation focuses on deterministic reference counting for acyclic object graphs, immediately deallocating objects when their reference count reaches zero. While the source analysis does not detail specific cycle detection mechanisms, Monty likely requires explicit breaking of cycles through weak references or specific cleanup protocols, similar to strategies used in other reference-counted systems. The immediate deallocation path in `dec_ref` correctly handles all acyclic references, ensuring no memory leaks in standard usage patterns.