Skip to main content
How Computers Actually Run Your Code

Absolute Foundations

How Computers Actually Run Your Code

Reading12 min read

How Computers Actually Run Your Code

You write print("hello"), hit run, and text appears. Most tutorials stop there. But "how" matters the first time your code does something you didn't expect — a variable that's somehow the wrong value, a program that's mysteriously slow, a crash with a stack trace you can't parse. All three make more sense once you know what actually happens between your source file and the blinking cursor.

Source Code Is Just Text, Until It Isn't

Your .py, .js, or .java file is plain text. The CPU doesn't understand Python or JavaScript — it understands a small set of binary instructions (machine code) specific to its architecture. Something has to bridge that gap, and there are two main strategies: compiling and interpreting.

A compiler (like javac for Java, or gcc for C) reads your entire source file up front and translates it into another form — machine code, or an intermediate bytecode — before your program ever runs. Errors like a missing semicolon or a type mismatch get caught at this stage, before execution starts.

An interpreter (the default for Python, and for JavaScript running in a browser) reads and executes your code more or less line by line, translating as it goes. This is why a Python script can fail halfway through with a NameError on line 40 after successfully printing output from lines 1-39 — the interpreter never looked ahead.

Most modern languages blur this line. Java compiles to bytecode, then a virtual machine (the JVM) interprets or just-in-time (JIT) compiles that bytecode to native machine code while running. JavaScript engines like V8 do something similar. "Compiled vs interpreted" is a spectrum, not a binary choice, and knowing where your language sits explains a lot of its behavior — including its error messages.

The Stack and the Heap, Conceptually

While your program runs, it needs somewhere to keep track of things: which function called which, and what data is currently in use. Two regions of memory matter most:

  • The stack holds function call frames — local variables, and the address to return to when a function finishes. It's fast, and it's automatically cleaned up: the moment a function returns, its frame is popped off. This is also why deep, unbounded recursion crashes with a "stack overflow" — you've run out of stack space.
  • The heap holds data whose lifetime isn't tied to a single function call — objects, arrays, anything that needs to outlive the function that created it, or whose size isn't known until runtime. It's slower to allocate from and needs cleanup (garbage collection in most modern languages, manual free() in C).
def make_greeting(name):      # `name` and the return address live on the stack
    message = f"Hello, {name}"  # `message` (the string object) lives on the heap;
    return message               # the *reference* to it lives on the stack

greeting = make_greeting("Ada")  # the stack frame for make_greeting is gone now,
                                  # but the string on the heap is still reachable
                                  # via `greeting`, so it survives

Why This Matters Even If You Never Touch Assembly

You don't need to write machine code to benefit from understanding this. It explains why passing a huge object to a function is cheap (you're copying a small heap reference, not the whole object), why some languages have predictable performance and others have occasional GC pauses, and why "it works on my machine" sometimes really does mean a different runtime is compiling or interpreting your code differently than you expect.

💬 Discussion

Have you ever hit a bug or performance issue that made more sense once you understood stack vs. heap, or compiled vs. interpreted execution? What was it?

Q
Knowledge Check
1 / 3

What is the key practical difference between a compiler and an interpreter?

Next Lesson

Variables, Types, and the Memory Behind Them