Running Python in the Browser with Pyodide & WebContainer

Jun 14, 2026
17 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: running-python-in-the-browser-with-pyodide-webcontainer
Quick Answer

WebContainer integration, Monaco editor, xterm terminal, Pyodide Python runner, filesystem sandbox, data file uploads, and the security model that keeps it all safe.

Quick Summary

Learn how to run Python in the browser using Pyodide and WebContainer. Explore instant execution and full environment capabilities.

Running Python in the Browser

Every Python code block on this blog is editable and runnable. Readers can modify the code, execute it, and see the output — all within the browser, without a backend sandbox.

There are two execution modes: Pyodide (lightweight, instant) and WebContainer (full environment, slower startup). Here's how both work.


Pyodide: Instant Python Execution

For simple code blocks (standard library only, no external packages), Pyodide runs Python compiled to WebAssembly directly in the browser:

tsx
async function runPyodide(code: string): Promise<string> {
    const pyodide = await loadPyodide({ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/" });
    const output: string[] = [];

    pyodide.setStdout({ batched: (text: string) => output.push(text) });
    pyodide.setStderr({ batched: (text: string) => output.push(text) });

    await pyodide.runPythonAsync(code);
    return output.join("\n");
}

The Pyodide worker lives in a separate Web Worker to avoid blocking the main thread:

typescript
// src/workers/pythonRunner.worker.ts
self.onmessage = async (e: MessageEvent) => {
    const { code, type } = e.data;

    if (type === "run") {
        try {
            const result = await runPyodide(code);
            self.postMessage({ type: "result", output: result });
        } catch (err) {
            self.postMessage({ type: "error", message: (err as Error).message });
        }
    }
};

The frontend communicates with the worker:

tsx
const workerRef = useRef<Worker | null>(null);

useEffect(() => {
    workerRef.current = new Worker(
        new URL("@/workers/pythonRunner.worker", import.meta.url)
    );
    return () => workerRef.current?.terminate();
}, []);

function runCode(code: string) {
    setRunning(true);
    workerRef.current?.postMessage({ code, type: "run" });
    workerRef.current!.onmessage = (e) => {
        if (e.data.type === "result") setOutput(e.data.output);
        if (e.data.type === "error") setError(e.data.message);
        setRunning(false);
    };
}

WebContainer: Full Environment

For posts that need external packages (pandas, requests, numpy) or a real filesystem, WebContainer spins up a full Node.js environment in the browser, which then runs Python:

typescript
import { WebContainer } from "@webcontainer/api";

let containerInstance: WebContainer | null = null;

async function getContainer() {
    if (!containerInstance) {
        containerInstance = await WebContainer.boot();
    }
    return containerInstance;
}

async function runInContainer(code: string, files: DataFile[]) {
    const container = await getContainer();

    await container.mount({
        "script.py": { file: { contents: code } },
        ...Object.fromEntries(
            files.map((f) => [f.name, { file: { contents: f.url } }])
        ),
    });

    const process = await container.spawn("python3", ["script.py"]);
    const output: string[] = [];

    process.output.pipeTo(
        new WritableStream({
            write(data) { output.push(data); },
        })
    );

    await process.exit;
    return output.join("");
}

WebContainer starts in about 5-10 seconds on first load (cached after that). The trade-off: startup time for capability. Simple code blocks use Pyodide (instant); complex code blocks use WebContainer (5s startup, full package support).


Monaco Editor Integration

Code blocks render as Monaco editor instances with Python syntax highlighting:

tsx
import Editor from "@monaco-editor/react";

function InteractiveCodeCell({ code, language, onChange }: Props) {
    return (
        <div className="rounded-xl overflow-hidden border border-white/[0.08] my-6">
            <div className="flex items-center justify-between px-4 py-2 bg-white/[0.02] border-b border-white/[0.08]">
                <span className="text-[10px] font-bold uppercase tracking-widest text-white/40">
                    {language === "python" ? "Python" : language}
                </span>
                <RunButton />
            </div>
            <Editor
                height="auto"
                defaultLanguage={language === "python" ? "python" : language}
                value={code}
                onChange={(val) => onChange?.(val ?? "")}
                theme="vs-dark"
                options={{
                    minimap: { enabled: false },
                    fontSize: 14,
                    lineNumbers: "off",
                    scrollBeyondLastLine: false,
                    padding: { top: 12, bottom: 12 },
                    readOnly: false,
                }}
            />
            <OutputTerminal output={output} />
        </div>
    );
}

Xterm Terminal Integration

The output area uses xterm.js for a terminal-like experience:

tsx
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";

function OutputTerminal({ output }: { output: string }) {
    const terminalRef = useRef<HTMLDivElement>(null);
    const terminal = useRef<Terminal | null>(null);

    useEffect(() => {
        if (!terminalRef.current) return;

        terminal.current = new Terminal({
            theme: { background: "#0d0d0f", foreground: "#e4e4e4", cursor: "#e4e4e4" },
            fontSize: 13,
            fontFamily: "'JetBrains Mono', monospace",
            rows: 10,
            cursorBlink: false,
            disableStdin: true,
        });

        const fitAddon = new FitAddon();
        terminal.current.loadAddon(fitAddon);
        terminal.current.open(terminalRef.current);
        fitAddon.fit();

        return () => terminal.current?.dispose();
    }, []);

    useEffect(() => {
        if (terminal.current && output) {
            terminal.current.clear();
            terminal.current.write(output);
        }
    }, [output]);

    return <div ref={terminalRef} className="h-48" />;
}

Filesystem Sandbox

Posts can include data files (CSV, Excel) that are mounted into the WebContainer filesystem:

typescript
interface DataFile {
    name: string;
    url: string;
}

async function mountDataFiles(container: WebContainer, files: DataFile[]) {
    const mountEntries: Record<string, { file: { contents: string } }> = {};

    for (const file of files) {
        const response = await fetch(file.url);
        const contents = await response.text();
        mountEntries[file.name] = { file: { contents } };
    }

    await container.mount(mountEntries);
}

The frontend shows a file browser for uploaded data files:

tsx
function DataFileBrowser({ files }: { files: DataFile[] }) {
    return (
        <div className="card p-4">
            <p className="text-[10px] font-bold uppercase tracking-widest text-white/40 mb-3">
                Data Files
            </p>
            <div className="space-y-2">
                {files.map((file) => (
                    <div key={file.name}
                        className="flex items-center gap-2 text-sm text-white/70"
                    >
                        <FileIcon extension={file.name.split(".").pop()!} />
                        <span>{file.name}</span>
                    </div>
                ))}
            </div>
        </div>
    );
}

Security Model

Three layers of security prevent malicious code from escaping the sandbox:

Layer 1 — Pyodide's WebAssembly sandbox. Pyodide runs in a Web Worker with no access to the DOM, localStorage, or network. Code can only compute and print. No os.system(), no subprocess, no file writes outside the virtual filesystem.

Layer 2 — WebContainer's restricted environment. WebContainer provides a restricted POSIX-like environment. Network requests go through an HTTP proxy that blocks private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x) and limits outbound connections to allowlisted domains.

Layer 3 — Frontend isolation. Each code cell is rendered in an isolated React component. If Pyodide or WebContainer crashes, only that cell is affected. The rest of the page continues working.


Detection: Pyodide vs. WebContainer

The post editor has a toggle for "Enable Filesystem." When enabled, the post uses WebContainer. Otherwise, Pyodide is used:

typescript
const enableFilesystem = postData?.enable_filesystem ?? false;

async function runCode(code: string) {
    if (enableFilesystem) {
        return runInContainer(code, dataFiles);
    } else {
        return runPyodide(code);
    }
}

The decision is made at post-authoring time based on whether the post needs data files or external packages. Most posts use Pyodide. Posts with data analysis (pandas, numpy) or file I/O use WebContainer.


What's Next

The next post covers the Interview Simulator — AI-powered mock interviews, question generation from reading history, difficulty scaling, and feedback scoring.


Built with Pyodide, WebContainer, Monaco Editor, xterm.js, and zero server-side code execution.

Frequently Asked Questions

What is Pyodide used for in the browser?
Pyodide is used for running simple Python code blocks that only require the standard library, without external packages, directly in the browser by compiling Python to WebAssembly.
How does WebContainer differ from Pyodide?
WebContainer provides a full Node.js environment in the browser, allowing for the execution of Python code that requires external packages or a real filesystem, unlike Pyodide which is limited to the standard library.
What is the startup time for WebContainer?
WebContainer starts in about 5-10 seconds on the first load, and this time is reduced on subsequent loads due to caching.
How does the frontend communicate with the Pyodide worker?
The frontend communicates with the Pyodide worker by posting messages to it and listening for messages from it, which contain either the execution result or an error message.
What editor is used for rendering code blocks with Python syntax highlighting?
Code blocks are rendered as Monaco editor instances with Python syntax highlighting.

Related Work

See how this thinking shows up in shipped systems.