A program is a list of instructions. When you run it, the computer reads those instructions one at a time, top to bottom, and does exactly what they say — nothing more, nothing less. Understanding this single idea will help you write better code, predict what your program will do before it runs, and find bugs faster when things go wrong.
This article is organised in layers. Each section starts with the basic idea — what every Grade 9 programmer needs to know. Then it goes deeper, and deeper still. You do not have to read all three layers at once. Start with the basic idea. Come back to the deeper layers when you are ready.
| Layer | What it means | When to read it |
|---|---|---|
| Basic idea | What every programmer needs. Read this first, always. | Right now. |
| At a deeper level | The mechanism behind the basic idea. Helps you debug and reason about your code. | Once you have written a few programs and seen a few bugs. |
| At the deepest level | How Python works under the hood. Not required for Grade 9 — but explains things that will otherwise seem mysterious. | When you are curious, or when something doesn't make sense any other way. |
1 What Happens When You Click Run
When you click Run (or type python hello.py in the terminal), Python reads your code from top to bottom and carries out each instruction in order. If it cannot understand a line, it stops and tells you what went wrong. If it can, it runs the whole program and produces a result.
Running a program is actually three separate steps, and they always happen in this order:
- Parse. Python reads your entire file and checks whether it can understand the structure. Missing colons, unclosed brackets, and misspelled keywords are caught here. If Python finds a problem, the program never starts — you see a
SyntaxErrororIndentationErrorbefore a single line executes. - Compile to bytecode. Python translates your code into a simpler internal format. This happens invisibly in a fraction of a second.
- Execute line by line. Python works through the bytecode one instruction at a time, doing calculations, updating memory, and producing output.
Understanding that parsing happens before execution is why you sometimes see an error on a line you never expected to reach — Python found the problem during step 1, before the program even started.
The "compilation" in step 2 produces bytecode — a compact set of low-level instructions stored in .pyc files inside the __pycache__/ folder that appears in your project directory. This is not machine code; it is an intermediate format specific to Python.
That bytecode is then run by the CPython interpreter — a separate program, written in C, that reads each bytecode instruction and carries it out on the hardware. This interpreter layer is why Python is called an "interpreted language." Every instruction passes through it, which takes time. Languages like C or Rust skip the interpreter entirely and compile straight to machine code the processor can run directly — which is why those languages are faster. You will hear "Python is slow" in the programming world; this is what people mean.
The __pycache__ folder is Python saving time: if your source code hasn't changed since the last run, Python reuses the compiled bytecode instead of recompiling. It is not a bug or an error — leave it alone.
2 Variables and Memory
A variable is a labelled storage location in the computer's memory. When you write score = 0, Python sets aside a piece of memory, stores the number 0 in it, and labels it score. Every time you use score afterwards, Python looks up that label and retrieves whatever is stored there.
name = "Alex"
score = 0
score = score + 10
print(f"{name} has {score} points")After line 3 runs, score holds 10, not 0. The old value is replaced. This is the most important thing to understand about assignment: = does not mean "equals" — it means "store this value in this label."
Tracing a program means tracking what every variable holds after every line. Here is the program above, traced step by step:
| Line | What Python does | Memory after this line |
|---|---|---|
name = "Alex" | Creates a memory location labelled name, stores "Alex". | name → "Alex" |
score = 0 | Creates a memory location labelled score, stores 0. | name → "Alex", score → 0 |
score = score + 10 | Reads score (gets 0), adds 10, stores 10 back into score. The 0 is gone. | name → "Alex", score → 10 |
print(...) | Reads name and score, builds "Alex has 10 points", sends to screen. | Unchanged. print does not modify memory. |
Bugs almost always come from a variable holding a value you did not expect. Building the habit of tracing — predicting what each variable holds at each line before running — is the single most useful debugging skill at this stage.
In CPython, a variable is not a box that contains a value directly. It is a name bound to an object. When you write score = 0, Python creates an integer object holding the value 0 somewhere in memory, and then binds the name score to that object. When you write score = score + 10, Python creates a new integer object holding 10, and rebinds score to the new object. The original 0 object may still exist briefly in memory until Python's garbage collector reclaims it.
This distinction — names pointing to objects rather than containing values — explains several things that otherwise seem strange: why two variable names can point to the same list object and changes through one name show up through the other; why is and == test different things; and why integers up to 256 are cached (small integers are reused objects, not freshly created each time). You do not need this for Grade 9, but it will matter when you work with mutable objects like lists and dictionaries.
3 Execution Order
Python runs your code in the order you wrote it — top to bottom, one line at a time. It never skips ahead, never goes backwards, and never runs line 4 before line 3. The order you write your code is the order it runs.
Three things can change that order: if statements (which can skip lines), loops (which can repeat lines), and function calls (which jump to a different part of the file and come back).
| Structure | What it does to execution order | Common bug it causes |
|---|---|---|
if / elif / else | Skips the lines in a block if the condition is false. | A variable that was only assigned inside the if block is used later — but the block was skipped, so the variable does not exist yet. |
for / while loops | Repeats a block of lines, then continues past it. | A loop that runs one iteration too many, causing an IndexError on the last pass. |
| Function calls | Pauses the current position, runs the function's lines, then resumes. | Using a variable that was created inside the function — it does not exist outside it. |
When something goes wrong, the most productive question you can ask is: "What order did Python actually run these lines in?" Trace the execution path on paper. You will almost always find the answer.
Under the hood, Python's execution order is controlled by an instruction pointer — a register in the CPython interpreter that holds the address of the next bytecode instruction to execute. Normally it advances by one instruction at a time. Conditionals and loops are implemented as jump instructions in the bytecode: a JUMP_FORWARD skips ahead past a block; a JUMP_BACKWARD (formerly JUMP_ABSOLUTE) loops back to an earlier instruction. Function calls push a new frame onto the call stack and set the instruction pointer to the first instruction of the called function.
You can inspect Python's bytecode yourself using the dis module: import dis; dis.dis(my_function). Doing this even once makes the relationship between Python syntax and machine-level execution concrete in a way that is hard to unsee.
4 Functions, Scope, and the Call Stack
When your program calls a function, Python runs the function's lines and then comes back to where it left off. Variables created inside a function exist only while that function is running — once it returns, those variables are gone. This is called scope.
def greet(user):
message = f"Hello, {user}!"
return message
result = greet("Alex")
print(result) # prints: Hello, Alex!
print(message) # NameError: name 'message' is not definedmessage only existed inside greet(). Once the function returned, it ceased to exist. If you want a value out of a function, you must return it.
Python uses a structure called the call stack to track where it is when it enters a function. Think of it as a stack of bookmarks:
- When Python hits
greet("Alex"), it places a bookmark at the current line and jumps into the function. - Inside the function, Python creates a brand-new, separate memory space. Variables created here (
user,message) are completely isolated from the main program. - When Python hits
return, it takes the return value, destroys the function's memory space, retrieves the bookmark, and resumes exactly where it left off.
The call stack is why tracebacks look the way they do. When Python crashes, it prints every bookmark currently on the stack — the entire chain of calls that led to the crash — so you can see how it got there. The bottom entry is the innermost function; the top entry is the outermost (usually the main program).
Traceback (most recent call last):
File "mission.py", line 12, in <module>
result = calculate_fuel(speed, distance)
File "mission.py", line 7, in calculate_fuel
return distance / speed
ZeroDivisionError: division by zeroTwo bookmarks were on the stack when this crashed: the main program (line 12) and calculate_fuel (line 7). Read tracebacks bottom-first: the last line tells you what went wrong; the last file/line entry tells you where.
Scope stops functions from accidentally overwriting each other's data. Without it, calling any function could silently corrupt a variable you were using in your main program. When a variable "disappears" inside a function, that is the system working as designed — not a bug.
Each entry on the call stack is called a frame (or stack frame). A frame holds the local namespace for one function call: the names of all local variables and their current bindings. When Python executes CALL bytecode, it pushes a new frame onto the stack. When it executes RETURN_VALUE, it pops the frame, passes the return value to the frame below, and resumes execution there.
You can inspect the live call stack in Python using the inspect module: import inspect; inspect.stack() returns a list of frame records for every currently active function call. Debuggers like pdb use exactly this mechanism — they pause execution inside a frame and let you inspect local variables as they exist at that moment. Understanding frames is the conceptual prerequisite for understanding debuggers, decorators, generators, and coroutines. All of them manipulate the call stack in non-obvious ways.
5 Putting It Together
| Layer | What happens here | What goes wrong here |
|---|---|---|
| Your code | Instructions written in Python syntax. | Logic errors — wrong answer, silent wrong behaviour. |
| Parse and check | Python checks structure before running anything. | SyntaxError, IndentationError — program never starts. |
| Execution (line by line) | Instructions run one at a time, memory updates, functions called and returned. | NameError, TypeError, ZeroDivisionError, etc. — program crashes mid-run. |
| Bytecode / Interpreter | CPython runs compiled bytecode through the interpreter. Cached in __pycache__. | Performance issues, not correctness bugs. Rarely the source of Grade 9 problems. |
A running program is just a list of instructions being carried out one at a time, updating memory as it goes. Python never skips lines, never guesses, and never does what you meant — only what you wrote. When something goes wrong, the question is always: "What order did Python run these lines in, and what was in memory at that moment?" That question, asked consistently, is the foundation of debugging.
- When you run a Python program, what are the three steps that happen in order? Describe each one in your own words.
Trace through this program on paper. Write down the value of every variable after each line runs:
total = 100 tax_rate = 0.10 tax = total * tax_rate total = total + tax print(total)
- Explain in one sentence: why does a variable created inside a function not exist outside it?
A student sees this error and panics. What are the two most important things to read first, and what does each one tell you?
Traceback (most recent call last): File "shop.py", line 9, in <module> total = calculate_total(prices) File "shop.py", line 4, in calculate_total return sum(prices) / len(prices) ZeroDivisionError: division by zero- You notice a
__pycache__folder in your project directory. Should you delete it? Explain why or why not in one sentence.