🎉 75% of content is free forever — Unlock Premium from $10/mo →
CW
Search courses…
💼 Servicesℹ️ About✉️ ContactView Pricing Plansfrom $10

AST, Inspect, Metaprogramming, Code Generation

PythonMetaprogramming⭐ Premium

Advertisement

Google, Meta & Amazon Interview

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:

Architecture Diagram
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:

Architecture Diagram
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:

Architecture Diagram
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:

Architecture Diagram
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:

Architecture Diagram
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:

Architecture Diagram
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:

Architecture Diagram
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

TechniqueOverheadUse Case
inspectLowDebugging, testing
AST parsingMediumCode analysis
Code generationHighDynamic code
MetaprogrammingVariesFramework 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

  1. "When would you use AST manipulation?"

    • Building linters or formatters
    • Code generation tools
    • Static analysis
    • Refactoring tools
  2. "What are the security risks of exec/eval?"

    • Code injection attacks
    • Unintended code execution
    • Always validate input
  3. "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

ToolPurposeWhen to Use
inspectIntrospectionDebugging, testing
ASTCode analysisLinters, generators
exec/evalDynamic executionCareful use only
MetaprogrammingCode modificationFrameworks, decorators

Best Practices

  1. Use inspect for debugging and testing
  2. Use AST for code analysis and generation
  3. Avoid exec/eval when possible
  4. Document metaprogramming clearly
  5. Test generated code thoroughly
  6. Consider security implications

ℹ️

Key Takeaway: Introspection and metaprogramming are powerful tools for advanced Python development. Use them judiciously.


Practice Problems

  1. Code Analyzer: Build a tool that analyzes Python code complexity
  2. AST Transformer: Create a code transformer that adds logging
  3. Dynamic Class: Generate classes from JSON specifications
  4. Function Wrapper: Build a decorator factory for function wrapping
  5. Linter: Create a simple linter using AST

Further Reading

Remember: Metaprogramming should be used sparingly and only when it provides significant benefits.

Advertisement