In the previous post, I covered how the VS Code extension finds the current solution and test case files before starting debugpy.
This time, I want to look at what happens afterward: how the Python runtime actually executes the Solution class.
At first, I thought the runtime only needed to read the test case and call a method.
Once I started testing real LeetCode solutions, several additional problems appeared.
Input formats are not always consistent A Solution class may contain several methods Types such as ListNode and TreeNode do not exist locally Some copied solutions have no type annotations Some problems mutate their inputs and return nothing The user’s code may enter an infinite loop
These responsibilities are handled by leetcode_debug_runtime.py.
Parsing test cases
The test file accepts a format that is close to LeetCode’s examples.
1Input: nums = [2,7,11,15], target = 9 2Output: [0,1]
My first idea was to split everything after Input: using commas.
That does not work because lists also contain commas.
nums = [2,7,11,15], target = 9
A simple split(",") would also split the elements inside nums.
Instead, the parser reads the input one character at a time while tracking bracket depth and quoted strings.
A comma outside brackets → Separates method arguments A comma inside brackets → Belongs to a list or dictionary
The parser also supports multiline inputs.
1Input: 2grid = [ 3 ["1","1","0"], 4 ["0","1","0"], 5 ["1","0","1"] 6] 7Output: 3
It first tries to parse each value as JSON. If that fails, it uses Python’s ast.literal_eval().
LeetCode-style values such as null, true, and false are normalized to None, True, and False.
Multiple test cases can be separated with ---.
1Input: nums = [2,7,11,15], target = 9 2Output: [0,1] 3--- 4Input: nums = [3,2,4], target = 6 5Output: [1,2]
Finding the method to execute
The first version assumed that Solution would contain only one public method.
1class Solution: 2 def twoSum(self, nums, target): 3 ...
In that case, the runtime can simply execute twoSum().
Real solutions, however, often contain helper methods.
1class Solution: 2 def dfs(self, node): 3 ... 4 5 def maxDepth(self, root): 6 return self.dfs(root)
The runtime now has to decide whether dfs() or maxDepth() is the actual entry point.
It uses several pieces of information to make that decision.
Number of test case arguments Argument names from the test case Method parameter names Whether a method is called by another method
Python’s inspect module is used to examine method signatures, while ast is used to analyze calls such as self.dfs().
The runtime selects the method that best matches the test case and does not appear to be merely a helper called by another candidate method.
LeetCode-specific types
The following code works on LeetCode without defining ListNode.
1class Solution: 2 def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]: 3 ...
That is because LeetCode provides the class as part of its judge environment. A normal local Python environment does not.
The runtime therefore implements several common LeetCode types.
ListNode TreeNode Node NestedInteger Employee Interval Point ArrayReader BinaryMatrix MountainArray
Suppose the test file contains this input:
Input: head = [1,2,3,4,5]
After inspecting the method’s type information, the runtime converts the list into an actual linked list.
[1,2,3,4,5] → ListNode(1) └─ ListNode(2) └─ ListNode(3) └─ ListNode(4) └─ ListNode(5)
Once the method returns, the linked list is serialized back into a regular list.
ListNode(3) → ListNode(4) → ListNode(5) → [3,4,5]
Trees, graphs, N-ary trees, and random-pointer lists follow the same basic process.
Compact test case value → LeetCode-compatible Python object → Execute the Solution method → Serialize the result
Solutions without type annotations
Not every copied LeetCode solution keeps its type annotations.
1class Solution: 2 def maxDepth(self, root): 3 if not root: 4 return 0 5 6 return 1 + max( 7 self.maxDepth(root.left), 8 self.maxDepth(root.right) 9 )
Nothing explicitly says that root is a TreeNode.
The runtime therefore examines parameter names and how each parameter is used.
root.left and root.right → Probably a TreeNode head.next → Probably a ListNode node.children → Probably a Node
The first implementation only checked type annotations. After testing copied solutions, I found that many of them no longer had that information.
I added AST-based inference so the runtime could use the structure of the code itself as a fallback.
Problems that mutate their inputs
Some LeetCode methods do not return their result. Instead, they mutate one of their input values.
1class Solution: 2 def rotate(self, matrix: list[list[int]]) -> None: 3 ...
The return value of rotate() is None.
If the runtime only compared return values, the result would always appear as null, even when matrix had been modified correctly.
The runtime now uses the mutated input when the method returns None.
The method returns a value → Compare the return value The method returns None → Compare the mutated board, matrix, nums, or similar input
This supports common in-place problems such as:
rotate(matrix) solve(board) merge(nums1, m, nums2, n)
Handling infinite loops
A solution being debugged may enter an infinite loop.
while True: ...
If one test case never finishes, the runtime cannot continue to the remaining cases.
For that reason, each case has a default limit of five seconds.
Case 1: INFINITE LOOP stopped after 5s of CPU time
The limit can be changed with the following environment variable:
LEETCODE_DEBUG_TIMEOUT_SECONDS
Setting it to 0 disables the timeout.
Comparing the result
After executing the method, the runtime serializes the actual result and compares it with the value under Output:.
When both values match, it prints:
Case 1: PASS expected: [7,0,8] actual: [7,0,8]
When they differ, it prints:
Case 1: FAIL expected: [7,0,8] actual: [8,0,7]
If Output: is omitted, the runtime simply prints the actual result without validating it.
How the project evolved
The current structure was not implemented all at once.
The initial version only loaded a matching test file and executed a method from Solution.
I added more behavior as real solutions exposed missing cases.
Initial version - Read the test case file - Execute a Solution method - Convert common LeetCode types Later improvements - Handle Solution classes with helper methods - Infer untyped ListNode and TreeNode parameters - Support multiline inputs - Check the selected Python version - Stop suspected infinite loops - Support in-place problems - Add F5 debugging
Cases that previously failed were added to test_leetcode_debug_runtime.py.
The tests now cover situations such as helper methods, renamed parameters, untyped trees and linked lists, multiline grid input, infinite loops, and in-place output handling.
Conclusion
I initially thought running a LeetCode solution locally would be a matter of passing a few arguments to a method.
In practice, I had to recreate part of the execution environment that LeetCode normally provides.
Parse the test case → Find the Solution entry method → Convert the inputs → Execute the user’s code → Serialize the result → Compare it with the expected output
The finished project combines a VS Code extension with a small LeetCode-compatible Python runtime.
VS Code’s debugpy handles breakpoints, stepping, and variable inspection. The custom runtime prepares the submitted code so that debugpy can treat it like a normal Python program.
It does not reproduce every LeetCode feature. Hidden global APIs such as isBadVersion, knows, and read4 are not automatically mocked yet.
For common array, string, linked-list, and tree problems, however, the workflow becomes:
Write the solution → Create the matching test case file → Set a breakpoint → Press F5
The project started as a way to rely less on print() debugging. In the process, it also became an opportunity to learn how VS Code extensions communicate with a debugger, how Python code can be loaded dynamically, and how AST analysis and type conversion can recreate part of an online judge environment.