Skip to content

Stack & Heap Overview

Mental Model

Python's runtime uses two memory regions with different personalities. The stack is fast, organized, and automatic -- it grows and shrinks with function calls, holding local variable names. The heap is large, flexible, and managed by the garbage collector -- it stores every actual Python object. Names on the stack point to objects on the heap.

Two Memory Areas

1. Stack

Function call frames and local names:

```python def function(): x = 10 # Name on stack y = 20 # Name on stack return x + y

Stack grows/shrinks with calls

```

2. Heap

Objects storage:

python x = [1, 2, 3] # Object on heap y = "hello" # Object on heap z = 42 # Object on heap

Stack Properties

1. Fast Access

python def compute(): a = 10 # Fast stack access b = 20 return a + b

2. Automatic Management

python def f(): x = 10 # Stack allocated return x # x deallocated when f returns

3. Limited Size

```python def recursive(n): if n == 0: return return recursive(n - 1)

Too deep causes stack overflow

```

Heap Properties

1. Dynamic Size

```python

Can grow as needed

lst = [] for i in range(1000000): lst.append(i) # Heap grows ```

2. Manual Management

```python x = [1, 2, 3] # Heap allocated

Stays until GC'd

del x # Remove reference ```

3. Slower Access

```python

Heap access slower than stack

But necessary for objects

```

What Goes Where

1. Stack

  • Function frames
  • Local name bindings
  • Return addresses
  • Parameters

2. Heap

  • All Python objects
  • Lists, dicts, strings
  • Integers, floats
  • User-defined objects

Memory Visualization

1. Example

python def process(): x = [1, 2, 3] y = x return y

Memory: ``` Stack: [process frame] x -----> y -----> [1, 2, 3] (Heap)

Heap: [1, 2, 3] object ```

2. Multiple Frames

```python def outer(): a = [1, 2] return inner(a)

def inner(param): b = param return b ```

Stack: ``` [outer frame] a -----> [1, 2] (Heap)

[inner frame]
param -----> [1, 2] (Heap) b -----> [1, 2] (Heap) ```

Frame Objects

1. Stack Frame

```python import inspect

def example(): frame = inspect.currentframe() print(frame.f_locals)

example() ```

2. Frame Info

```python def show_frame(): frame = inspect.currentframe() print(f"Function: {frame.f_code.co_name}") print(f"Line: {frame.f_lineno}")

show_frame() ```

Summary

1. Stack

  • Fast, limited size
  • Function frames
  • Name bindings
  • Auto management

2. Heap

  • Slower, dynamic size
  • All objects
  • Manual/GC management
  • Longer lifetime

Runnable Example: stack_vs_heap.py

```python """ 01_stack_vs_heap.py - Stack vs Heap Memory (Conceptual Foundation) Topic #23: Memory and Namespace """

=============================================================================

Main

=============================================================================

if name == "main":

print("=" * 70)
print("STACK VS HEAP MEMORY")
print("=" * 70)

print("""
COMPUTER MEMORY (RAM) IS DIVIDED INTO:

STACK                          HEAP
- Organized (frames)          - Unorganized (flexible)
- Automatic management        - Requires GC
- Fixed size per frame        - Variable size allocations
- Very fast                   - Slower
- LIFO structure              - Random access
- Function calls              - All Python objects
- Local variable NAMES        - Everything is an object!

KEY INSIGHT:
Variable NAMES live on stack
Variable OBJECTS live on heap
""")

# Simple example
x = 42
name = "Alice"
numbers = [1, 2, 3]

print(f"\nCreated: x={x}, name='{name}', numbers={numbers}")

print("""
MEMORY MODEL:

STACK:                  HEAP:
┌──────────┐           ┌─────────────┐
│ x    ────┼──────────→│ int: 42     │
│ name ────┼──────────→│ str: "Alice"│
│ numbers ─┼──────────→│ list: [...]│
└──────────┘           └─────────────┘

Stack holds NAMES (references)
Heap holds OBJECTS (actual data)
""")

# Function call stack
def outer():
    print("  → outer() called")
    inner()
    print("  ← outer() returns")

def inner():
    print("    → inner() called")
    x = 100
    print(f"    x = {x}")
    print("    ← inner() returns")

print("\nFunction call stack demonstration:")
outer()

print("""
STACK FRAMES:

Start:           [global]
Call outer():    [outer] [global]
Call inner():    [inner] [outer] [global]  ← Stack grows
inner returns:   [outer] [global]
outer returns:   [global]                  ← Stack shrinks

Each function gets its own frame!
""")

print("\nKey takeaways:")
print("1. Stack: Fast, automatic, organized")
print("2. Heap: Flexible, requires GC, all objects")
print("3. Names on stack point to objects on heap")
print("4. Understanding this helps debug memory issues")

print("\nSee exercises.py for practice!")

```

Exercises

Exercise 1. Write a script that creates 5 nested function calls (a calls b calls c calls d calls e). In function e, use inspect.stack() to print the entire call stack, showing each frame's function name and line number. Explain in comments which parts are on the stack vs the heap.

Solution to Exercise 1
```python
import inspect

def a():
    b()

def b():
    c()

def c():
    d()

def d():
    e()

def e():
    # Stack frames are on the stack; the frame objects
    # and their local namespaces are Python objects on the heap
    print("Call stack (innermost first):")
    for info in inspect.stack():
        print(f"  {info.function}() at line {info.lineno}")

a()
```

Exercise 2. Demonstrate that objects outlive their creating frame: write a function create_data() that creates a list, stores a weakref.ref to it in a global variable, and returns the list. After the function returns, verify that the weak reference is still alive (because the caller holds a strong reference to the returned value).

Solution to Exercise 2
```python
import weakref

global_ref = None

def create_data():
    global global_ref
    data = list(range(1000))
    global_ref = weakref.ref(data)
    return data

result = create_data()
print(f"Weak ref alive: {global_ref() is not None}")  # True
print(f"Same object: {global_ref() is result}")        # True

del result
print(f"After del: {global_ref() is not None}")        # False
```

Exercise 3. Write a function that creates a local variable referencing a large list, then reassigns that variable to None. Use tracemalloc snapshots before and after the reassignment to show that the heap memory is freed even though the stack frame is still active.

Solution to Exercise 3
```python
import tracemalloc

def free_in_scope():
    tracemalloc.start()
    snap1 = tracemalloc.take_snapshot()

    data = list(range(500_000))  # Allocate on heap

    snap2 = tracemalloc.take_snapshot()
    growth = snap2.compare_to(snap1, 'lineno')
    alloc = sum(s.size_diff for s in growth if s.size_diff > 0)
    print(f"After allocation: +{alloc / 1024:.1f} KB")

    data = None  # Free the heap object

    snap3 = tracemalloc.take_snapshot()
    shrink = snap3.compare_to(snap2, 'lineno')
    freed = sum(s.size_diff for s in shrink if s.size_diff < 0)
    print(f"After reassignment: {freed / 1024:.1f} KB freed")

    tracemalloc.stop()

free_in_scope()
```