The short version:
- The stack stores short-lived, fixed-size data tied to function calls.
- The heap stores data whose size or lifetime is not known locally.
- Stack allocation is usually cheaper; heap allocation is more flexible.
- Python hides this choice. Rust makes it visible through types such as
Box.
These are useful models, not guarantees: compilers and runtimes may optimise where data actually lives.
Stack
Each function call gets a stack frame containing things such as arguments, local values, and a return address. Frames are removed in reverse order when calls return, so allocation is little more than moving a pointer.
The stack is fast and automatically managed, but limited in size. Very deep recursion can overflow it.
Typical stack examples include:
- A counter, flag, or other fixed-size value whose size is known before the function runs.
- A small fixed-size array, such as a 32-byte checksum buffer.
- Function arguments and local values that only need to exist until the current function returns.
- Temporary state used while processing one operation, when its lifetime is tied to that operation.
Heap
Heap memory can outlive the function that requested it and can hold dynamically sized values. Allocating and releasing it requires more bookkeeping; ownership, garbage collection, or reference counting decides when it can be reused.
Typical heap examples include:
- Data read from a file or network connection when its size is not known until runtime.
- A buffer that must grow as more input arrives, when a fixed-size stack allocation would not be enough.
- Data returned from a function because it must remain alive after the function's stack frame has been removed.
- Shared data whose lifetime is determined by the last consumer rather than by one function call.
Rust: the distinction is visible
fn main() {
let count: i32 = 3; // fixed-size value; normally stack
let name = String::from("pelican"); // String metadata on stack, text on heap
let boxed = Box::new(42); // Box pointer on stack, 42 on heap
println!("{count} {name} {boxed}");
}
Rust releases boxed when its owner leaves scope. No garbage collector is needed. Moving ownership can change which variable controls heap data without copying that data.
Python: the distinction is hidden
def total() -> int:
numbers = [1, 2, 3]
return sum(numbers)
In CPython, the function call has an interpreter frame, while the list and integers are Python objects managed on the heap. numbers is a reference to the list, not the list stored directly in a conventional stack slot.
Python normally reclaims objects through reference counting, with a cyclic garbage collector as backup. That convenience adds overhead but removes most manual memory decisions.
Practical rule
In Python, focus on object lifetimes, copies, and container sizes. In Rust, also notice whether a type owns heap data and when that ownership moves. Reach for heap allocation when values must grow, escape their scope, or have an unknown size—not because “heap” or “stack” is inherently better.
Quiz
Test your understanding, from basics to more advanced ownership questions. Open each answer only after you have had a go.
1. What is the difference between a Rust array and a vector?
let array = [1, 2, 3]; // Fixed length: always three elements.
let mut vector = vec![1, 2, 3]; // Growable: can hold more elements.
vector.push(4); // The vector now has four elements.
Show answer
An array has a fixed length, which is part of its type: [i32; 3] is different from [i32; 4]. A Vec<i32> is a growable, heap-backed collection whose length can change at runtime. You can mutate an array's elements if it is declared mut, but you cannot add or remove elements.
2. Why can stack allocation be faster than heap allocation?
let count = 3; // Usually a simple stack allocation.
let numbers = Box::new([1, 2, 3]); // Requires heap-allocation bookkeeping.
Show answer
Stack allocation can often be handled by moving a stack pointer, while heap allocation needs bookkeeping to find and manage a suitable region of memory. This is a useful generalisation, not a guarantee about every compiler or runtime.
3. What usually happens if a function creates a local value and then returns?
fn make_count() -> i32 {
let count = 3; // This local value is tied to the function call.
count
} // The function's stack frame is removed here.
Show answer
The function's stack frame is removed, so values whose lifetime is tied to that frame are no longer available. Data that must survive the return needs an appropriate longer-lived owner, often involving heap allocation.
4. Why might a dynamically growing buffer use the heap instead of a fixed-size stack array?
let fixed = [0u8; 1024]; // Fixed size, reserved up front.
let mut growing = Vec::new(); // Heap-backed storage can grow at runtime.
Show answer
Its final size is not known in advance, and it may need to grow beyond the space reserved for a fixed-size local array. A heap-backed buffer can request more space at runtime and can live for as long as its owner needs it.
5. In the Rust example, where are the String metadata and its text normally stored?
let name = String::from("pelican"); // A String tracks a pointer, length, and capacity.
Show answer
The String value itself—typically a pointer, length, and capacity—is stored in the local variable, normally on the stack. The character data owned by the String is stored on the heap.
6. In the Rust example, what does Box::new(42) put on the stack and what does it put on the heap?
let boxed = Box::new(42); // Consider the owner and the value it owns separately.
Show answer
The Box value is a pointer-like owner stored in the local variable, normally on the stack. The i32 value 42 is allocated on the heap and owned by that Box.
7. Why does returning a heap-backed value from a Rust function not leave a dangling reference?
fn make_value() -> Box<i32> {
let value = Box::new(42); // Ownership can move to the caller.
value
} // The Box moves out; its heap allocation remains alive.
Show answer
The heap allocation is owned by a value whose ownership can be moved to the caller. The function's stack frame disappears, but the allocation remains alive because its new owner still exists.
When the final owner is dropped, Rust deallocates the heap allocation. The memory becomes available to the program's allocator for reuse; it is not necessarily returned to the operating system or made directly available to other processes.
8. In Python, why is a list variable better described as a reference to an object than as the list itself stored in a stack slot?
numbers = [1, 2, 3]
alias = numbers # This assigns another reference; it does not copy the list.
Show answer
The variable refers to a Python list object, while the list's contents and object bookkeeping are managed elsewhere, typically on the heap in CPython. Python's implementation details can vary, but the reference-versus-object distinction explains why assigning or passing a list does not automatically copy all its elements.
9. What is the practical risk of making a recursive function call itself too deeply?
def count_down(n):
if n > 0:
count_down(n - 1) # Each call needs another stack frame.
Show answer
Each call needs another stack frame. If recursion becomes too deep, the available stack can be exhausted, causing a stack overflow—or, in Python, typically a recursion-related error before the native stack is exhausted.
Heap allocation does not remove the stack frames created by recursive calls.
10. Why is knowing that a value is “on the stack” not enough to understand how it is stored and preserved during program execution?
fn square(value: i32) -> i32 {
value * value // An optimiser may keep this in a register or remove it.
}
Show answer
Knowing that a value is on the stack does not by itself explain who owns the data, how long it must live, or whether it can safely outlive the current call. Stack and heap are useful models, but a compiler or runtime may keep a value in a register, eliminate it, move it, or use another representation.