I built a debugging-practice site where student code runs entirely in the
browser. Python via Pyodide, JavaScript in a worker. No
server executes anything.
No execution bill, no queue, no sandbox to maintain. But four things bit me hard.
1. Your arguments aren’t Python objects
Pass a JS object into Python and you get this:
TypeError: 'pyodide.ffi.JsProxy' object is not subscriptable
Enter fullscreen mode Exit fullscreen mode
It’s not a dict. It’s a live view of the JS object, and it supports neither
obj[key] nor .get().
Convert explicitly:
const pyArgs = input.map((arg) => pyodide.toPy(arg));
const result = fn(...pyArgs);
Enter fullscreen mode Exit fullscreen mode
2. null is not None
This one passed my entire test suite while being broken in production.
pyodide.toPy(null)
Enter fullscreen mode Exit fullscreen mode
check resulttype(v)
JsNull
bool(v)
False
✅ falsy, as expected
v is None
False
❌ the surprise
It’s falsy, so truthiness checks work fine. But is None fails — which was
exactly what my code was checking.
Why my tests missed it: the harness used json.loads. The app used toPy.
Different conversion paths, different answers.
If you need a real None, create it in Python. Don’t pass one across.
3. sys.settrace is a free step debugger
Want to show users their code running line by line? Python basically hands it to you:
def _tracer(frame, event, arg):
if frame.f_code.co_name != target:
return None # skip library frames
if event == "line":
steps.append({
"line": frame.f_lineno,
"locals": dict(frame.f_locals),
})
return _tracer
Enter fullscreen mode Exit fullscreen mode
Two things this naive version gets wrong:
- Add a step cap. A tight loop generates steps faster than it burns a 5-second timeout. You need both guards.
-
Handle
exception. During unwinding, thereturnevent still fires witharg=None. Miss it and your trace says “returned None” for code that crashed.
4. Your snapshots are lying
A user screenshot exposed this one.
Every step in the trace showed the final state of a list. Step 1 included
mutations that hadn’t happened yet.
tracing: nums = []; nums.append(1); nums.append(2)
- what the trace showed + what actually happened
- step 1 nums = [1, 2] + step 1 nums = []
- step 2 nums = [1, 2] + step 2 nums = [1]
- step 3 nums = [1, 2] + step 3 nums = [1, 2]
Enter fullscreen mode Exit fullscreen mode
frame.f_locals gives you references. Snapshot a list and you’ve stored a
pointer to something the program keeps mutating.
# copy at capture time
return json.loads(json.dumps(value))
Enter fullscreen mode Exit fullscreen mode
Any time you snapshot mutable state over time, you’re one reference away from
a history that rewrites itself.
Worth it?
Yes. Zero execution cost, scales infinitely, works on a locked-down lab machine
with nothing installed.
But test against the real runtime. Every bug above was found by running the
actual thing — not by unit tests around it. The JsNull one passed a fully green
suite.
Built for BugHunt — free debugging practice,
runs in your browser, no account needed.
답글 남기기