Skip to main content

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.',)
note

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

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.

  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.