Playground: add-file support (client-side, no storage)
Date: 2026-07-01 Status: Design approved, not yet implemented. Pick up here next session.
Problem
The files & exceptions Python chapter teaches reading files, but the playground had no way for a learner to bring a real file into the runtime. Concern was that this would need a server upload / database / size limits. It does not.
Key fact that makes this simple
The playground (docs/.vitepress/theme/components/PythonPlayground.vue) runs Pyodide — CPython on WebAssembly, entirely in the browser tab. Pyodide ships an in-memory virtual filesystem (MEMFS). So:
open(),read/write,with,try/except,FileNotFoundErroralready work today with zero changes — files live in browser RAM, never touch a server or DB.- "Upload" here means browser-memory only. Nothing leaves the tab. No storage, no privacy surface, no real need for a size limit beyond protecting RAM.
- Pyodide's default cwd is
/home/pyodide, which is whatopen('x.txt')resolves against. _pyodideis module-level, so the FS already persists for the whole tab session (across runs and across closing/reopening the modal); only a hard reload clears it.
Decisions (locked)
- What: let learners add their own file(s) from their computer into the FS.
- Placement: global playground feature (an "Add file" button in
PythonPlayground.vuenear the run controls), available from every chapter. - File count: multiple files coexist, shown as a chip list with remove (×).
- Size cap: 1 MB per file; oversize rejected with a friendly inline message.
- Persistence: in-memory only. Nothing written to disk/IndexedDB (safest, matches "nothing is stored" principle; within-session persistence already comes free from the module-level Pyodide instance). Re-upload after a hard reload.
Architecture
Source of truth = a JS array, not the FS. Keep workspace files as a reactive array of { name, bytes } in PythonPlayground.vue. Right before the existing exec step in the run logic, loop the array and pyodide.FS.writeFile(name, bytes).
Why: a learner can add a file before Pyodide has loaded, the file survives a Pyodide re-init, and the run path stays simple and ordering-bug-free.
Changes
PythonPlayground.vue(only file touched):- hidden
<input type="file" multiple>+ an "Add file" button near run controls - reactive
filesarray of{ name, bytes } - chip list of current files, each with a remove (×) button
- on pick:
File.arrayBuffer()→Uint8Array→ push tofiles[] - on run (before exec): for each file,
pyodide.FS.writeFile('/home/pyodide/'+name, bytes) - on remove: drop from array and
pyodide.FS.unlink(path)if present
- hidden
- No new files. No markdown changes. Chapter content unchanged (examples can now say "add a file, then read it").
Guardrails
- Filename sanitized to basename only (strip
/and..) — nothing escapes cwd. - Size cap 1 MB per file; oversize rejected, never written.
- Bytes not text — write
Uint8Arraysoopen(f)(text) andopen(f,'rb')(binary) both work. - Duplicate name replaces the existing entry (no silent double-write).
Data flow
pick file(s) → arrayBuffer() → Uint8Array → push to files[] → chip appears → on Run each file written to FS → learner's open(...) reads it → output shown. On reload, files[] and the FS both reset.
Error handling
- oversize / unreadable file → inline notice, file not added.
- reading a file the learner did not add → normal Python
FileNotFoundError(a teachable moment for the exceptions half of the chapter).
Test checklist
- add a file, read it back
- add two files, read both
- remove one, then run
- oversize file rejected with message
- binary file read via
'rb' - hard reload clears everything
Next step
Turn this into an implementation plan (writing-plans skill), then implement in PythonPlayground.vue. Relevant run logic is around lines 255-300 of that file (the exec/stream-reset block).

