How Python Bytecode Hacking Unlocks the Forbidden Arts of Runtime Mutation

28 June, 2025 15 minute read

🧬 The Stack-Based Machine Behind Your Code

A majority of people think Python is interpreted directly line-by-line. In reality, CPython (the default Python interpreter) compiles your Python code into bytecode - a set of instructions for a stack-based virtual machine. This bytecode is stored in the __code__ object of every function (bet you didn't even know this existed).

Example Function:

def greet(name):
    return "Hello, " + name

When you run this, Python compiles it to bytecode that looks something like this:

1. LOAD_FAST 0

Loads the 'name' argument onto the stack

2. LOAD_CONST 1

Loads the string "Hello, " onto the stack

3. BINARY_ADD

Adds the two strings together

4. RETURN_VALUE

Returns the result from the stack

This bytecode is what Python actually executes. It's a low-level representation of your high-level code that runs on Python's VM (this is actually what creates those pesty .pyc files which you have to gitignore everytime).

Editing Function Bytecode

We can directly manipulate function bytecode at runtime to alter function behavior:


import types

def secret(x):
    return x + 1  # Original function adds 1

# Get the code object
original_code = secret.__code__

# Create new bytecode instructions:
# LOAD_FAST 0, LOAD_CONST 1, BINARY_SUBTRACT, RETURN_VALUE
new_bytecode = bytes([
    124, 0,    # LOAD_FAST 0 (load argument x)
    100, 1,    # LOAD_CONST 1 (load constant 1)
    24,        # BINARY_SUBTRACT (subtract instead of add)
    83         # RETURN_VALUE (return result)
])

# Create new constants tuple (only contains 1)
new_consts = (None, 1)

# Create a new code object with our modified bytecode
patched_code = types.CodeType(
    original_code.co_argcount,
    original_code.co_posonlyargcount,
    original_code.co_kwonlyargcount,
    original_code.co_nlocals,
    original_code.co_stacksize,
    original_code.co_flags,
    new_bytecode,
    new_consts,
    original_code.co_names,
    original_code.co_varnames,
    original_code.co_filename,
    original_code.co_name,
    original_code.co_firstlineno,
    original_code.co_lnotab
) # Keep other attributes the same
 
# Replace the function's code object
secret.__code__ = patched_code

print(secret(10))  # Now outputs 9 instead of 11

This code replaces the original bytecode of the secret function with new instructions that subtract 1 instead of adding 1. We've effectively rewritten Python logic at runtime (the stuff compilers do)!

Modify Logic Mid-Flight

Python's ast module lets you parse code into an Abstract Syntax Tree (AST), modify it, and recompile it. For example, forcing all conditionals to always execute (oh god no):


import ast

# Create a transformer that replaces all conditionals with True
class AlwaysTrueTransformer(ast.NodeTransformer):
    def visit_If(self, node):
        # Replace the test condition with True
        node.test = ast.Constant(value=True)
        return node

source_code = """
def check(val):
    if val > 10:
        return "big"
    else:
        return "small"
"""

# Parse source code into AST
parsed_ast = ast.parse(source_code)

# Apply our transformer
transformed_ast = AlwaysTrueTransformer().visit(parsed_ast)

# Fix missing line numbers
transformed_ast = ast.fix_missing_locations(transformed_ast)

# Compile and execute the modified AST
exec(compile(transformed_ast, filename="", mode="exec"))

# Test our transformed function
print(check(0))  # Outputs "big" because condition is always True
print(check(15)) # Also outputs "big"

You can apply this technique to class methods, imported modules, and even entire scripts.

Practical Use Cases

  • Security testing - Analyze and modify third-party library behavior
  • Metaprogramming - Create self-modifying code

⚠️ Important Safety Considerations

These techniques are powerful but EXTREMELY dangerous:

  • Bytecode manipulation can break stack traces and debugging
  • Changes may not be compatible across Python versions
  • Can introduce subtle bugs that are extremely hard to traceback
  • Can conflict with linters, type checkers, and other static analysis tools
Always use in controlled environments and with extensive testing. This post is purely educational and does not endorse using these techniques in production code.

Conclusion

Python's bytecode and AST manipulation features allow you to change program behavior at runtime in ways that aren't possible with standard code. While powerful, these techniques are best reserved for debugging, experimentation, or specialized tooling-use them thoughtfully and always consider maintainability and compatibility.