Table of Contents
Python bytecode
Python bytecode is the intermediate representation Python compiles source code into before execution. The dis module lets you inspect bytecode instructions, showing exactly what operations Python executes. Understanding bytecode helps you optimize hot paths, debug performance issues, and understand Python's execution model.
Use bytecode inspection to understand how Python executes your code, identify performance bottlenecks, or verify compiler optimizations.
Example
This example shows bytecode inspection with the dis module.
# run: python3 bytecode.py # description: inspecting bytecode with dis module import dis def simple_add(a, b): return a + b print("Bytecode for simple_add:") dis.dis(simple_add) print("\n" + "="*60 + "\n") # More complex function def factorial(n): if n <= 1: return 1 return n * factorial(n - 1) print("Bytecode for factorial:") dis.dis(factorial) print("\n" + "="*60 + "\n") # List comprehension vs loop def using_loop(): result = [] for i in range(10): result.append(i * 2) return result def using_comprehension(): return [i * 2 for i in range(10)] print("Loop version:") dis.dis(using_loop) print("\nList comprehension version:") dis.dis(using_comprehension) print("\n" + "="*60 + "\n") # Inspect bytecode of expressions code = "x = 1; y = 2; z = x + y" print(f"Bytecode for: {code}") dis.dis(compile(code, '<string>', 'exec')) print("\n" + "="*60 + "\n") # Inspect bytecode object func = lambda x: x * 2 print(f"Code object: {func.__code__}") print(f"Bytecode: {func.__code__.co_code}") print(f"Variable names: {func.__code__.co_varnames}") print(f"Constants: {func.__code__.co_consts}") print(f"Argument count: {func.__code__.co_argcount}") print("\nDisassembly:") dis.dis(func)
Common patterns
Inspecting functions:
dis.dis(func): print bytecode for functiondis.dis(lambda x: x+1): works on lambdas toodis.dis(code_object): works on code objects
Inspecting classes and methods:
dis.dis(ClassName.method): method bytecodedis.dis(ClassName): all methods in class (Python 3.11+)
Bytecode for strings:
compile(code_str, 'filename', 'exec'): compile string to code objectdis.dis(code_object): inspect compiled code
Code object introspection:
func.__code__: code object for functioncode.co_varnames: local variable namescode.co_consts: constants used in functioncode.co_names: names used (globals, attributes)code.co_argcount: number of argumentscode.co_code: raw bytecode bytes
Common bytecode instructions:
LOAD_CONST: load constant onto stackLOAD_FAST: load local variableLOAD_GLOBAL: load global variableBINARY_OP: binary operation (+, -, *, etc.)CALL_FUNCTION: call functionRETURN_VALUE: return from functionPOP_TOP: discard top of stackJUMP_IF_FALSE_OR_POP: conditional jump
Performance insights:
- Local variable access is faster than global (LOAD_FAST vs LOAD_GLOBAL)
- Function calls are expensive (visible in bytecode)
- List comprehensions are optimized (separate code path)
- Loop variables are looked up every iteration
When to inspect bytecode:
- Understanding performance characteristics
- Debugging mysterious behavior
- Verifying compiler optimizations work
- Learning how Python executes code
- Investigating performance regressions
Limitations:
- Bytecode is implementation detail; can change between Python versions
- Some optimizations happen at interpretation time (C-level)
- JIT compilers (PyPy, etc.) generate different bytecode
- Most optimization is better done at algorithm level, not bytecode
Python version differences:
- Bytecode format changes with Python versions
- Don't rely on specific bytecode in version-sensitive code
- Use
sys.version_infoif you need version-specific behavior
