Skip to main content

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 code object (types.CodeType) and all its attributes
  • How .pyc files are structured on disk
  • Reading .pyc files manually with marshal and importlib
  • The line number table (co_lnotab / co_linetable): how tracebacks find your source
  • How decorators and closures look in bytecode
  • Practical applications: pytest test discovery, lru_cache key 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 for x at the start of outer
  • STORE_DEREF in outer: stores 10 into the cell (not into fastlocals)
  • LOAD_DEREF in inner: 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 decorator
  • MAKE_FUNCTION - create the function object for decorated
  • CALL_FUNCTION (or CALL in 3.11+) - call my_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 code objects (types.CodeType) and is cached to disk as .pyc files
  • Every Python function, class body, module, lambda, and comprehension compiles to its own code object
  • co_code: the raw bytecode bytes - each instruction is 2 bytes (opcode + argument) in Python 3.6+
  • co_consts: literal constants including None; always contains None for functions; may contain nested code objects
  • co_varnames: local variable names, arguments first; indexed by LOAD_FAST/STORE_FAST
  • co_names: global and attribute names; indexed by LOAD_GLOBAL/LOAD_ATTR
  • co_freevars: free variable names captured from enclosing scopes (the closure)
  • co_cellvars: local variable names shared with inner functions via cell objects
  • co_filename and co_firstlineno: used by tracebacks, debuggers, and test frameworks to locate source
  • .pyc files have a header (magic number + timestamp/hash) followed by marshal.dumps(code_object). CPython skips recompilation if the .pyc is not stale
  • Closures use cell objects shared between outer and inner frames - loaded with LOAD_DEREF, stored with STORE_DEREF
  • Decorators are applied at definition time as function calls; the function body is compiled first, then the decorator wraps it
  • .pyc files 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:

  1. Accepts any Python callable
  2. Reports whether it is a generator, coroutine, or regular function
  3. Reports all global names it accesses
  4. Reports all free variables it captures
  5. Recursively finds all nested code objects (comprehensions, nested functions) and their names and types
  6. 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_type checks co_flags bitmask - the correct way to detect generators, coroutines, and async generators
  • _find_nested recurses into co_consts - every comprehension and nested function is stored there as a types.CodeType instance
  • _total_consts recursively 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-readable report()

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.

© 2026 EngineersOfAI. All rights reserved.