Python Basics: Inspect Bytecode Instructions with dis
Bytecode inspection helps explain what the interpreter compiled, especially when a name is loaded from a function argument or the global namespace. Use the structured dis API to inspect a small function without depending on unstable byte offsets.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need the named runtime, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. Run Python without -O so the assertions remain enabled. Any generated fixture files live inside an automatically cleaned temporary directory.
Keep using the same terminal. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-dis.py:
import dis
RATE = 3
def total(quantity):
return quantity * RATE
instructions = list(dis.get_instructions(total))
locals_loaded = [item.argval for item in instructions if item.opname == "LOAD_FAST"]
globals_loaded = [item.argval for item in instructions if item.opname == "LOAD_GLOBAL"]
assert locals_loaded == ["quantity"]
assert globals_loaded == ["RATE"]
assert total(4) == 12
print("Local loads:", locals_loaded)
print("Global loads:", globals_loaded)
print("Result before change:", total(4))
RATE = 5
assert total(4) == 20
print("Result after global change:", total(4))
print("Function arguments:", total.__code__.co_argcount)
assert total.__code__.co_argcount == 1Run it:
python3 -B demonstrate-dis.pyObserved output:
Local loads: ['quantity']
Global loads: ['RATE']
Result before change: 12
Result after global change: 20
Function arguments: 1Read the result
get_instructions exposes named instruction fields, so the example selects only the name loads relevant to the question. quantity is a local parameter; RATE is looked up as a global when the function executes. Changing that global changes the later result without redefining the function. The assertions connect the observed instruction categories to actual behavior.
Apply it carefully
This example targets CPython 3.12, whose bytecode is an implementation detail. Opcode names and specialization behavior can change between Python releases; do not parse disassembly as a stable interchange format or use instruction counts as a timing benchmark. dis.dis(total) is useful for a complete interactive listing, but its offsets and formatting are deliberately omitted here. Disassembly is also not a security sandbox or a substitute for reviewing source.
The official reference describes the API and its options. The checks above cover the specific fixture, not every possible input or environment.
Clean up
Remove the source and then the empty practice directory:
rm -- demonstrate-dis.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect the remaining files rather than deleting unknown contents.
Acknowledgements
This post was created with assistance from AI.