By the end of this chapter, you should understand:
- What Python bytecode is.
- Why CPython compiles source code into bytecode.
- Why bytecode is not the same as CPU machine code.
- What the Python Virtual Machine does.
- How bytecode instructions produce runtime behavior.
- Why CPython uses a stack-based execution model.
- What a code object contains.
- What a frame is at a high level.
- How function calls create new execution frames.
- Why
disis useful for understanding Python internals. - Why exact bytecode instructions may differ across Python versions.
- How bytecode connects source code to objects, names, functions, and control flow.
This chapter answers the question left open by Chapter 07:
Once CPython has bytecode, how does that bytecode actually run?
In Chapter 07, we followed Python source code through this pipeline:
.py file
-> source text
-> tokens
-> AST
-> code object
-> bytecode
-> Python Virtual Machine
-> behavior
This chapter focuses on the last major part:
code object
-> bytecode
-> Python Virtual Machine
-> runtime behavior
Python bytecode is a lower-level instruction format used by CPython.
It is not written for humans.
It is not written for the CPU directly.
It is written for the Python Virtual Machine.
For example, source code like this:
x = 10
y = 20
print(x + y)is compiled into instructions that conceptually say:
load constant 10
store it as x
load constant 20
store it as y
load print
load x
load y
add x and y
call print
discard the return value
The exact bytecode names depend on the Python version.
But the idea is stable:
CPython translates readable Python source into smaller interpreter instructions, then executes those instructions.
Think of source code as a recipe written for a human:
Make tea:
1. Boil water.
2. Put tea leaves in a cup.
3. Pour water.
4. Wait.
Bytecode is closer to detailed kitchen actions:
LOAD kettle
LOAD water
CALL boil
LOAD cup
LOAD tea_leaves
CALL place
...
The Python Virtual Machine is the worker following those lower-level instructions.
Another mental model:
Python source code
Human-friendly description
Bytecode
CPython-friendly instruction sequence
Python Virtual Machine
The executor of those instructions
The CPU is still involved, but indirectly.
The CPU executes CPython itself.
CPython executes your bytecode.
CPU
executes
CPython machine code
which executes
Python bytecode
which represents
your Python program
This distinction removes a lot of confusion.
CPython could theoretically interpret source text directly.
But that would be inefficient and complicated.
Source text contains details that are useful to humans:
- Whitespace
- Comments
- Formatting
- Literal spelling
- High-level syntax
The interpreter does not want to repeatedly reason about raw text.
It wants a compact set of instructions.
Bytecode exists because CPython needs a representation that is:
- Easier to execute than source code.
- More explicit than source code.
- Independent of most formatting details.
- Connected to runtime operations.
- Portable across machines running compatible Python versions.
- Easier to cache for imported modules.
Source code is designed for people.
Bytecode is designed for the interpreter.
An intermediate representation is a form between the original source language and the final execution machinery.
In CPython:
Python source
|
v
bytecode
|
v
Python Virtual Machine execution
Bytecode is not the final thing the CPU executes.
It is the final thing CPython's interpreter loop executes.
This is different from a C program:
C source
|
v
machine code
|
v
CPU execution
For CPython:
Python source
|
v
Python bytecode
|
v
CPython interpreter
|
v
CPU executes CPython's machine code
That is why Python bytecode is not generally a standalone executable program.
It needs a compatible Python runtime.
Python bytecode is an implementation detail.
It can change between Python versions.
For example, Python 3.10, Python 3.11, Python 3.12, and later versions may show different bytecode for similar source code.
The exact instruction names and optimizations are not the main thing to memorize.
The important mental model is:
source code
-> compiled interpreter instructions
-> executed by the Python Virtual Machine
When this book shows bytecode examples, focus on the meaning:
- Loading values
- Storing names
- Calling functions
- Performing operations
- Returning values
- Jumping for control flow
The exact printed output from dis may differ on your machine.
That is normal.
Python includes a standard library module named dis.
dis means disassembler.
A disassembler shows a lower-level instruction representation.
Example:
import dis
def add(a, b):
return a + b
dis.dis(add)You may see output containing instructions such as:
LOAD_FAST
BINARY_OP
RETURN_VALUE
The output may include additional instructions depending on your Python version.
Conceptually:
LOAD_FASTloads a local variable.BINARY_OPperforms an operation such as addition.RETURN_VALUEreturns a value from the function.
The source code:
return a + bis human-friendly.
The bytecode says more operationally:
load a
load b
add them
return the result
That is the key idea.
Consider:
def add(a, b):
return a + bAt a high level, CPython needs to do this when add(2, 3) runs:
1. Create a function call frame.
2. Bind a to 2.
3. Bind b to 3.
4. Load a.
5. Load b.
6. Add them.
7. Return the result.
8. Destroy or release the frame when the call is done.
Bytecode represents the middle part:
load a
load b
add
return
Notice what bytecode does not look like.
It does not look like the original source:
return a + bIt also does not look like CPU assembly for a specific processor.
It is its own instruction language for the Python runtime.
The Python Virtual Machine is the part of CPython that executes bytecode.
It is not a separate program you usually launch manually.
It is part of the CPython runtime.
At a high level, the PVM does this:
while there are bytecode instructions:
read the next instruction
perform the operation
update runtime state
This is called an interpreter loop.
The PVM repeatedly:
- Fetches an instruction.
- Decodes what it means.
- Executes it.
- Moves to the next instruction.
This is similar in spirit to a CPU instruction cycle:
fetch
decode
execute
But the CPU executes machine instructions.
The Python Virtual Machine executes Python bytecode instructions.
A physical machine has:
- Instructions
- Memory
- Execution state
- Control flow
The Python Virtual Machine is "virtual" because it is implemented in software.
It provides an execution environment for Python bytecode.
It has:
- Bytecode instructions
- A value stack
- Frames
- Namespaces
- Exception handling state
- Instruction pointers
It is not virtual in the same way as a full operating-system virtual machine such as VirtualBox or VMware.
The PVM does not emulate an entire computer.
It is a language virtual machine.
It exists to execute Python programs.
CPython bytecode uses a stack-based execution model.
A stack is a last-in, first-out structure.
You can think of it like a stack of plates:
push plate A
push plate B
pop -> gets plate B first
pop -> gets plate A
In bytecode execution, the PVM uses an evaluation stack.
Instructions push values onto the stack and pop values from the stack.
For example, to compute:
2 + 3the bytecode model is conceptually:
push 2
push 3
pop 3
pop 2
add them
push 5
The result is left on the stack.
Source:
result = 2 + 3Conceptual bytecode:
LOAD_CONST 2
LOAD_CONST 3
BINARY_OP +
STORE_NAME result
Stack behavior:
start:
[]
LOAD_CONST 2:
[2]
LOAD_CONST 3:
[2, 3]
BINARY_OP +:
[5]
STORE_NAME result:
[]
namespace:
result -> 5
The stack is temporary working space.
The name result is stored in a namespace.
This distinction will become important in later chapters:
- Stack: temporary execution values.
- Namespace: mapping from names to objects.
- Object: actual runtime value.
Consider:
print(10 + 20)Conceptually:
load print
load 10
load 20
add them
call print with the result
Stack behavior:
start:
[]
load print:
[print]
load 10:
[print, 10]
load 20:
[print, 10, 20]
add:
[print, 30]
call:
[]
In reality, modern CPython has specific call-related instructions that can vary by version.
The important idea is that function calls are built from bytecode operations that load the callable, load arguments, and perform the call.
Chapter 07 introduced code objects briefly.
Now we can look more closely.
A code object is a compiled representation of a block of Python code.
Blocks of code include:
- A module
- A function body
- A class body
- A comprehension body
- Code passed to
eval()orexec()
For example:
def square(x):
return x * xThe function square has a code object.
You can access it using:
square.__code__That code object contains information such as:
- The bytecode instruction sequence.
- Constants used by the code.
- Names referenced by the code.
- Local variable names.
- Argument information.
- Filename information.
- Function name information.
- Line number information for tracebacks and debugging.
The code object is not executing by itself.
It is data that the Python runtime can execute inside a frame.
This distinction is very important.
Consider:
def greet(name):
return "Hello, " + nameWhen Python executes the def statement, it creates a function object.
The function object contains or references:
- A code object
- A global namespace
- Default argument values, if any
- Closure variables, if any
- Metadata such as the function name
The code object contains the compiled instructions.
The function object is the callable runtime object.
Mental model:
function object
|
v
code object
|
v
bytecode instructions
When you call the function, Python uses the function object to create a new execution frame from the code object.
Example:
def square(x):
return x * x
code = square.__code__
print(code.co_name)
print(code.co_varnames)
print(code.co_consts)You may see output like:
square
('x',)
(None,)
The exact details may vary.
Important ideas:
co_namestores the code object's name.co_varnamesstores local variable names.co_constsstores constants used by the code.
For example:
def answer():
return 42The constant 42 is stored in the code object's constants.
This means constants are not rediscovered from source text every time the function runs.
They are part of the compiled representation.
Consider:
def show():
message = "Hello"
print(message)The code object must know:
- The string constant
"Hello". - The local name
message. - The referenced name
print.
Conceptually:
constants:
None
"Hello"
local variables:
message
names:
print
The bytecode then refers to these tables.
Instead of storing the full string "Hello" inside every instruction, bytecode can say:
load constant at index 1
Instead of storing all name data directly inside every instruction, bytecode can refer to name tables.
This makes execution more structured.
To execute a code object, CPython creates a frame.
A frame represents one active execution context.
When a function is running, it has a frame.
When a module is executing top-level code, it has a frame.
A frame contains runtime state such as:
- The code object being executed.
- The current instruction position.
- The evaluation stack.
- Local variables.
- Global variables reference.
- Builtins reference.
- Exception handling state.
If a code object is the recipe, a frame is the active cooking session.
The same recipe can be used many times.
Each active run needs its own state.
Consider:
def double(x):
return x * 2
result = double(5)At a high level:
module frame starts
|
v
function object double is created
|
v
double(5) is called
|
v
new frame for double is created
|
v
x is bound to 5 inside that frame
|
v
bytecode for double executes
|
v
return value goes back to module frame
The module and the function do not share one flat execution space.
The function call gets its own frame.
This is why local variables inside a function do not automatically become global variables.
When functions call other functions, frames stack up.
Example:
def a():
b()
def b():
c()
def c():
print("inside c")
a()While c() is running, the call stack is conceptually:
c frame
b frame
a frame
module frame
The top frame is currently executing.
When c() returns, its frame is removed.
Then execution resumes in b().
This connects directly to Chapter 03, where we introduced stack memory and function calls.
Python frames are the language-level execution records that make function calls work.
Bytecode often works with names.
For example:
x = 10
print(x)The bytecode must:
- Store
10under the namex. - Later load the value associated with
x. - Load the callable named
print. - Call it.
Names live in namespaces.
A namespace is a mapping from names to objects.
At a high level:
namespace:
x -> 10
print -> built-in print function
Later chapters will deeply explain names and references.
For now, the important connection is:
Bytecode instructions do not merely compute numbers. They also load and store names in namespaces.
Many bytecode instructions are about moving values.
Common conceptual categories:
LOAD -> put a value onto the evaluation stack
STORE -> take a value from the stack and bind/store it somewhere
CALL -> call a callable object
RETURN -> return from the current frame
JUMP -> move execution to another instruction
Example:
x = 10Conceptual operations:
LOAD_CONST 10
STORE_NAME x
Example:
print(x)Conceptual operations:
LOAD_NAME print
LOAD_NAME x
CALL
The exact names differ between module scope, function scope, and Python versions.
But the categories remain useful.
Inside functions, local variables are handled efficiently.
Example:
def add(a, b):
total = a + b
return totalThe names a, b, and total are local variables.
CPython can access many local variables through fast internal storage rather than a normal dictionary lookup.
That is why you may see bytecode names like:
LOAD_FAST
STORE_FAST
Conceptually:
LOAD_FASTloads a local variable.STORE_FASTstores a local variable.
The word "fast" hints that local variables are optimized.
This also explains why Python needs to know which names are local in a function.
Compilation analyzes the function body and determines local variable layout.
Source:
x = 10Conceptual bytecode:
LOAD_CONST 10
STORE_NAME x
Execution:
LOAD_CONST 10
stack becomes [10]
STORE_NAME x
stack becomes []
namespace gets x -> 10
Visible result:
print(x)prints:
10
The bytecode explains that assignment is not putting a value inside a box.
It is binding a name to an object.
Chapter 10 will develop this mental model fully.
Source:
result = 2 + 3 * 4Python must respect operator precedence.
Multiplication happens before addition.
Conceptual operations:
load 2
load 3
load 4
multiply 3 and 4
add 2 and 12
store result
Stack walkthrough:
[]
[2]
[2, 3]
[2, 3, 4]
[2, 12]
[14]
[]
namespace:
result -> 14
The AST already represented the correct structure.
The compiler turns that structure into bytecode that produces the correct result.
Source:
if temperature > 30:
status = "hot"
else:
status = "comfortable"Bytecode must support branching.
Conceptually:
load temperature
load 30
compare >
if false, jump to else branch
load "hot"
store status
jump past else branch
load "comfortable"
store status
This is how high-level control flow becomes instruction-level control flow.
The source code shows a clean if statement.
The bytecode contains conditional jumps.
Source:
count = 0
while count < 3:
print(count)
count = count + 1Conceptually:
store count as 0
loop_start:
load count
load 3
compare <
if false, jump to loop_end
load print
load count
call print
load count
load 1
add
store count
jump to loop_start
loop_end:
Loops are not magic.
They are repeated jumps and tests.
This connects Python control flow back to the execution concepts from Chapter 03.
Source:
def greet(name):
return "Hello, " + nameThis often surprises learners:
Defining a function does not run the function body.
When Python executes the def statement, it creates a function object and binds it to the function name.
Conceptually:
load code object for greet
create function object
store function object under name greet
The function body bytecode exists, but it runs later when the function is called.
This is why:
def greet():
print("hello")
print("done")prints:
done
The body of greet does not run.
The function object is created.
Source:
def greet(name):
print("Hello", name)
greet("Ada")At runtime:
1. The module frame creates function object greet.
2. The name greet is bound to that function object.
3. The call greet("Ada") loads the function object.
4. The argument "Ada" is loaded.
5. CPython creates a new frame for the call.
6. Inside that frame, name is bound to "Ada".
7. The bytecode inside greet executes.
8. The frame returns.
9. Execution resumes in the module frame.
Function calls are frame transitions.
This idea will become central when we study scope, call stacks, recursion, closures, and exceptions.
A function returns by sending a value from its frame back to the caller.
Example:
def add(a, b):
return a + b
result = add(2, 3)Conceptually:
module frame:
call add(2, 3)
add frame:
load a
load b
add
return 5
module frame:
receive 5
store result -> 5
The return value crosses from the callee frame back to the caller frame.
If a function does not explicitly return a value, Python returns None.
That is why:
def say_hi():
print("hi")
value = say_hi()
print(value)prints:
hi
None
The call to say_hi() produces the return value None.
Exceptions are also part of runtime execution.
Consider:
print("before")
1 / 0
print("after")The source is valid.
CPython can compile it.
The error happens when bytecode attempts division.
At that point, Python raises an exception.
Execution does not continue normally to the next instruction.
Instead, Python looks for exception-handling logic.
If no handler is found, the program terminates and prints a traceback.
Tracebacks are possible because frames contain information about:
- The current code object.
- The current line number.
- The active call stack.
This is why Python can tell you where an error happened.
Consider:
def divide(a, b):
return a / b
def run():
return divide(10, 0)
run()The traceback shows a chain of calls:
module code called run
run called divide
divide attempted division by zero
This mirrors the frame stack:
divide frame
run frame
module frame
Tracebacks are not random error messages.
They are reports of active or recently active frames at the moment an exception occurred.
At the heart of bytecode execution is an evaluation loop.
Conceptually:
while frame is active:
instruction = next bytecode instruction
execute instruction
update stack, names, frame, or instruction pointer
Different instructions do different things:
LOAD_CONST
push a constant onto the stack
STORE_NAME
pop a value and bind it to a name
BINARY_OP
pop operands, perform operation, push result
CALL
call a callable object
RETURN_VALUE
return from the current frame
JUMP
move to another instruction
The real CPython evaluation loop is implemented in C and is much more complex.
It handles:
- Function calls
- Exceptions
- Generators
- Async execution
- Tracing
- Debugging hooks
- Adaptive optimizations
- Reference counting
- The Global Interpreter Lock
But the beginner mental model remains:
The PVM repeatedly reads bytecode instructions and updates runtime state.
Modern CPython versions include adaptive interpreter optimizations.
This means CPython may specialize some bytecode execution paths at runtime based on observed behavior.
For example, if Python sees that a particular addition operation repeatedly adds integers, it may optimize that operation internally.
You do not need to master these details now.
The important point is:
- The language meaning stays the same.
- The implementation may optimize execution.
- Bytecode details can vary by Python version.
This is another reason not to memorize every instruction name too early.
Learn the model first.
Bytecode operates on objects.
When bytecode loads the constant 10, it loads a Python object representing the integer value 10.
When bytecode loads "hello", it loads a string object.
When bytecode calls print, it loads a function-like object and calls it.
This prepares us for Chapter 09:
Runtime execution is object manipulation.
Source code:
x = 10Conceptually:
load integer object 10
bind name x to that object
Python does not treat values as raw unstructured data.
Python values are objects.
Bytecode is the instruction layer that creates, loads, stores, and manipulates those objects.
Bytecode also explains why names matter.
Source:
x = 10
y = xConceptually:
load 10
store name x
load name x
store name y
This does not mean x is a box containing 10.
It means:
- There is an object representing
10. - The name
xrefers to that object. - The name
ycan be bound to the same object.
Chapter 10 will explain names and references in full.
But bytecode already hints at the truth:
Python execution loads objects and binds names to objects.
Consider:
items = []
items.append("a")Conceptually:
create/load empty list object
store name items
load items
load append method
load "a"
call append
The second line does not reassign items.
It calls a method on the object that items refers to.
That method mutates the list.
This will matter later when we study mutability.
Bytecode helps us separate:
- Binding names
- Loading objects
- Calling methods
- Mutating objects
Those are different operations.
High-level Python control flow becomes bytecode jumps.
Examples:
if condition:
do_this()
else:
do_that()becomes conceptually:
evaluate condition
if false, jump to else
call do_this
jump to after if
call do_that
after if
Loops are also jumps:
while condition:
body()becomes conceptually:
loop start
evaluate condition
if false, jump after loop
call body
jump to loop start
after loop
This connects Python to general computer science:
Structured control flow is implemented using conditional and unconditional jumps.
Python syntax makes control flow readable.
Bytecode makes it executable by the interpreter.
Chapter 07 introduced __pycache__.
Now we can understand it more clearly.
When Python imports a module, CPython may save the compiled bytecode in a .pyc file.
Example:
project/
├── main.py
├── tools.py
└── __pycache__/
└── tools.cpython-312.pyc
The .pyc file stores compiled bytecode and metadata.
The purpose is to avoid recompiling unchanged imported modules every time.
Important:
.pycfiles are an optimization.- They are version-specific.
- They are not meant to be edited by humans.
- They do not replace the need for the Python runtime.
- They can usually be deleted and regenerated.
Bytecode caching improves startup/import behavior, especially in projects with many modules.
Learners sometimes expect every executed Python file to create __pycache__.
The behavior depends on how code is run and what is imported.
Most commonly, cached bytecode is created for imported modules.
If you run:
python main.pyPython may not create a .pyc for main.py in the same way it does for imported modules.
But if main.py imports helper.py, you may see cached bytecode for helper.py.
The practical rule is:
__pycache__is normal. Do not panic when you see it. Do not depend on editing it.
You can write Python for a long time without reading bytecode.
So why learn it?
Because bytecode explains the machinery behind many behaviors:
- Why syntax errors happen before runtime.
- Why function bodies do not run at definition time.
- Why local variables are different from globals.
- Why loops and conditionals are jumps.
- Why function calls create frames.
- Why tracebacks show call chains.
- Why imports execute top-level module code.
- Why
__pycache__exists. - Why Python is not simply "line by line text execution."
You do not need bytecode for every daily programming task.
But understanding it gives you a deeper mental model of Python.
That is the goal of this book.
When you first see dis output, it may look intimidating.
Example output might include columns such as:
line number
instruction offset
instruction name
instruction argument
argument meaning
You do not need to memorize the table.
Ask these questions instead:
- What values are being loaded?
- What names are being stored?
- What operation is being performed?
- Is a function being called?
- Is execution jumping somewhere?
- Where does the function return?
This turns bytecode from noise into a readable execution story.
Source:
def example(x):
y = x + 1
return yConceptual bytecode story:
load local x
load constant 1
add them
store local y
load local y
return it
Visible behavior:
print(example(4))Output:
5
The source tells you what the function means.
The bytecode tells you the execution steps CPython follows.
Consider:
def broken():
return 1 / 0
print("function created")Output:
function created
No error occurs yet.
Why?
The function body is compiled and stored in a code object.
But it is not executed until the function is called.
Now:
def broken():
return 1 / 0
print("function created")
broken()Output:
function created
then a ZeroDivisionError.
The runtime error occurs when the function's bytecode executes inside a call frame.
Now compare:
def broken():
if True
return 1
print("function created")This does not print:
function created
Why?
The syntax error prevents compilation.
The function object is never created.
The module does not begin normal execution.
This reinforces the distinction:
syntax error:
detected before bytecode execution
runtime error:
detected during bytecode execution
Bytecode is not CPU machine code.
It is an instruction format for the Python Virtual Machine.
The CPU executes CPython.
CPython executes Python bytecode.
The goal is not memorization.
The goal is understanding execution.
Instruction names can change across Python versions.
The mental model is more important:
load values
operate on values
store names
call functions
jump for control flow
return values
The PVM is part of the Python runtime implementation.
When using CPython, it is part of CPython.
You usually do not launch it separately.
Executing def creates a function object.
The function body runs when the function is called.
The function's code object already exists, but execution waits until call time.
CPython can optimize local variable access.
That is why local variable bytecode may use fast local storage.
Global names require different lookup behavior.
This chapter focuses on CPython.
Other Python implementations may use different execution strategies.
For example, PyPy uses a JIT compiler.
The Python language is shared, but implementation internals can differ.
Bytecode helps explain why errors occur where they do.
Syntax errors happen before bytecode execution.
Runtime errors happen while bytecode is executing.
Tracebacks show frame information because Python execution happens inside frames.
When a function is called, Python creates a frame.
That frame has local variables, an evaluation stack, and an instruction position.
This explains:
- Local variables
- Recursion
- Tracebacks
- Return values
- Call stack behavior
Imports compile and execute module code.
Cached bytecode may be stored in __pycache__.
This explains:
- Import-time side effects
- Startup time
.pycfiles- Why module top-level code should be written carefully
Bytecode helps explain why Python has overhead compared with native compiled languages.
For many operations, CPython must:
- Interpret bytecode.
- Work with Python objects.
- Perform dynamic type checks.
- Manage references.
- Handle possible exceptions.
This flexibility is powerful, but it has costs.
Later chapters will return to performance with better foundations.
Many tools connect to Python's execution model.
Debuggers inspect frames.
Profilers observe function calls and execution time.
Coverage tools map executed bytecode back to source lines.
Linters and type checkers often work before bytecode execution.
The dis module exposes compiled instruction structure.
This chapter connects directly to earlier chapters:
Chapter 01:
Software is instructions and data.
Chapter 02:
CPUs execute machine instructions.
Chapter 03:
Execution uses processes, stacks, and function calls.
Chapter 04:
The OS starts the Python process and provides file access.
Chapter 05:
Python source is designed for readability.
Chapter 06:
CPython is the implementation this chapter studies.
Chapter 07:
CPython compiles source code into bytecode.
Chapter 08:
The Python Virtual Machine executes bytecode.
This chapter also prepares the next phase:
Bytecode loads and stores objects.
Objects have type, value, and identity.
Names refer to objects.
Mutation changes objects.
Equality compares objects.
That is exactly where the book goes next.
The full runtime picture is:
source code
|
v
AST
|
v
code object
|
v
bytecode instructions
|
v
execution frame
|
v
evaluation stack + namespaces
|
v
Python Virtual Machine loop
|
v
runtime behavior
Important terms:
| Term | Meaning |
|---|---|
| Bytecode | CPython instruction format for the PVM |
| PVM | Runtime machinery that executes bytecode |
| Code object | Compiled code plus metadata |
| Frame | Active execution context for a code object |
| Evaluation stack | Temporary stack used by bytecode execution |
| Namespace | Mapping from names to objects |
| Instruction pointer | Position of the next instruction to execute |
- What is Python bytecode?
- What executes Python bytecode in CPython?
- Is bytecode the same as CPU machine code?
- What module can show Python bytecode?
- What is a code object?
- What is a frame?
- What is the evaluation stack used for?
- What happens when a function is called?
- What is
__pycache__used for? - Why can bytecode output differ between Python versions?
- Why does CPython use bytecode instead of repeatedly interpreting raw source text?
- Why is bytecode called an intermediate representation?
- Why does a function body not run when the
defstatement is executed? - Why does a function call need a new frame?
- Why is stack-based execution useful for evaluating expressions?
- Why are local variables often accessed differently from global names?
- Why can syntax errors prevent bytecode execution entirely?
- Why do tracebacks reflect the call stack?
- Why is
__pycache__an optimization rather than a source of truth? - Why should beginners learn the bytecode model without memorizing every instruction?
- Explain bytecode to someone who knows only source code.
- Explain the Python Virtual Machine.
- Explain why bytecode is not machine code.
- Explain how
2 + 3can be evaluated using a stack. - Explain what happens when a function is called.
- Explain how a traceback relates to frames.
- Explain why importing a module may create cached bytecode.
def greet():
print("hello")
print("done")What is printed?
Answer:
done
Reason:
The def statement creates a function object. The body does not run until the function is called.
def greet():
print("hello")
greet()
print("done")What is printed?
Answer:
hello
done
Reason:
Calling greet() creates a frame and executes the function body's bytecode.
def broken():
return 1 / 0
print("created")What is printed?
Answer:
created
Reason:
The function body is compiled but not executed.
def broken():
return 1 / 0
print("created")
broken()
print("done")What happens?
Answer:
created
Then Python raises ZeroDivisionError.
Reason:
The error occurs when the function's bytecode executes inside a call frame.
x = 10
y = x
print(y)What is printed?
Answer:
10
Reason:
The bytecode stores the object represented by 10 under x, then loads x and stores the same value under y.
- Draw the relationship between source code, bytecode, and the PVM.
- Draw how
2 + 3is evaluated using a stack. - Draw a function object pointing to a code object.
- Draw the frame stack while function
a()callsb()andb()callsc(). - Draw where local variables live during a function call.
- Draw how a return value moves from a callee frame back to a caller frame.
Disassemble a simple function:
import dis
def add(a, b):
return a + b
dis.dis(add)Identify instructions that appear to:
- Load local variables.
- Perform an operation.
- Return a value.
Do not memorize the instruction names.
Write the execution story in plain English.
Inspect a code object:
def answer():
return 42
code = answer.__code__
print(code.co_name)
print(code.co_consts)
print(code.co_varnames)Explain what each printed value represents.
Trace stack execution for:
result = 2 + 3 * 4Draw the conceptual stack after each operation:
load 2
load 3
load 4
multiply
add
store result
Explain why the result is 14, not 20.
Compare definition time and call time:
def show():
print("inside")
print("outside")Then:
def show():
print("inside")
show()
print("outside")Explain the difference using function objects, code objects, and frames.
Create a traceback:
def divide(a, b):
return a / b
def run():
return divide(10, 0)
run()Read the traceback and identify:
- The module frame.
- The
runframe. - The
divideframe. - The line where runtime execution failed.
Create two files:
# helper.py
def double(x):
return x * 2# main.py
import helper
print(helper.double(5))Run:
python main.pyLook for __pycache__.
Explain why cached bytecode may appear for helper.py.
In this chapter we learned:
- CPython compiles Python source code into bytecode.
- Bytecode is an instruction format for the Python Virtual Machine.
- Bytecode is not CPU machine code.
- The CPU executes CPython; CPython executes Python bytecode.
- The
dismodule can show bytecode. - Exact bytecode output can vary across Python versions.
- Code objects contain bytecode and metadata.
- Function objects reference code objects.
- Executing code requires a frame.
- Function calls create new frames.
- Frames contain runtime state such as local variables, an evaluation stack, and an instruction position.
- CPython bytecode uses stack-based execution.
- Assignments, arithmetic, conditionals, loops, function definitions, and function calls all become bytecode operations.
- Tracebacks are connected to frames and the call stack.
__pycache__stores cached bytecode for imported modules.
The core mental model is:
source code
-> code object
-> bytecode
-> frame
-> evaluation stack and namespaces
-> Python Virtual Machine
-> runtime behavior
You now understand how CPython moves from compiled instructions to actual execution.
Bytecode does not operate on vague values.
It operates on Python objects.
When bytecode loads 10, it loads an integer object.
When bytecode loads "hello", it loads a string object.
When bytecode calls a function, it calls a function object.
In the next chapter, we begin the most important Python mental model:
Everything is an object.