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

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

The Heap struct in 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:

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

The Value enum in 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 (lines 1075-1087) increments the reference counter for a given HeapId:

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 (lines 1090-1115) handles both decrementing the counter and deallocating objects when the count reaches zero:

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 (lines 1918-1948) provides the safe way to duplicate a heap reference:

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 (lines 1655-1669) ensures proper cleanup when a value is discarded:

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 demonstrates ownership-transfer semantics. When pushing items, the caller retains responsibility for reference counting:

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:

// 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 (lines 360-770) and 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 and 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 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 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), 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.

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 →