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
print(f"Name: {calculate.__name__}")
print(f"Docstring: {calculate.__doc__}")
print(f"Defaults: {calculate.__defaults__}")
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.',)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)
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
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_VALUEExplanation 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.
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:
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_methodSolution: Check if Built-in
Before attempting to access source code, verify if the object is a pure Python function.
import inspect
target_func = len
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.