Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Stack vs. Heap: Python & TypeScript Memory Demystified

Recommended resources

Introduction

In our last lesson, we explored the high-level runtime and memory management models of TypeScript, Python, and C#, focusing on how they execute code and automatically reclaim memory. We saw the trade-offs between JIT compilation and interpretation, and between different garbage collection strategies like tracing and reference counting.

Today, we're going to zoom in and examine memory organization at a more fundamental level. This lesson focuses on a core concept in computer science: the distinction between the stack and the heap. Our goal is to fulfill the learning outcome: Trace stack vs. heap allocation in a small Python and TypeScript snippet to observe memory management differences. Understanding this division is essential for diagnosing performance issues, preventing memory leaks, and grasping why seemingly similar code can behave differently across languages. This knowledge is a prerequisite for writing efficient, robust applications, a key skill for any full-stack developer.


The Two Regions of Memory: Stack and Heap

When your program runs, the operating system allocates a block of memory for it. The language's runtime divides this memory primarily into two regions: the stack and the heap. They serve different purposes and have vastly different characteristics.

  • The Stack: Think of the stack as a neat pile of plates. It's a highly organized memory region that operates on a "Last-In, First-Out" (LIFO) principle. When a function is called, a new "stack frame" is pushed onto the top of the stack. This frame holds the function's local variables, arguments, and return address. When the function finishes, its frame is popped off the stack, and all its local data is instantly gone. Allocation and deallocation are extremely fast—it's just a matter of moving a single pointer up or down. The stack has a fixed size, and if you try to put too much on it (e.g., through infinite recursion), you get a "stack overflow" error.

  • The Heap: The heap, in contrast, is like a large, messy storage closet. It's used for data that needs to live longer than a single function call or whose size isn't known at compile time. This includes things like objects, arrays, and other complex data structures. Allocating memory on the heap is a more complex process: the runtime has to find a free block of memory of the right size. Deallocation is handled by the garbage collector we discussed in the last lesson. Accessing heap memory is generally slower than accessing stack memory because it involves following pointers from the stack to the heap.

The key question for any language is: what goes where? The answer is fundamental to its design.


Stack vs. Heap in TypeScript/JavaScript

JavaScript, and by extension TypeScript, makes a clear distinction between primitive types and reference types. This distinction directly maps to how they use the stack and the heap.

To understand this better, let's turn to a helpful resource that contrasts Python and JavaScript. While its goal is broader, its explanation of JavaScript's type system is excellent for our purposes.

Python vs JavaScript for Pythonistas – Real Python

This article section explains the crucial difference between primitive and reference types in JavaScript, which is the key to understanding its memory model.

Please read the section titled JavaScript Types. Focus on the definitions of primitive and reference types, and the examples showing how assignment works differently for each. This will form the basis of our comparison.

As you've just read, the rules are quite clear:

  • Primitive Types (number, string, boolean, null, undefined, symbol, bigint) have their values stored directly on the stack. When you assign a primitive to another variable, you are creating a complete copy of the value.
  • Reference Types (Object, Array, Function, etc.) are more complex. The actual object data is allocated on the heap. The variable on the stack holds only a reference (a memory address) pointing to that heap data. When you assign a reference type to another variable, you are only copying the reference, not the object itself. Both variables now point to the same object on the heap.

Let's look at a visual representation. The image below shows exactly this concept with code you're familiar with from your Node.js experience.

This diagram illustrates how TypeScript/JavaScript organizes memory. Primitive variables and references are stored on the stack. The actual data for reference types like objects, arrays, and functions is stored on the heap, with the stack variables holding pointers to these heap locations.

Notice how students and numbers are variables on the stack, but they only hold an address (the arrows) that points to the actual object and array data residing on the heap. In contrast, if we had a variable let age = 15;, both the variable name age and the value 15 would reside on the stack.

When we do const otherStudent = student;, we are only copying the arrow, not the object itself. That's why changing otherStudent.age would also change student.age—they both point to the same block of memory on the heap.


Stack vs. Heap in Python

Python's model is philosophically different and, in some ways, simpler: everything is an object. There is no formal distinction between "primitive" and "reference" types in the way JavaScript has. An integer is an object, a string is an object, and a list is an object.

This leads to a different memory allocation strategy:

  • A variable name (like x) exists within a stack frame.
  • This name is bound to a reference.
  • This reference points to an object that always lives on the heap.

This is true for all data types, including numbers and strings, which were primitives in JavaScript. CPython (the standard implementation) may perform optimizations like pre-allocating small integers, but the conceptual model remains: all data values are objects on the heap.

The image below shows a Python program's memory during a recursive function call. It clearly separates the "Frames" (the call stack) from the "Objects" (the heap).

This diagram shows the memory layout in Python. The "Frames" column represents the call stack, containing local variables that are references. The "Objects" column represents the heap, where all data (including numbers, tuples, and functions) is stored as objects. The arrows show how stack variables reference heap objects.

Look at the listSum frame. The variables numbers, f, and rest are just names on the stack. They hold references that point to objects on the heap. Even the number 1 is an object on the heap that f points to. This is a fundamental difference from JavaScript, where the number 1 would live directly on the stack.

This means that in Python, an assignment like y = x always copies a reference. Both x and y will point to the same object on the heap, regardless of whether that object is a number, a list, or a custom class instance. Modifiability then depends on whether the object itself is mutable (like a list) or immutable (like a number or a string).


Tying It All Together

Let's summarize the key differences in a table:

Feature TypeScript/JavaScript (V8) Python (CPython)
Numbers, Booleans Primitive type. Value stored on the stack. Object. Data stored on the heap. Reference on the stack.
Strings Primitive type. Value stored on the stack (for most implementations). Object. Data stored on the heap. Reference on the stack.
Arrays, Objects/Dicts Reference type. Data stored on the heap. Reference on the stack. Object. Data stored on the heap. Reference on the stack.
Assignment y = x Copies the value for primitives. Copies the reference for objects. Always copies the reference.

This subtle difference has significant implications. Python's "everything is an object on the heap" model provides a very consistent and simple mental model, but it can come at a performance cost. Creating and managing heap objects for every single number in a calculation adds overhead compared to working with primitives directly on the stack, as JavaScript can. This contributes to the performance characteristics we discussed in the last lesson, where Python can be slower for purely computational, number-crunching tasks.


Conclusion

In this lesson, we've dissected how programs organize memory into the stack and the heap. You can now see how this low-level organization directly influences a language's behavior and performance profile.

  • Key Takeaway 1: The stack is for fast, LIFO-based storage of function calls and local variables. The heap is for dynamic, longer-lived data.
  • Key Takeaway 2: TypeScript/JavaScript distinguishes between primitive types (value on stack) and reference types (data on heap, reference on stack).
  • Key Takeaway 3: Python's model is simpler: everything is an object on the heap. Variables on the stack are merely references to these heap objects.
  • Key Takeaway 4: This difference helps explain performance variations. Accessing stack variables is faster than dereferencing a pointer to the heap, giving JavaScript a potential edge in tight loops that only use primitive types.

You are now building a much deeper, more transferable mental model of how programming languages work. You're moving beyond syntax and seeing the architectural choices that define a language's character.

In our next lesson, we will move up a level of abstraction again. We'll evaluate a language's ecosystem based on its package manager, build tooling, and standard library. A language doesn't exist in a vacuum, and understanding its surrounding ecosystem is just as important for a full-stack developer as understanding its internal mechanics.