AST, Inspect, Metaprogramming, Code Generation
Advanced introspection and code generation techniques
Interview Question
"How do you inspect and modify Python code at runtime? Explain AST, the inspect module, and code generation. When would you use these techniques?"
Difficulty: Hard | Frequently asked at Google, Meta, Amazon
Theoretical Foundation
What is Introspection?
Introspection is the ability to examine objects, classes, and functions at runtime.
# Basic introspection
def example_function(x: int, y: str = "hello") -> bool:
"""Example function."""
return True
# Inspect function
print(f"Name: {example_function.__name__}")
print(f"Doc: {example_function.__doc__}")
print(f"Annotations: {example_function.__annotations__}")
print(f"Defaults: {example_function.__defaults__}")
Output:
Name: example_function
Doc: Example function.
Annotations: {'x': <class 'int'>, 'y': <class 'str'>, 'return': <class 'bool'>}
Defaults: ('hello',)
ℹ️
Key Concept: Python provides powerful introspection tools for examining and modifying code at runtime.
inspect Module
Basic Inspection
import inspect
import types
def example_function(x: int, y: str = "hello") -> bool:
"""Example function."""
return True
class ExampleClass:
"""Example class."""
def method(self, x: int) -> str:
return str(x)
@staticmethod
def static_method():
pass
@classmethod
def class_method(cls):
pass
# Get source code
print("Source code:")
print(inspect.getsource(example_function))
# Get function signature
sig = inspect.signature(example_function)
print(f"\nSignature: {sig}")
print(f"Parameters: {list(sig.parameters.keys())}")
# Get file location
print(f"\nFile: {inspect.getfile(example_function)}")
print(f"Line: {inspect.getsourcelines(example_function)[1]}")
# Inspect class
print(f"\nClass methods:")
for name, method in inspect.getmembers(ExampleClass, predicate=inspect.isfunction):
print(f" {name}")
Advanced Inspection
import inspect
import types
class ComplexClass:
"""Class with various method types."""
class_var = "class variable"
def __init__(self, value):
self.value = value
def instance_method(self):
return self.value
@staticmethod
def static_method():
return "static"
@classmethod
def class_method(cls):
return cls.class_var
@property
def prop(self):
return self.value
# Inspect class attributes
print("Class attributes:")
for name, value in inspect.getmembers(ComplexClass):
if not name.startswith('_'):
print(f" {name}: {type(value).__name__}")
# Check method types
obj = ComplexClass(42)
print("\nMethod types:")
print(f" instance_method: {inspect.ismethod(obj.instance_method)}")
print(f" static_method: {inspect.isfunction(ComplexClass.static_method)}")
print(f" class_method: {inspect.ismethod(ComplexClass.class_method)}")
print(f" prop: {inspect.isdatadescriptor(type(ComplexClass).prop)}")
# Get call stack
def outer():
def inner():
frame = inspect.currentframe()
print(f"\nCall stack:")
for frame_info in inspect.getouterframes(frame):
print(f" {frame_info.function} at {frame_info.filename}:{frame_info.lineno}")
inner()
outer()
Inspect Source Code
import inspect
def fibonacci(n: int) -> int:
"""Calculate Fibonacci number."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Get source code
source = inspect.getsource(fibonacci)
print("Source code:")
print(source)
# Get AST
tree = inspect.getsourcelines(fibonacci)
print(f"\nSource lines: {len(tree[0])}")
print(f"First line number: {tree[1]}")
# Get AST module
import ast
ast_tree = ast.parse(source)
print(f"\nAST nodes:")
for node in ast.walk(ast_tree):
print(f" {type(node).__name__}")
💡
Interview Tip: The inspect module is essential for debugging, testing, and metaprogramming.
Abstract Syntax Tree (AST)
Basic AST Operations
import ast
# Parse Python code
code = """
def add(a, b):
return a + b
result = add(1, 2)
"""
# Parse to AST
tree = ast.parse(code)
print("AST dump:")
print(ast.dump(tree, indent=2))
# Walk AST
print("\nWalking AST:")
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(f" Function: {node.name}")
elif isinstance(node, ast.Return):
print(f" Return statement")
elif isinstance(node, ast.Call):
print(f" Function call")
Output:
AST dump:
Module(
body=[
FunctionDef(
name='add',
args=arguments(
posonlyargs=[],
args=[arg(arg='a'), arg(arg='b')],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[]),
body=[
Return(value=BinOp(left=Name(id='a'), op=Add(), right=Name(id='b')))],
decorator_list=[],
returns=None),
Assign(
targets=[Name(id='result')],
value=Call(func=Name(id='add'), args=[Constant(value=1), Constant(value=2)], keywords=[]))],
type_ignores=[])
AST Transformation
import ast
import astor # pip install astor
class AddPrintTransformer(ast.NodeTransformer):
"""Add print statement before function calls."""
def visit_Call(self, node):
# Add print statement before function call
print_call = ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[ast.Constant(value=f"Calling function")],
keywords=[]
)
return ast.Expr(value=print_call)
# Example transformation
code = """
def greet(name):
return f"Hello, {name}!"
result = greet("World")
"""
# Parse and transform
tree = ast.parse(code)
transformer = AddPrintTransformer()
new_tree = transformer.visit(tree)
# Convert back to code
print("Transformed code:")
print(astor.to_source(new_tree))
AST Analysis
import ast
code = """
import os
import sys
from typing import List
def process_data(data: List[int]) -> int:
result = 0
for item in data:
if item > 0:
result += item
return result
class DataProcessor:
def __init__(self):
self.data = []
def add(self, item):
self.data.append(item)
"""
# Parse and analyze
tree = ast.parse(code)
# Find all imports
print("Imports:")
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
print(f" import {alias.name}")
elif isinstance(node, ast.ImportFrom):
print(f" from {node.module} import ...")
# Find all functions
print("\nFunctions:")
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(f" {node.name}() at line {node.lineno}")
# Find all classes
print("\nClasses:")
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
print(f" {node.name} at line {node.lineno}")
# Count complexity
def count_complexity(tree):
"""Count cyclomatic complexity."""
complexity = 1
for node in ast.walk(tree):
if isinstance(node, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
complexity += 1
return complexity
print(f"\nCyclomatic complexity: {count_complexity(tree)}")
Output:
Imports:
import os
import sys
from typing import List
Functions:
process_data() at line 6
__init__ at line 13
add at line 16
Classes:
DataProcessor at line 12
Cyclomatic complexity: 3
ℹ️
AST Use Cases: Code analysis, transformation, generation, linting, and refactoring tools.
Code Generation
Dynamic Code Execution
# exec() and eval()
code = """
def dynamic_function(x, y):
return x + y
"""
# Execute dynamic code
exec(code)
result = dynamic_function(1, 2)
print(f"Dynamic result: {result}")
# eval() for expressions
expression = "x ** 2 + y ** 2"
x, y = 3, 4
result = eval(expression)
print(f"Eval result: {result}")
# Compile and execute
compiled = compile(code, '<string>', 'exec')
exec(compiled)
AST-Based Code Generation
import ast
def generate_function(name: str, params: list, body: str) -> str:
"""Generate Python function code."""
# Create AST
tree = ast.Module(
body=[
ast.FunctionDef(
name=name,
args=ast.arguments(
posonlyargs=[],
args=[ast.arg(arg=p) for p in params],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[]
),
body=[ast.parse(body).body[0]],
decorator_list=[],
returns=None
)
],
type_ignores=[]
)
# Convert to source
import astor
return astor.to_source(tree)
# Generate function
code = generate_function(
name="add",
params=["a", "b"],
body="return a + b"
)
print("Generated code:")
print(code)
# Execute generated code
exec(code)
result = add(1, 2)
print(f"Result: {result}")
Output:
Generated code:
def add(a, b):
return a + b
Result: 3
Code Generator Class
import ast
from typing import List, Dict, Any
class CodeGenerator:
"""Generate Python code from specifications."""
def __init__(self):
self.indent_level = 0
def indent(self) -> str:
return " " * self.indent_level
def generate_class(self, name: str, bases: List[str],
methods: Dict[str, str]) -> str:
"""Generate class code."""
lines = []
# Class definition
bases_str = ", ".join(bases) if bases else ""
lines.append(f"class {name}({bases_str}):")
self.indent_level += 1
# Methods
for method_name, method_body in methods.items():
lines.append(f"{self.indent()}def {method_name}(self):")
self.indent_level += 1
lines.append(f"{self.indent()}{method_body}")
self.indent_level -= 1
lines.append("")
self.indent_level -= 1
return "\n".join(lines)
def generate_dataclass(self, name: str, fields: Dict[str, str]) -> str:
"""Generate dataclass code."""
lines = ["from dataclasses import dataclass", ""]
lines.append("@dataclass")
lines.append(f"class {name}:")
self.indent_level += 1
for field_name, field_type in fields.items():
lines.append(f"{self.indent()}{field_name}: {field_type}")
self.indent_level -= 1
return "\n".join(lines)
# Usage
generator = CodeGenerator()
# Generate class
class_code = generator.generate_class(
name="UserService",
bases=["BaseService"],
methods={
"get_user": "return self.db.get_user(user_id)",
"create_user": "return self.db.create_user(user_data)"
}
)
print("Generated class:")
print(class_code)
# Generate dataclass
dataclass_code = generator.generate_dataclass(
name="User",
fields={
"id": "int",
"name": "str",
"email": "str"
}
)
print("\nGenerated dataclass:")
print(dataclass_code)
⚠️
Security Warning: exec() and eval() can execute arbitrary code. Use with caution and validation.
Metaprogramming Patterns
Dynamic Attribute Access
class DynamicProxy:
"""Proxy that dynamically forwards attribute access."""
def __init__(self, target):
self._target = target
def __getattr__(self, name):
"""Forward attribute access to target."""
print(f"Accessing: {name}")
return getattr(self._target, name)
def __setattr__(self, name, value):
"""Forward attribute setting to target."""
if name.startswith('_'):
super().__setattr__(name, value)
else:
print(f"Setting: {name} = {value}")
setattr(self._target, name, value)
# Usage
class RealService:
def method(self):
return "Real method"
real = RealService()
proxy = DynamicProxy(real)
# Attribute access is forwarded
print(proxy.method())
proxy.new_attr = "value"
Dynamic Class Creation
# Using type()
def method1(self):
return "method1"
def method2(self):
return "method2"
# Create class dynamically
MyClass = type('MyClass', (object,), {
'method1': method1,
'method2': method2,
'class_var': 'value'
})
# Usage
obj = MyClass()
print(f"method1: {obj.method1()}")
print(f"method2: {obj.method2()}")
print(f"class_var: {obj.class_var}")
# Using exec for complex classes
code = """
class DynamicClass:
def __init__(self, value):
self.value = value
def get_value(self):
return self.value
"""
exec(code)
obj = DynamicClass(42)
print(f"Dynamic: {obj.get_value()}")
Decorator for Code Generation
import ast
import inspect
from typing import Any, Callable
def auto_serialize(cls):
"""Decorator that adds serialization methods to a class."""
# Get class source
source = inspect.getsource(cls)
# Create serialization method
serialize_code = f"""
def serialize(self):
return {{k: v for k, v in self.__dict__.items()}}
"""
# Add method to class
exec(compile(serialize_code, '<string>', 'exec'), cls.__dict__)
# Create deserialize class method
deserialize_code = f"""
@classmethod
def deserialize(cls, data):
return cls(**data)
"""
exec(compile(deserialize_code, '<string>', 'exec'), cls.__dict__)
return cls
@auto_serialize
class User:
def __init__(self, name, email):
self.name = name
self.email = email
# Usage
user = User("Alice", "alice@example.com")
data = user.serialize()
print(f"Serialized: {data}")
user2 = User.deserialize(data)
print(f"Deserialized: {user2.name}, {user2.email}")
Output:
Serialized: {'name': 'Alice', 'email': 'alice@example.com'}
Deserialized: Alice, alice@example.com
💡
Interview Tip: Metaprogramming is powerful but complex. Use it only when simpler solutions won't work.
Advanced Patterns
Function Wrapping
import functools
import time
from typing import Callable, Any
def wrap_function(func: Callable) -> Callable:
"""Wrap function with additional behavior."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
# Dynamic wrapping
def wrap_functions(module):
"""Wrap all functions in a module."""
for name in dir(module):
obj = getattr(module, name)
if callable(obj) and not name.startswith('_'):
setattr(module, name, wrap_function(obj))
# Example module
class MathOperations:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
# Wrap all methods
wrap_functions(MathOperations)
# Usage
math_ops = MathOperations()
print(math_ops.add(1, 2))
print(math_ops.multiply(3, 4))
Code Analysis
import ast
from typing import List, Dict
class CodeAnalyzer:
"""Analyze Python code."""
def __init__(self, code: str):
self.tree = ast.parse(code)
self.stats = {
'functions': 0,
'classes': 0,
'methods': 0,
'lines': len(code.split('\n'))
}
def analyze(self) -> Dict[str, Any]:
"""Analyze code and return statistics."""
for node in ast.walk(self.tree):
if isinstance(node, ast.FunctionDef):
self.stats['functions'] += 1
elif isinstance(node, ast.ClassDef):
self.stats['classes'] += 1
# Count methods
for item in node.body:
if isinstance(item, ast.FunctionDef):
self.stats['methods'] += 1
return self.stats
def find_functions(self) -> List[str]:
"""Find all function names."""
functions = []
for node in ast.walk(self.tree):
if isinstance(node, ast.FunctionDef):
functions.append(node.name)
return functions
# Usage
code = """
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def helper():
pass
"""
analyzer = CodeAnalyzer(code)
stats = analyzer.analyze()
print(f"Statistics: {stats}")
print(f"Functions: {analyzer.find_functions()}")
Output:
Statistics: {'functions': 3, 'classes': 1, 'methods': 2, 'lines': 11}
Functions: ['add', 'subtract', 'helper']
AST Visitor
import ast
from typing import List
class FunctionVisitor(ast.NodeVisitor):
"""Visit all functions in AST."""
def __init__(self):
self.functions: List[Dict] = []
def visit_FunctionDef(self, node):
self.functions.append({
'name': node.name,
'args': [arg.arg for arg in node.args.args],
'line': node.lineno
})
self.generic_visit(node)
# Usage
code = """
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def complex(x, y=10, *args, **kwargs):
pass
"""
tree = ast.parse(code)
visitor = FunctionVisitor()
visitor.visit(tree)
print("Functions found:")
for func in visitor.functions:
print(f" {func['name']}({', '.join(func['args'])}) at line {func['line']}")
Output:
Functions found:
add(a, b) at line 2
subtract(a, b) at line 5
complex(x, y=10, *args, **kwargs) at line 8
Complexity Analysis
Performance Impact
| Technique | Overhead | Use Case |
|---|---|---|
| inspect | Low | Debugging, testing |
| AST parsing | Medium | Code analysis |
| Code generation | High | Dynamic code |
| Metaprogramming | Varies | Framework code |
When to Use
# inspect: Debugging, testing, documentation
# AST: Code analysis, linting, refactoring
# Code generation: Frameworks, DSLs, templates
# Metaprogramming: Advanced patterns, decorators
Interview Tips
Common Follow-up Questions
-
"When would you use AST manipulation?"
- Building linters or formatters
- Code generation tools
- Static analysis
- Refactoring tools
-
"What are the security risks of exec/eval?"
- Code injection attacks
- Unintended code execution
- Always validate input
-
"How do you debug metaprogramming code?"
- Use inspect module
- Print AST dumps
- Step through code generation
Code Review Tips
# BAD: Using exec without validation
def unsafe_execute(code):
exec(code) # Dangerous!
# GOOD: Validate before executing
def safe_execute(code):
# Validate code structure
try:
tree = ast.parse(code)
except SyntaxError as e:
raise ValueError(f"Invalid code: {e}")
# Additional validation...
exec(code)
# BAD: Too much metaprogramming
class OverEngineered:
def __getattr__(self, name):
# Complex dynamic behavior
pass
# GOOD: Simple, explicit code
class Simple:
def method(self):
return "explicit"
⚠️
Common Mistake: Overusing metaprogramming when simpler solutions exist. Always prefer explicit code.
Summary
| Tool | Purpose | When to Use |
|---|---|---|
| inspect | Introspection | Debugging, testing |
| AST | Code analysis | Linters, generators |
| exec/eval | Dynamic execution | Careful use only |
| Metaprogramming | Code modification | Frameworks, decorators |
Best Practices
- Use inspect for debugging and testing
- Use AST for code analysis and generation
- Avoid exec/eval when possible
- Document metaprogramming clearly
- Test generated code thoroughly
- Consider security implications
ℹ️
Key Takeaway: Introspection and metaprogramming are powerful tools for advanced Python development. Use them judiciously.
Practice Problems
- Code Analyzer: Build a tool that analyzes Python code complexity
- AST Transformer: Create a code transformer that adds logging
- Dynamic Class: Generate classes from JSON specifications
- Function Wrapper: Build a decorator factory for function wrapping
- Linter: Create a simple linter using AST
Further Reading
- Python Docs:
inspect,astmodules - AST Tutorial: https://docs.python.org/3/library/ast.html
- Books: "Python Metaprogramming" by David Mertz
- Tools: pylint, flake8, black (use AST internally)
Remember: Metaprogramming should be used sparingly and only when it provides significant benefits.