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).
def greet(name):
return "Hello, " + name
When you run this, Python compiles it to bytecode that looks something like this:
Loads the 'name' argument onto the stack
Loads the string "Hello, " onto the stack
Adds the two strings together
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).
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)!
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.
These techniques are powerful but EXTREMELY dangerous:
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.