Hey ! I've made a simple stack-based virtual machine that translates and executes Python bytecode because I wanna have some fun. This project was originally created for a high school presentation to demonstrate how Python interprets code but I wanna share it here if anyone wants to take a look
disclaimer : the script was fully made by myself and NOT AI generated so if you see comments it's just mine ( crazy I need to say that ) -_-
When you write Python code, CPython doesn't execute it directly. It first compiles your source code into bytecode, then interprets that bytecode. This project replicates a small part of that process:
- Takes Python source code
- Compiles it using CPython's
compile() - Translates the bytecode into a custom virtual instruction set
- Executes the translated bytecode on a custom stack-based VM
It's not a full Python interpreter, it supports a limited subset of operations (variables, constants, print, comparisons, simple arithmetic, conditionals). The goal is educational, to show the core concepts of bytecode interpretation.
python vm.pyIt will:
- Compile the test code
- Translate it to virtual bytecode
- Save it as
output.vmpy - Execute the virtual bytecode
from vm import VM
source = """
x = 42
print(x)
if x > 10:
print("big")
else:
print("small")
"""
vm = VM(source)
vm.virtualize()
vm.run("output.vmpy")Python source → CPython compilation → Python bytecode → Translation → .vmpy file → VM execution
The VM uses a LIFO stack for temporary values. For example, var += 1 becomes:
LOAD_NAME var → push current value of var
LOAD_CONST 1 → push 1
BINARY_OP += → pop two values, add them, push result
STORE_NAME var → pop result and store in variables["var"]
See the handle_binary_op and handle_inplace_add methods in vm.py for how these operations are implemented.
The execution loop follows the fetch-decode-execute pattern ( simple as that ):
while ip < len(bytecode):
opcode, arg = fetch_instruction()
handler = handlers[opcode]
handler(arg)Check out the dispatcher method in vm.py
LOAD_CONST,LOAD_NAME,STORE_NAME- seehandle_load_const,handle_load_name,handle_store_nameCALL(function calls) - seehandle_callCOMPARE_OP(comparisons) - seehandle_compare_opBINARY_OP,INPLACE_ADD(arithmetic) - seehandle_binary_op,handle_inplace_addPOP_JUMP_IF_FALSE,JUMP_FORWARD(control flow) - seehandle_pop_jump_if_false,handle_jump_forwardRETURN_CONST,RETURN_VALUE- seehandle_return_const,handle_return_value
All handlers are defined in the VM class in vm.py.
I wanted to make my own custom format based on Python one ( yeah totally not the same thing )
magic(4) + bytecode_length(4) + bytecode + marshal(consts) + marshal(names)
magic: just a custom magic numberPYVMbytecode_length: Length of the virtual bytecode sectionbytecode: Encoded VM instructionsconsts/names: Serialized metadata using Python's marshal module
The VM state consists of:
stack: Temporary values for operationsvariables: Dictionary storing variable names and valuesip: Instruction pointer (program counter)consts: Constants from compiled codenames: Variable/function names from compiled code
This is an educational project, not a production interpreter:
- Only supports a small subset of Python bytecode
- No loops, functions, classes, or imports
- No exception handling
- Not suitable for running untrusted code
- The
.vmpyformat usesmarshalwhich is not designed for security
Just check the documentation in the docs/ folder
- Python 3.11+ (tested on 3.12)
- Standard library only:
opcode,dis,struct,marshal