Bytecode Inspection - Inside the code Object
Reading time: ~35 minutes | Level: Intermediate → Engineering
Before reading further, predict the exact output of this program, line by line:
def add(a, b):
return a + b
print(add.__code__.co_consts) # ?
print(add.__code__.co_varnames) # ?
print(add.__code__.co_code) # ?
Write out every line you expect to see, in order.
Most developers have never typed __code__ into a REPL. The actual output (CPython 3.12):
(None,)
('a', 'b')
b'\x97\x00|\x00|\x01\x17\x00S\x00'
co_consts is (None,) - the only constant in add is the implicit None that every function has as a possible return value. co_varnames is ('a', 'b') - the local variable names in order. co_code is a bytes object - the raw bytecode instructions, encoded as a sequence of bytes.
Every Python function carries a code object. It is not hidden. It is a first-class Python object with a documented interface. Understanding it is what separates "I know Python" from "I understand what Python is doing."
What You Will Learn
- What bytecode is and how it fits in the execution pipeline
- The
codeobject (types.CodeType) and all its attributes - How
.pycfiles are structured on disk - Reading
.pycfiles manually withmarshalandimportlib - The line number table (
co_lnotab/co_linetable): how tracebacks find your source - How decorators and closures look in bytecode
- Practical applications:
pytesttest discovery,lru_cachekey derivation, debugging tools
Prerequisites
- Lesson 01: CPython Architecture (the eval loop, the execution pipeline)
- Python Foundation: functions, closures, decorators
Part 1 - What Bytecode Is
The Intermediate Representation
Bytecode is the intermediate representation CPython uses between Python source and the eval loop. It is not machine code - it is not specific to any CPU architecture. It is a sequence of virtual machine instructions for CPython's stack-based virtual machine.
The pipeline from Lesson 01:
Source (.py) → Tokenizer → Parser → AST → Compiler → Bytecode → Eval Loop → Result
Bytecode is what the compiler outputs and what the eval loop inputs. It is stored inside code objects, and code objects are cached to disk as .pyc files.
Why does this matter? Because bytecode is the truth. Whatever Python source you write, the bytecode is what actually runs. Understanding bytecode lets you:
- Verify that two equivalent-looking Python expressions compile to the same or different instructions
- Understand why local variables are faster than global lookups
- See exactly what a decorator does to a function at compile time
- Debug performance issues at the lowest level without resorting to C profiling
- Write tools that introspect, transform, or analyse Python code at the bytecode level (coverage tools, debuggers, monkey-patchers)
Part 2 - The code Object
Accessing the Code Object
Every Python function, class body, module, and comprehension compiles to a code object:
def greet(name):
"""Say hello."""
message = f"Hello, {name}"
return message
code = greet.__code__
print(type(code)) # <class 'code'>
You can also compile a string directly:
code = compile("x = 1 + 2", "<string>", "exec")
print(type(code)) # <class 'code'>
All co_ Attributes Explained
The code object has approximately 15 attributes. They fall into four categories:
Bytecode Attributes
co_code (bytes): The raw bytecode. Each instruction is 2 bytes in Python 3.6+: one byte for the opcode, one byte for the argument. To read it meaningfully, use the dis module (Lesson 03).
def add(a, b):
return a + b
print(add.__code__.co_code)
# b'\x97\x00|\x00|\x01\x17\x00S\x00'
# Each pair of bytes: opcode + argument
co_consts (tuple): All literal constants in the code. This always includes None for functions (the implicit return value). Also includes string literals, numeric literals, and nested code objects (for nested functions and comprehensions):
def example():
x = 42
msg = "hello"
return x
print(example.__code__.co_consts)
# (None, 42, 'hello')
co_stacksize (int): The maximum depth the value stack needs during execution of this code object. The compiler calculates this statically. CPython allocates this much space on the frame's stack.
def simple():
return 1 + 2 + 3
print(simple.__code__.co_stacksize) # typically 2 or 3
co_flags (int): A bitmask of flags describing properties of the code:
def regular():
pass
def generator():
yield 1
async def coroutine():
pass
# Common flag values:
# CO_OPTIMIZED (0x01): uses LOAD_FAST (has local variables)
# CO_NEWLOCALS (0x02): a new local namespace should be created
# CO_VARARGS (0x04): takes *args
# CO_VARKEYWORDS (0x08): takes **kwargs
# CO_GENERATOR (0x20): is a generator function
# CO_COROUTINE (0x100): is an async def function
print(hex(regular.__code__.co_flags)) # 0x3 (OPTIMIZED | NEWLOCALS)
print(hex(generator.__code__.co_flags)) # 0x23 (OPTIMIZED | NEWLOCALS | GENERATOR)
print(hex(coroutine.__code__.co_flags)) # 0x103 (includes COROUTINE)
Variable Attributes
co_varnames (tuple): Names of all local variables, including arguments. Arguments come first, in order:
def func(a, b, *args, key=None, **kwargs):
local = a + b
return local
print(func.__code__.co_varnames)
# ('a', 'b', 'args', 'key', 'kwargs', 'local')
co_names (tuple): Names of global variables and attributes accessed in the code. These are loaded with LOAD_GLOBAL or LOAD_ATTR:
import sys
def uses_globals():
print(sys.version) # print is a global; sys is a global; version is an attribute
print(uses_globals.__code__.co_names)
# ('print', 'sys', 'version')
co_freevars (tuple): Names of variables that are free variables - defined in an enclosing scope and captured by a closure:
def outer():
x = 10
def inner():
return x # x is a free variable of inner
return inner
inner = outer()
print(inner.__code__.co_freevars) # ('x',)
print(inner.__code__.co_varnames) # () - x is not a local of inner
co_cellvars (tuple): Names of local variables that are captured by an inner function. These are stored in cell objects so the inner function can access them even after the outer frame is gone:
def outer():
x = 10 # x is a cell variable - inner captures it
def inner():
return x
return inner
print(outer.__code__.co_cellvars) # ('x',)
co_argcount (int): Number of positional arguments (not including *args or **kwargs):
def func(a, b, c=10):
pass
print(func.__code__.co_argcount) # 3 (a, b, c - c has a default but counts)
print(func.__code__.co_kwonlyargcount) # 0
print(func.__code__.co_posonlyargcount) # 0
Source Info Attributes
co_filename (str): The path to the source file. Used by tracebacks and debuggers.
co_name (str): The function name. co_qualname (Python 3.3+) is the qualified name including class scope:
class MyClass:
def method(self):
pass
print(MyClass.method.__code__.co_name) # 'method'
print(MyClass.method.__code__.co_qualname) # 'MyClass.method'
co_firstlineno (int): The line number in the source file where this code object begins.
:::tip Use co_filename and co_firstlineno in Debugging Tools
These two attributes are how debuggers, profilers, and test frameworks locate source. When you write a custom decorator or error handler and want to point the user to the right source location, use the wrapped function's code object attributes - not your wrapper's:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
filename = func.__code__.co_filename
lineno = func.__code__.co_firstlineno
print(f"Calling {func.__name__} at {filename}:{lineno}")
return func(*args, **kwargs)
return wrapper
:::
Part 3 - The Line Number Table
Mapping Bytecode to Source Lines
Tracebacks show you the source line where an error occurred. CPython achieves this by storing a compact mapping from bytecode offsets to line numbers inside the code object.
In Python 3.10 and earlier, this was co_lnotab - a compressed table of (bytecode delta, line delta) pairs. In Python 3.10+, it was replaced by co_linetable, a more expressive format that also encodes column offsets.
You do not need to decode this manually. The dis module provides structured access:
import dis
def example():
x = 1
y = 2
return x + y
# Get line number for each instruction:
for instruction in dis.get_instructions(example):
print(f"offset {instruction.offset:3d}: line {instruction.starts_line} - {instruction.opname}")
# Output (Python 3.12):
# offset 0: line 1 - RESUME
# offset 2: line 2 - LOAD_CONST
# offset 4: line 2 - STORE_FAST
# offset 6: line 3 - LOAD_CONST
# offset 8: line 3 - STORE_FAST
# offset 10: line 4 - LOAD_FAST
# offset 12: line 4 - LOAD_FAST
# offset 14: line 4 - BINARY_OP
# offset 16: line 4 - RETURN_VALUE
This table is what lets CPython say "the error is on line 42" in a traceback - it maps the current instruction pointer offset back to the original source line.
:::note Bytecode Format Changed Significantly in Python 3.11 and 3.12
Python 3.11 introduced the "specialising adaptive interpreter" - opcodes can be replaced at runtime with specialised versions (LOAD_FAST_CHECK, BINARY_OP_ADD_INT, etc.) after profiling hot paths. Python 3.12 expanded this further. The co_linetable format also changed in 3.10. Always check the Python version when interpreting raw bytecode output.
:::
Part 4 - How .pyc Files Work
Structure of a .pyc File
A .pyc file is a binary file with a small header followed by a marshaled code object:
[ 4 bytes: magic number ]
[ 4 bytes: bit flags ]
[ 4 bytes: timestamp ] (if flags bit 0 is 0)
[ 4 bytes: source size ] (if flags bit 0 is 0)
[ N bytes: marshal.dumps(code_object) ]
The magic number is a 4-byte value that encodes the Python version. CPython rejects .pyc files whose magic number does not match the running interpreter:
import importlib.util
print(importlib.util.MAGIC_NUMBER.hex()) # e.g. 'e50d0d0a' on 3.12
The timestamp (or source hash if bit 1 of flags is set) lets CPython determine whether the .pyc is stale. If the source .py file has a newer modification time, CPython recompiles.
Reading a .pyc File Manually
import marshal
import struct
import time
def read_pyc(path):
with open(path, "rb") as f:
magic = f.read(4)
bit_field = struct.unpack("<I", f.read(4))[0]
if bit_field == 0:
timestamp = struct.unpack("<I", f.read(4))[0]
source_size = struct.unpack("<I", f.read(4))[0]
print(f"Magic: {magic.hex()}")
print(f"Timestamp: {time.ctime(timestamp)}")
print(f"Source size: {source_size} bytes")
else:
f.read(8) # skip hash fields
print(f"Magic: {magic.hex()} (hash-validated)")
code = marshal.loads(f.read())
return code
# Generate a .pyc first: python -c "import mymodule"
code = read_pyc("__pycache__/mymodule.cpython-312.pyc")
print(f"Filename: {code.co_filename}")
print(f"Constants: {code.co_consts}")
:::danger .pyc Files Do NOT Provide Security
Bytecode is trivially decompiled back to readable Python source. Tools like uncompyle6, decompile3, and pycdc can reconstruct Python source from .pyc files with high fidelity.
pip install uncompyle6
uncompyle6 mymodule.cpython-312.pyc
If your business logic must stay confidential, bytecode is not the answer. Consider compiled C extensions (Cython, cffi) or server-side execution where the code never leaves your infrastructure.
:::
Part 5 - Closures and Decorators in Bytecode
How Closures Look: LOAD_DEREF and STORE_DEREF
When a variable is captured by an inner function, CPython handles it differently from a regular local. It creates a cell object - a wrapper that both the outer and inner function share:
import dis
def outer():
x = 10
def inner():
return x
return inner
dis.dis(outer)
Key opcodes you will see:
MAKE_CELL(3.12+): creates the cell object forxat the start ofouterSTORE_DEREFinouter: stores10into the cell (not intofastlocals)LOAD_DEREFininner: loads the value from the shared cell
dis.dis(outer()) # disassemble inner
# LOAD_DEREF x - reads from the shared cell object
# RETURN_VALUE
The cell object is what makes closures work: x is not copied into inner - it is stored in a shared cell that both outer's frame and inner's closure reference. This is why closures capture by reference, not by value:
def make_adder(n):
def adder(x):
return x + n # n is a cell variable
return adder
add5 = make_adder(5)
print(add5.__closure__) # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents) # 5
How Decorators Look in Bytecode
Decorators are applied at function definition time, not at call time. The function body is compiled first, then the decorator is called on the resulting function object:
def my_decorator(func):
return func
@my_decorator
def decorated():
return 42
# Equivalent to:
# def decorated(): return 42
# decorated = my_decorator(decorated)
Disassembling the module-level code shows:
LOAD_GLOBAL my_decorator- load the decoratorMAKE_FUNCTION- create the function object fordecoratedCALL_FUNCTION(orCALLin 3.11+) - callmy_decorator(decorated)STORE_NAME decorated- store the result
The function body is a separate code object stored in co_consts of the module. The decorator is just a function call that happens to take the compiled function object.
:::warning Modifying __code__ Directly Is Fragile
It is technically possible to replace a function's __code__:
def a():
return 1
def b():
return 2
a.__code__ = b.__code__ # a() now returns 2
This is occasionally used in debugging tools and hot-patching scenarios. It is fragile: the replacement code object must have compatible variable counts and types, or you get ValueError. Prefer ast transforms (for source-level modification) or functools.wraps with delegation for behavioural wrappers.
:::
Part 6 - Practical Applications
How pytest Uses Code Objects
pytest uses code objects extensively for test discovery and introspection:
import inspect
def test_addition():
assert 1 + 1 == 2
# pytest does something like this internally:
code = test_addition.__code__
print(code.co_filename) # tells pytest where this test lives
print(code.co_firstlineno) # tells pytest what line to report failures on
print(code.co_name) # 'test_addition' - matches the 'test_*' pattern
# pytest also uses inspect.getsource to show the failing assertion context:
print(inspect.getsource(test_addition))
pytest's assertion rewriting - the feature that shows values of sub-expressions in assertion failures - works by transforming the AST of test files before they are compiled. Understanding that pytest operates at the AST and code object level explains why assertion rewriting only works in files pytest imports, not arbitrary runtime assertions.
How functools.lru_cache Derives Cache Keys
lru_cache caches function calls by their arguments. The cache key is a tuple of the arguments:
import functools
@functools.lru_cache(maxsize=128)
def expensive(n):
return n ** 2
print(expensive.cache_info()) # CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
expensive(10)
expensive(10) # cache hit
print(expensive.cache_info()) # CacheInfo(hits=1, misses=1, ...)
Understanding that lru_cache keys on argument tuples explains why it cannot cache calls with unhashable arguments (like lists) and why mutable default arguments break it.
Building a Simple Code Inspector
import types
def inspect_function(func):
"""Print a readable summary of a function's code object."""
code = func.__code__
print(f"Function: {code.co_qualname}")
print(f"File: {code.co_filename}:{code.co_firstlineno}")
print(f"Arguments: {code.co_varnames[:code.co_argcount]}")
print(f"Locals: {code.co_varnames[code.co_argcount:]}")
print(f"Globals used: {code.co_names}")
print(f"Free vars: {code.co_freevars}")
print(f"Cell vars: {code.co_cellvars}")
print(f"Constants: {code.co_consts}")
print(f"Stack depth: {code.co_stacksize}")
is_generator = bool(code.co_flags & 0x20)
is_coroutine = bool(code.co_flags & 0x100)
print(f"Generator: {is_generator}")
print(f"Coroutine: {is_coroutine}")
nested = [c for c in code.co_consts if isinstance(c, types.CodeType)]
if nested:
print(f"Nested code: {[c.co_name for c in nested]}")
print()
def process_users(users):
active = [u for u in users if u["active"]]
return {u["id"]: u["name"] for u in active}
inspect_function(process_users)
# Function: process_users
# File: /path/to/script.py:1
# Arguments: ('users',)
# Locals: ('active',)
# Globals used: ()
# Free vars: ()
# Cell vars: ()
# Constants: (None,)
# Stack depth: 6
# Generator: False
# Coroutine: False
# Nested code: ['<listcomp>', '<dictcomp>']
The nested code objects for <listcomp> and <dictcomp> confirm that comprehensions compile to separate code objects - their loop variables do not leak into the enclosing function.
Key Takeaways
- Bytecode is the intermediate representation CPython uses between source and the eval loop. It lives inside
codeobjects (types.CodeType) and is cached to disk as.pycfiles - Every Python function, class body, module, lambda, and comprehension compiles to its own
codeobject co_code: the raw bytecode bytes - each instruction is 2 bytes (opcode + argument) in Python 3.6+co_consts: literal constants includingNone; always containsNonefor functions; may contain nested code objectsco_varnames: local variable names, arguments first; indexed byLOAD_FAST/STORE_FASTco_names: global and attribute names; indexed byLOAD_GLOBAL/LOAD_ATTRco_freevars: free variable names captured from enclosing scopes (the closure)co_cellvars: local variable names shared with inner functions via cell objectsco_filenameandco_firstlineno: used by tracebacks, debuggers, and test frameworks to locate source.pycfiles have a header (magic number + timestamp/hash) followed bymarshal.dumps(code_object). CPython skips recompilation if the.pycis not stale- Closures use cell objects shared between outer and inner frames - loaded with
LOAD_DEREF, stored withSTORE_DEREF - Decorators are applied at definition time as function calls; the function body is compiled first, then the decorator wraps it
.pycfiles offer zero security - bytecode is trivially decompiled back to Python source
Graded Practice Challenges
Level 1 - Predict the Output
Question 1: What does this print?
def multiply(x, y=2):
result = x * y
return result
code = multiply.__code__
print(code.co_argcount)
print(code.co_varnames)
print(code.co_consts)
Show Answer
Output:
2
('x', 'y', 'result')
(None,)
co_argcount is 2 - both x and y count as positional arguments (defaults do not change the count). co_varnames lists all local variable names: arguments first (x, y), then locals (result). co_consts is (None,) - the constant 2 (the default value) is stored in the function's __defaults__ tuple, not in co_consts. The only constant in the bytecode is None.
Question 2: What does this print?
def outer():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
f = outer()
print(f.__code__.co_freevars)
print(f.__code__.co_varnames)
print(outer.__code__.co_cellvars)
Show Answer
Output:
('count',)
()
('count',)
inner's co_freevars is ('count',) because count is defined in outer's scope and captured by inner. inner's co_varnames is () because count is not a local of inner - it is a cell variable accessed via LOAD_DEREF. outer's co_cellvars is ('count',) because count is a local of outer that is shared with an inner function.
Question 3: What does this print?
import types
def make_list():
return [x * 2 for x in range(5)]
code = make_list.__code__
nested = [c for c in code.co_consts if isinstance(c, types.CodeType)]
print(len(nested))
print(nested[0].co_name)
Show Answer
Output:
1
<listcomp>
The list comprehension compiles to a separate nested code object inside make_list's co_consts. There is one such nested code object. Its co_name is '<listcomp>' - CPython's conventional name for comprehension code objects.
Question 4: What does this print?
async def fetch(url):
return url
print(hex(fetch.__code__.co_flags & 0x100))
print(hex(fetch.__code__.co_flags & 0x20))
Show Answer
Output:
0x100
0x0
async def functions have the CO_COROUTINE flag (bit 8, value 0x100) set. They do not have the CO_GENERATOR flag (bit 5, value 0x20) - those are for functions with yield. 0x100 & 0x100 = 0x100. 0x100 & 0x20 = 0x0.
Question 5: What does this print?
def has_star_args(*args, **kwargs):
pass
code = has_star_args.__code__
print(code.co_argcount)
print(code.co_varnames)
Show Answer
Output:
0
('args', 'kwargs')
co_argcount counts only positional arguments - it does not count *args or **kwargs. So it is 0. co_varnames still lists args and kwargs because they are local names inside the function. The presence of *args is indicated by the CO_VARARGS flag (0x04) in co_flags.
Level 2 - Debug Challenge
A developer writes a caching decorator. It works for simple functions but silently returns wrong results for closures. Find and fix the bug:
import functools
_cache = {}
def simple_cache(func):
@functools.wraps(func)
def wrapper(*args):
key = (func.__code__, args)
if key not in _cache:
_cache[key] = func(*args)
return _cache[key]
return wrapper
def make_adder(n):
@simple_cache
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # expect 8
print(add10(3)) # expect 13 - what do you actually get?
Show Solution
The bug: add5 and add10 share the same __code__ object - they are both created from the same adder function definition. The cache key (adder.__code__, (3,)) is identical for both calls, so add10(3) hits add5(3)'s cache entry and returns 8 instead of 13.
Root cause: func.__code__ identifies the function's compiled bytecode, not its identity. Two closures generated from the same function definition have the same __code__ but different closures (different n values).
Fix: include the closure cell contents in the cache key:
import functools
_cache = {}
def simple_cache(func):
@functools.wraps(func)
def wrapper(*args):
# Include closure contents so closures with different captured values
# don't share cache entries
closure_key = tuple(
cell.cell_contents
for cell in (func.__closure__ or [])
)
key = (func.__code__, closure_key, args)
if key not in _cache:
_cache[key] = func(*args)
return _cache[key]
return wrapper
def make_adder(n):
@simple_cache
def adder(x):
return x + n
return adder
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8 - correct
print(add10(3)) # 13 - correct, different closure_key
An even simpler fix: key on the function object's identity instead of its code:
key = (id(func), args)
id(func) is unique per function object. Since make_adder creates a new adder function object each time, add5 and add10 have different ids even though they share __code__.
Level 3 - Design Challenge
Design a CodeInspector class that:
- Accepts any Python callable
- Reports whether it is a generator, coroutine, or regular function
- Reports all global names it accesses
- Reports all free variables it captures
- Recursively finds all nested code objects (comprehensions, nested functions) and their names and types
- Reports the total number of constants across all nested code objects
# Target usage:
def outer():
x = 10
evens = [i for i in range(x) if i % 2 == 0]
async def fetcher(url):
return url
return evens
inspector = CodeInspector(outer)
inspector.report()
# Function: outer
# Type: regular
# Globals accessed: []
# Free variables: []
# Nested code objects:
# <listcomp> (regular)
# fetcher (coroutine)
# Total constants: N
Show Reference Solution
import types
class CodeInspector:
"""
Inspect a callable's code object and report its properties.
"""
def __init__(self, func):
if not hasattr(func, "__code__"):
raise TypeError(f"{func!r} does not have a __code__ attribute")
self._func = func
self._code = func.__code__
def _function_type(self, code: types.CodeType) -> str:
CO_GENERATOR = 0x20
CO_COROUTINE = 0x100
CO_ASYNC_GENERATOR = 0x200
flags = code.co_flags
if flags & CO_ASYNC_GENERATOR:
return "async generator"
if flags & CO_COROUTINE:
return "coroutine"
if flags & CO_GENERATOR:
return "generator"
return "regular"
def _total_consts(self, code: types.CodeType) -> int:
"""Count constants in this code object and all nested ones."""
count = len(code.co_consts)
for const in code.co_consts:
if isinstance(const, types.CodeType):
count += self._total_consts(const)
return count
def _find_nested(self, code: types.CodeType):
"""Yield (name, type_str) for all nested code objects."""
for const in code.co_consts:
if isinstance(const, types.CodeType):
yield const.co_name, self._function_type(const)
yield from self._find_nested(const)
def report(self):
code = self._code
print(f"Function: {code.co_qualname}")
print(f"File: {code.co_filename}:{code.co_firstlineno}")
print(f"Type: {self._function_type(code)}")
print(f"Arguments ({code.co_argcount}): {list(code.co_varnames[:code.co_argcount])}")
print(f"Globals accessed: {list(code.co_names)}")
print(f"Free variables: {list(code.co_freevars)}")
print(f"Cell variables: {list(code.co_cellvars)}")
nested = list(self._find_nested(code))
if nested:
print("Nested code objects:")
for name, type_str in nested:
print(f" {name} ({type_str})")
else:
print("Nested code objects: none")
print(f"Total constants: {self._total_consts(code)}")
print(f"Max stack depth: {code.co_stacksize}")
def globals_used(self):
return set(self._code.co_names)
def is_generator(self):
return bool(self._code.co_flags & 0x20)
def is_coroutine(self):
return bool(self._code.co_flags & 0x100)
def nested_names(self):
return [name for name, _ in self._find_nested(self._code)]
# Demo:
def outer():
x = 10
evens = [i for i in range(x) if i % 2 == 0]
async def fetcher(url):
return url
return evens
inspector = CodeInspector(outer)
inspector.report()
Key design decisions:
_function_typechecksco_flagsbitmask - the correct way to detect generators, coroutines, and async generators_find_nestedrecurses intoco_consts- every comprehension and nested function is stored there as atypes.CodeTypeinstance_total_constsrecursively counts constants including those in nested code objects, giving a true total- Individual methods (
globals_used(),is_generator()) are usable programmatically in test infrastructure, separate from the human-readablereport()
What's Next
Lesson 03 covers Disassembly with dis - reading CPython's bytecode disassembler output, understanding every opcode, and comparing equivalent Python patterns at the instruction level. You have seen what the code object contains. Now you will make sense of co_code - the raw bytes - by using dis to translate them into human-readable instruction names and see exactly how Python executes your code.
