How to Analyze Internals and Bytecode of Python Functions
In Python, functions are first-class objects, meaning they have attributes and methods just like integers, strings, or lists. Understanding the internal structure of a function, how it stores default arguments, where the compiled bytecode lives, and how to inspect it at runtime, is essential for advanced debugging, writing decorators, and metaprogramming.
This guide explores the anatomy of Python functions using the __code__ attribute, the inspect module, and the dis (disassembler) library.
The Anatomy of a Function Object
When you define a function using def, Python creates an object with several standard attributes. The most important one is __code__, which contains the compiled bytecode and metadata about the function's local variables and arguments.
Accessing Internal Attributes directly
You can access metadata directly through "dunder" (double underscore) attributes.
def calculate(a, b=10):
"""Returns the sum of a and b."""
result = a + b
return result
# ✅ Inspecting dunder attributes
print(f"Name: {calculate.__name__}")
print(f"Docstring: {calculate.__doc__}")
print(f"Defaults: {calculate.__defaults__}")
# ✅ Inspecting the __code__ object
code_obj = calculate.__code__
print(f"Local Variables: {code_obj.co_varnames}")
print(f"Constants: {code_obj.co_consts}")
Output:
Name: calculate
Docstring: Returns the sum of a and b.
Defaults: (10,)
Local Variables: ('a', 'b', 'result')
Constants: ('Returns the sum of a and b.',)
The __defaults__ attribute is created when the function is defined, not called. This explains why mutable default arguments (like lists) persist across calls, i.e. they are stored inside the function object itself.
Runtime Introspection with inspect
While accessing dunder attributes works, it is often safer and more readable to use the standard library's inspect module. It handles details like extracting signatures and type hints gracefully.
Extracting Signatures and Parameters
Use inspect.signature() to analyze parameters, defaults, and annotations programmatically.
import inspect
def process_data(data: list, verbose: bool = False) -> int:
return len(data)
# ✅ Using inspect to analyze the signature
sig = inspect.signature(process_data)
print(f"Function: {process_data.__name__}")
print(f"Return Annotation: {sig.return_annotation}")
for name, param in sig.parameters.items():
print(f"Param: {name}, Default: {param.default}, Type: {param.annotation}")
Output:
Function: process_data
Return Annotation: <class 'int'>
Param: data, Default: <class 'inspect._empty'>, Type: <class 'list'>
Param: verbose, Default: False, Type: <class 'bool'>
Analyzing Bytecode with dis
Python source code is compiled into "bytecode", i.e. low-level instructions for the Python virtual machine. To understand exactly what Python is doing under the hood (e.g., to optimize performance), you can use the dis module to disassemble the code.
Disassembling a Function
import dis
def add_numbers(x, y):
return x + y
# ✅ Disassembling the function to see bytecode instructions
print("Bytecode Disassembly:")
dis.dis(add_numbers)
Output:
Bytecode Disassembly:
4 0 LOAD_FAST 0 (x)
2 LOAD_FAST 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
Explanation of the output:
- LOAD_FAST: Pushes a reference to a local variable onto the stack.
- BINARY_ADD: Pops two values, adds them, and pushes the result.
- RETURN_VALUE: Returns the top of the stack to the caller.
Use dis.dis() when you are unsure if two snippets of code compile to the same underlying instructions or to debug subtle behavioral differences.
Common Pitfall: Inspecting Built-in Functions
A common error occurs when trying to inspect the source code of built-in functions (like print, len, or math.sqrt) using inspect.getsource.
Error: TypeError on Built-ins
Built-in functions are typically written in C, not Python. Therefore, they have no Python source code to retrieve.
import inspect
try:
# ⛔️ Incorrect: 'len' is written in C, so it has no Python source
print(inspect.getsource(len))
except TypeError as e:
print(f"Error: {e}")
Output:
Error: module, class, method, function, traceback, frame, or code object was expected, got builtin_function_or_method
Solution: Check if Built-in
Before attempting to access source code, verify if the object is a pure Python function.
import inspect
target_func = len
# ✅ Correct: Check if it is a built-in function first
if inspect.isbuiltin(target_func):
print(f"'{target_func.__name__}' is a built-in function (C implementation). No source available.")
else:
print(inspect.getsource(target_func))
Output:
'len' is a built-in function (C implementation). No source available.
Conclusion
Analyzing Python function internals allows you to understand how your code is compiled and executed.
- Use
func.__code__to access raw metadata like variable names and constants. - Use
inspect.signature(func)for a high-level view of parameters and type hints. - Use
dis.dis(func)to view the underlying bytecode instructions. - Remember that built-in functions (written in C) cannot be inspected to the same depth as Python functions.