From Call Stack to Heap: How Python Manages Execution and Data in Memory

Madhura AnandMadhura Anand
8 min read

When Python runs your code, it manages memory using two key areas:

🧠 Stack v/s Heap?

πŸ”§ The Stack

  • The call stack is a contiguous block of memory reserved by the operating system at program start.

  • It grows and shrinks as functions are called and return.

  • It operates on a LIFO (Last In, First Out) model.

  • Stack memory is thread-local and automatically managed β€” allocation is a simple pointer increment; deallocation is a pointer decrement.

  • Function calls store their stack frames here β€” which include local variables and return addresses.

    | Question | Answer | | --- | --- | | Where is the stack memory? | In the process’s virtual memory, near the top (high addresses) | | Who allocates it? | The operating system, at process or thread creation | | How is it used? | Managed using CPU stack pointer, grows/shrinks with function calls | | How big is it? | OS-defined, typically 1–8MB per thread (configurable) |


πŸ”© The Heap

The heap is an entirely separate region of memory provided by the OS. It is:

  • A region of virtual memory reserved by the OS for long-term dynamic allocation.

  • Allocations from the heap are done via manual control or an allocator (Python handles this behind the scenes). i.e Not automatically structured like the stack.

  • Heap memory is usually backed by pages, and the OS maps these into your process address space via system calls like mmap().

  • Allocated and deallocated using system calls (e.g., malloc/free in C, or brk/mmap at the OS level).

  • Managed by a dynamic memory allocator β€” in Python’s case, pymalloc (for small objects) or the OS (for large ones).

  • Not tied to the function call stack β€” meaning:

    • Objects on the heap persist beyond the function call that created them.

    • Any scope can reference them (global, local, etc.)

  • | Question | Answer | | --- | --- | | Where is the heap memory? | In the process’s virtual memory, typically located below the stack, growing upward | | Who allocates it? | The operating system, via system calls (brk, mmap) when requested by Python’s memory manager | | How is it used? | Dynamically allocates and frees memory for objects like lists, dicts, classes | | How big is it? | Limited by available virtual memory or system limits (can be GBs in size) | | Who manages cleanup? | Python’s garbage collector and reference counting automatically free unused memory | | Is access fast? | Slower than stack β€” requires pointer dereferencing and memory bookkeeping |


🧡 Memory Map of a Python Process

When a program runs (including a Python script), the operating system creates a process with a virtual memory layout that looks roughly like this:

HIGH MEMORY ADDRESSES
+-----------------------------+
|         Stack              | ← grows down
+-----------------------------+
|      Heap (malloc/pymalloc)| ← grows up
+-----------------------------+
|  Global/Static Variables   |
+-----------------------------+
|       Code Segment         |
+-----------------------------+
LOW MEMORY ADDRESSES
  • The stack starts near the top of the virtual address space and grows downward.

  • The heap starts after the static data and grows upward.

  • This design prevents them from immediately colliding and helps the OS detect overflows.


4. 🧠 Stack vs Heap at the Hardware Level

Stack:

  • Managed using a dedicated stack pointer register (RSP/ESP) that always points to the top of the stack.

  • CPU instructions like call, ret, push, and pop directly manipulate the stack and this register.

  • Though managed by the OS and CPU, the stack is backed by real RAM via the process’s virtual address space.

Heap:

  • The heap has no dedicated CPU register.

  • Memory is allocated at runtime via system calls like brk() or mmap(), often through functions like malloc() or Python’s memory manager.

  • Allocation returns a pointer to the memory; it's up to the program to manage it.

  • Access is indirect β€” you follow pointers to interact with heap data.

  • There's no automatic structure like push/pop; memory must be explicitly managed (or garbage collected in Python).

  • Like the stack, heap memory is also backed by physical RAM, but is dynamically mapped in the process’s address space.

FeatureStackHeap
CPU RegisterUses dedicated stack pointer (RSP/ESP)❌ No dedicated register
Access MethodDirectly accessed via push, pop, call, ret instructionsAccessed via pointers returned from allocators like malloc
Memory ManagementAutomatically grows/shrinks with function calls and returnsMust be manually managed (or via garbage collection in Python)
StructureLinear LIFO (Last In First Out)No fixed structure β€” random access possible
Allocation System CallsReserved at process/thread startUses brk(), mmap() under the hood
PersistenceTemporary β€” tied to function lifetimeLong-lived β€” survives beyond function calls
Backed ByReal RAM via virtual memoryReal RAM via virtual memory

πŸ“ž What Happens When a Function Is Called?

πŸ”Ή 1. The Call Stack

The call stack keeps track of function calls. Each time a function is invoked, Python creates a stack frame for that function. This frame contains:

  • The function’s parameters

  • All local variables

πŸ§ͺ Example:

def add(x, y):
    result = x + y
    return result

def main():
    a = add(2, 3)
    print(a)

main()

πŸ” Stack Behavior

Let’s trace the execution of the program step-by-step and visualize how the call stack and local variables evolve.


▢️ Step 1: main() is called (Line 8)

Call Stack:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  main()    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Local Variables:

main: a = ?

▢️ Step 2: add(2, 3) is called (Line 5)

Call Stack:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ add(2, 3)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  main()    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Local Variables:

add: x = 2, y = 3, result = ?

▢️ Step 3: Inside add(), result = x + y (Line 2)

Call Stack:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ add(2, 3)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  main()    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Local Variables:

add: x = 2, y = 3, result = 5

▢️ Step 4: return result (Line 3) β†’ back to main()

Call Stack:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  main()    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Local Variables:

main: a = 5

▢️ Step 5: print(a) is executed (Line 6)

πŸ–¨οΈ Output:

5

Call Stack remains:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  main()    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

βœ… Step 6: End of main()

Call Stack: (empty)

The program completes and all memory is cleaned up.

Local variables like x, y, result, and a exist only inside their respective function frames. Once the function finishes, they're gone.


πŸ”Ά 2. The Heap

While the call stack handles how functions are executed, Python uses a separate memory area β€” the heap β€” to store the data those functions operate on. Whenever you create complex objects like lists, dictionaries, or class instances, Python places them in the heap so they can persist beyond a single function call, be shared across scopes, and grow dynamically.

πŸ“¦ How Python Stores Data on the Heap

We’ve seen how Python uses the call stack to manage function execution. But what happens when we create data β€” like a list β€” that persists or is shared between scopes?

Let’s walk through it with an example function:


πŸ”§ Example: Heap Allocation in Action

def make_shopping_list():
    shopping_list = ['apples', 'bananas']
    return shopping_list

cart = make_shopping_list()

🧠 What Happens in Memory?

πŸ”Ή Step 1: make_shopping_list() is called

  • Python creates a stack frame for the function.

  • Inside the function, a list object ['apples', 'bananas'] is created and stored in the heap.

  • The variable shopping_list (just a reference) lives in the stack frame.

Stack (during function):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ make_shopping_list()       β”‚
β”‚ ─ shopping_list ─────┐     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”€β”€β”€β”˜
                       β–Ό
Heap:
[ 'apples', 'bananas' ]  ← list object
   ↑        ↑
'apples'  'bananas'       ← individual string objects (also in heap)

πŸ”Ή Step 2: Function returns the list

  • The reference to the list object is returned.

  • The local variable shopping_list is discarded when the stack frame is popped.

  • Outside the function, cart now points to the same list.

Stack (after return):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   cart ─────┐ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚β”€β”˜
              β–Ό
Heap:
[ 'apples', 'bananas' ]  ← still lives in heap
   ↑        ↑
'apples'  'bananas'

πŸ” Why Is shopping_list in the Stack?

  • Variable names like shopping_list and cart live in their respective namespaces:

    • Inside functions β†’ in the call stack

    • Globally β†’ in the global namespace

  • They don't hold data themselves β€” they hold references (pointers) to the actual data, which lives in the heap.


βœ… Quick Recap

ComponentLocationExplanation
'apples', 'bananas'HeapImmutable string objects
['apples', 'bananas']HeapMutable list object that holds references
shopping_listStackLocal variable inside function (temporary reference)
cartStack/GlobalVariable in outer scope (holds same reference)

🧠 Why Use the Heap?

  • The list object needs to outlive the function that created it.

  • By storing it in the heap, Python ensures the object remains alive as long as there’s a reference to it.

  • When no variable points to the object anymore, Python’s garbage collector reclaims the memory.


In Python, understanding how the stack handles execution and how the heap manages data is key to writing efficient, bug-free code that truly respects memory.

0
Subscribe to my newsletter

Read articles from Madhura Anand directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Madhura Anand
Madhura Anand

Data Professional with experience in civic data, AI applications, and building data pipelines to solve real-world problems. My work with government datasets has given me unique domain knowledge of how AI can drive operational efficiency and informed decision-making. I’m now focused on bringing this expertise to AI-driven companies, with a passion for building products with a purpose turning complex data into solutions that matter. πŸ“§ madhura.anand@outlook.com Disclaimer: All opinions and views expressed in any posts/blogs are my own and do not reflect the views or values of my organization.