The Execution Model
Expert · 18 min read · ▶ live playground · ✦ checkpoint
You type score = score + 3, press Run, and Python does not execute that text directly. Your source code, the Python text you write, becomes bytecode, compact Python-specific instructions, then CPython, the default program that runs Python code, uses its interpreter loop, the repeated cycle that runs one instruction after another, while objects live and die in memory.
Source becomes bytecode
The execution model is the path your program follows from the text you write to the work Python performs. An implementation is a program that follows Python's language rules; CPython is the implementation behind ordinary python on most machines and is written mostly in C. This course's browser Python is still CPython, so the ideas here are not pretend.
import sys
print("implementation:", sys.implementation.name)implementation: cpython
Your .py file is source code. CPython first turns that source into bytecode for the interpreter. Think of bytecode as backstage cue cards: not the full script you wrote, and not raw machine code for your computer's processor, but a tighter set of cues CPython knows how to perform.
That middle step is why "compiled vs. interpreted" is a little too neat for Python. Python is interpreted in the sense that an interpreter runs your program, but CPython also compiles source to bytecode first.
Read bytecode with dis
The dis module is Python's standard-library tool for disassembling code, which means showing bytecode instructions in a readable form. You do not write bytecode by hand. You inspect it when you want to see what CPython is really being asked to do.
import dis
def award_points(score, streak):
bonus = streak * 2
return score + bonus
for instruction in dis.get_instructions(award_points):
print(instruction.opname, instruction.arg, repr(instruction.argrepr))RESUME 0 ''LOAD_FAST_BORROW 1 'streak'LOAD_SMALL_INT 2 ''BINARY_OP 5 '*'STORE_FAST 2 'bonus'LOAD_FAST_BORROW_LOAD_FAST_BORROW 2 'score, bonus'BINARY_OP 0 '+'RETURN_VALUE None ''
You can read the story even if the instruction names look odd. CPython loads streak, loads 2, multiplies, stores the result as bonus, loads score and bonus, adds them, and returns the value.
The exact instruction names can change between Python versions. That is normal. Bytecode is an internal format, not a promise to your application. The promise is the Python behavior: award_points(10, 4) returns 18.
Try it - inspect the trip
Run this, then change the function body. Try return score - penalty, add one more local name, or change the string in describe_result(). Watch the instruction list change.
The interpreter loop reads the next bytecode instruction, performs that operation, then moves to the next instruction. A simple mental model is a stage manager reading cue cards one by one: load this value, call that function, store this name, return now.
That loop sits under everything you already know. A for loop, a function call, a method lookup, a try block, and a list append all become smaller interpreter instructions.
References keep objects alive
Back in Variables & Memory, you learned that names are not boxes. Names are tags attached to objects. CPython makes that model concrete with a reference count, the number of active references pointing at an object.
sys.getrefcount() lets you peek at that count. It reports one extra temporary reference because calling the function itself briefly points at the object.
import sys
class Player:
pass
player = Player()
team = [player]
print("after team list:", sys.getrefcount(player))
backup = player
print("after backup tag:", sys.getrefcount(player))
team.clear()
print("after clearing team:", sys.getrefcount(player))after team list: 3after backup tag: 4after clearing team: 3
The object is alive because player, team[0], and then backup point at it. Clearing the list removes one reference. When an ordinary object's reference count reaches zero, CPython can reclaim it immediately.
A reference cycle is a group of objects that point back to each other. Here a list points to itself, so the garbage collector tracks it.
import gc
loop = []
loop.append(loop)
print("points to itself:", loop[0] is loop)
print("tracked by gc:", gc.is_tracked(loop))points to itself: Truetracked by gc: True
You rarely manage this by hand. You usually write clear ownership: close files with with, avoid needless shared mutable state, and let CPython handle memory. The model matters when you debug leaks, lingering objects, or surprising shared lists.
Identity is not equality
Object identity is the fact that each live object is one particular object in memory. id(obj) returns an identity value for that object while it lives, and is checks whether two names point at that exact same object.
Equality asks whether two values should count as the same by their contents. Lists with the same items are equal. They are not necessarily the same list.
team_a = ["Mina", "Ravi"]
team_b = team_a
team_c = ["Mina", "Ravi"]
print("same contents:", team_a == team_c)
print("same object:", team_a is team_c)
print("alias object:", team_a is team_b)
team_b.append("Noor")
print("team_a:", team_a)
print("team_c:", team_c)same contents: Truesame object: Falsealias object: Trueteam_a: ['Mina', 'Ravi', 'Noor']team_c: ['Mina', 'Ravi']
This is the precise memory model you have been using all along: names live in namespaces, objects live in memory, and references connect them. A namespace is a mapping from names to objects, like a wall of name tags. A frame is the runtime record for one running code block or function call; it holds things like the local namespace and the current execution state. The heap is the memory area where Python objects live.
Use == for value comparisons. Use is only when identity is the question, most commonly thing is None. Do not build logic around the numeric value from id(). In CPython it is address-like, but Python only promises it is unique for that object while the object is alive.
Checkpoint
Answer all three to mark this lesson complete
You now have a cleaner picture of the machine under your Python code: source, bytecode, interpreter loop, references, collection, and identity. Next, that same under-the-hood view moves into classes, where @property, class creation, and object construction become visible machinery instead of magic.