The Python Execution Model Explained Through Code Tracing
Most Python tutorials teach you what to write. Very few teach you what Python actually does when it runs your code. Understanding how Python actually runs your code makes you a significantly better developer.
This distinction matters more than most developers realize. When you understand the execution model, not just the syntax, you can predict behavior in unfamiliar situations, debug faster, and write code that does what you intend rather than what you accidentally specified.
This article is the first in a series on Python mastery through code tracing. We start with the execution model because everything else builds on it.
What Happens When Python Runs a Script
When you run a Python file, the interpreter does several things before executing a single line of your code.
Compilation: It compiles the source code to bytecode. This is the
.pycfile you sometimes see in__pycache__directories. The bytecode is a lower-level representation of your code that the Python virtual machine can execute efficiently.Namespace Creation: It creates the module's global namespace, which is a dictionary that will hold all the names defined at the top level of your file.
Execution: It begins executing the bytecode from the top of the file downward.
Understanding that names are stored in dictionaries is fundamental. When you write x = 10, Python adds the entry 'x': 10 to the current namespace dictionary. When you read x, Python looks up 'x' in that dictionary. This is not an abstraction; it is literally how name lookup works.
x = 10
y = 20
print(globals())
# Output contains: {'x': 10, 'y': 20, '__name__': '__main__', ...}
Function Calls and Stack Frames
Every function call creates a new stack frame. This frame has its own local namespace dictionary, a reference to the global namespace, and a reference to the calling frame.
def calculate(a, b):
result = a + b
return result
x = calculate(3, 4)
When calculate(3, 4) is called:
A new stack frame is created for
calculate.The local namespace is initialized with
{'a': 3, 'b': 4}.The assignment
result = a + badds'result': 7to the local namespace.The return statement returns the value and the frame is destroyed.
The name
xis bound to the returned value in the global namespace.
When the function returns, its stack frame is destroyed and all local names disappear. This is why local variables do not persist between function calls.
Objects, References, and Mutation
Python names are references to objects, not containers holding values. This distinction is the source of most mutation-related surprises.
a = [1, 2, 3]
b = a
b.append(4)
print(a) # Outputs: [1, 2, 3, 4]
Here is the trace of what actually happens under the hood:
[1, 2, 3]— A list object is created in memory at a specific address (let us say address 1000).a = [1, 2, 3]— The nameais added to the namespace dictionary with the value 1000 (the memory address).b = a— The namebis added with the exact same value 1000. Both names now point to the same object.b.append(4)— The list object at address 1000 is modified in place. Bothaandbstill point to address 1000, so both reflect the change.
print(id(a) == id(b)) # True (it is the exact same object)
This is not a bug. It is a direct consequence of how names work. Understanding it converts a confusing surprise into predictable behavior.
The Practical Implication for Code Tracing
When tracing code that involves assignment and mutation, always ask one question before predicting the output:
Does this operation create a new object or modify an existing one?
Operations that create new objects:
+, slicing,sorted(),list(),dict(), and most string methods.Operations that modify existing objects:
.append(),.extend(),.update(), item assignment, and+=on lists.
Once you can reliably answer that question for any operation, your trace accuracy on mutation-related problems will improve substantially.
Next Steps and Practice
The best way to build this mental model is to practice dry-running code manually. Look at code snippets, trace the references in a table, and verify your logic.
You can practice building this mental model with real problems at PyCodeIt. The platform generates Python tracing problems that specifically target these execution model concepts. Try it out at pycodeit.com.
The next article in this series covers scope resolution and the LEGB rule in depth, including the closure behaviors that catch even experienced developers off guard.
Written by the developer behind PyCodeIt, a free AI-powered Python dry-run practice platform for technical interview preparation.
Top comments (0)