Skip to content

How to Analyze Internals and Bytecode of Python Functions

Explore Python function internals using inspect __code__ attributes, use inspect for signatures, and analyze bytecode with dis for debugging, metaprogramming, and performance optimization.

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_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.

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_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

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.

  1. Use func.__code__ to access raw metadata like variable names and constants.
  2. Use inspect.signature(func) for a high-level view of parameters and type hints.
  3. Use dis.dis(func) to view the underlying bytecode instructions.
  4. Remember that built-in functions (written in C) cannot be inspected to the same depth as Python functions.
Navigation