2165 words
11 minutes
Creating a VS Code Extension in Python with Webview and Pyodide

Building a VS Code Extension: Running Python in the Browser with Webview and Pyodide#

Creating a Visual Studio Code extension extends the functionality of the popular code editor, allowing developers to integrate custom tools, languages, and workflows directly within the IDE. While VS Code extensions are typically written in TypeScript or JavaScript and run in a Node.js environment (the “extension host”), there are scenarios where executing complex logic, especially code written in other languages like Python, becomes necessary. Directly embedding and running a full Python environment within the Node.js extension host is generally impractical due to dependencies, environment management, and potential conflicts.

A powerful pattern to address this involves using a Webview. A Webview is essentially an embedded browser panel within VS Code that can display standard web content (HTML, CSS, JavaScript). This provides a sandboxed environment, separate from the extension host, where rich user interfaces and web technologies can be leveraged.

Combining a Webview with Pyodide offers a compelling solution for running Python code. Pyodide is a distribution of CPython compiled to WebAssembly, allowing Python to run directly in modern web browsers. By integrating Pyodide into a Webview, a VS Code extension can create an environment where Python code execution is facilitated client-side, within the isolated browser context of the Webview, orchestrated by the extension running in the Node.js host.

This approach enables the creation of extensions with interactive UIs that utilize Python logic and libraries, without requiring a local Python installation on the user’s machine or complex inter-process communication between the extension host and a separate Python process.

Essential Concepts#

Understanding the core components and their interaction is crucial for building such an extension.

VS Code Extension Architecture#

VS Code extensions operate in an “extension host” process, which is a Node.js environment separate from the main VS Code UI process.

  • Extensions are defined by a package.json manifest file specifying activation events (when the extension loads) and contributions (commands, menus, views).
  • The core logic resides in a main file (often extension.ts or extension.js), which is loaded when an activation event occurs.
  • Extensions interact with VS Code and the workspace via the vscode API.
  • Commands are a primary way users trigger extension functionality.

Webviews in VS Code#

Webviews provide a mechanism for extensions to create custom views or editors using standard web technologies.

  • They display arbitrary HTML content within a panel in the VS Code window.
  • Each Webview runs in an isolated context, similar to an <iframe> or a separate browser tab, with restricted access to the user’s machine for security.
  • Communication between the extension host (Node.js) and the Webview (JavaScript in the browser) is facilitated exclusively through message passing using the postMessage API. Data sent must be serializable JSON.
  • Local resources (like CSS, JavaScript, images) needed by the Webview must be exposed by the extension using special URIs obtained via webview.asWebviewUri.

Pyodide#

Pyodide brings the Python interpreter to the browser environment.

  • It compiles the CPython interpreter and standard library to WebAssembly.
  • It allows running Python code directly within a web page’s JavaScript environment.
  • Pyodide provides seamless interoperability between Python and JavaScript, allowing Python to access the DOM and JavaScript objects, and vice-versa.
  • It supports loading many pure-Python packages from PyPI, and some packages with C extensions that have been compiled to WebAssembly.

The Interaction Model#

The combined approach involves these steps:

  1. An activation event triggers the extension.
  2. A user action (e.g., executing a command) prompts the extension to create a new Webview panel.
  3. The extension sets the HTML content for the Webview. This HTML file typically loads necessary CSS and JavaScript resources.
  4. The Webview’s JavaScript loads the Pyodide environment (fetching the WebAssembly files and Python standard library).
  5. Once Pyodide is ready, the Webview is capable of executing Python code.
  6. The extension host can send Python code (as a string) to the Webview via postMessage.
  7. The Webview’s JavaScript receives the message, passes the Python code string to the Pyodide interpreter (pyodide.runPythonAsync).
  8. Pyodide executes the code. Output (like print statements) and results are captured by the Webview’s JavaScript.
  9. The Webview’s JavaScript sends the execution results and any errors back to the extension host via postMessage.
  10. The extension host receives the result message and can update the VS Code UI, display output in an output channel, or perform other actions.

This message-passing architecture ensures security and separation of concerns between the trusted extension host environment and the less-trusted Webview browser environment.

Step-by-Step Walkthrough: Building a Basic Pyodide Webview Extension#

This section outlines the process for creating a simple VS Code extension that opens a Webview capable of running Python code using Pyodide.

Prerequisites#

  • Node.js and npm installed.
  • VS Code installed.
  • Yeoman and the VS Code Extension Generator installed globally:
    Terminal window
    npm install -g yo generator-code

Project Setup#

  1. Run the VS Code Extension Generator:
    Terminal window
    yo code
  2. Select “New Extension (TypeScript)”.
  3. Follow the prompts: Enter extension name (e.g., pyodide-webview-demo), identifier, description, author, and select whether to initialize a Git repository.
  4. Open the generated project in VS Code (code pyodide-webview-demo).

The core files generated include package.json (extension manifest), src/extension.ts (main extension code), and tsconfig.json (TypeScript configuration).

Creating the Webview Files#

Create a directory (e.g., media) in the project root to hold the Webview’s static files. Inside media, create three files: index.html, script.js, and style.css.

  • media/index.html: The structure of the Webview’s page. This will include placeholders for loading scripts and styles that the extension will replace with correct webviewUris.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pyodide Console</title>
    <link rel="stylesheet" href="%styleUri%">
    </head>
    <body>
    <h1>Pyodide Web Console</h1>
    <textarea id="python-code" rows="10" cols="80">print("Hello from Pyodide!")</textarea>
    <button id="run-button">Run Python</button>
    <pre id="output"></pre>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js"></script>
    <script src="%scriptUri%"></script>
    </body>
    </html>
    • %styleUri% and %scriptUri% are placeholders.
    • The Pyodide script is loaded directly from a CDN.
  • media/style.css: Basic styling for the Webview content.

    body {
    font-family: sans-serif;
    padding: 20px;
    }
    #output {
    background-color: #f0f0f0;
    padding: 10px;
    white-space: pre-wrap;
    word-wrap: break-word;
    max-height: 300px;
    overflow-y: auto;
    }
  • media/script.js: The JavaScript code that runs inside the Webview. This handles loading Pyodide, receiving messages from the extension, sending messages back, and interacting with the HTML elements.

    const vscode = acquireVsCodeApi();
    let pyodide;
    async function loadPyodideAndRun() {
    console.log("Loading Pyodide...");
    try {
    pyodide = await loadPyodide({
    indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/"
    });
    console.log("Pyodide loaded.");
    appendToOutput("Pyodide ready.\n");
    } catch (err) {
    appendToOutput(`Error loading Pyodide: ${err}\n`, true);
    console.error("Error loading Pyodide:", err);
    }
    }
    async function runPythonCode(code) {
    if (!pyodide) {
    appendToOutput("Pyodide is not loaded yet.\n", true);
    return;
    }
    appendToOutput(`>>> ${code}\n`);
    try {
    // Redirect stdout/stderr to capture print output
    await pyodide.runPythonAsync(`
    import sys
    import io
    sys.stdout = io.StringIO()
    sys.stderr = io.StringIO()
    `);
    await pyodide.runPythonAsync(code);
    const stdout = pyodide.globals.get("sys").stdout.getvalue();
    const stderr = pyodide.globals.get("sys").stderr.getvalue();
    appendToOutput(stdout);
    if (stderr) {
    appendToOutput(stderr, true);
    }
    // Send result back to extension (optional for simple console)
    // vscode.postMessage({ type: 'pythonResult', result: stdout + stderr });
    } catch (err) {
    appendToOutput(`Error: ${err}\n`, true);
    // Send error back to extension
    // vscode.postMessage({ type: 'pythonError', error: err.toString() });
    console.error("Error running Python:", err);
    } finally {
    // Reset stdout/stderr
    await pyodide.runPythonAsync(`
    sys.stdout = sys.__stdout__
    sys.stderr = sys.__stderr__
    `);
    }
    }
    function appendToOutput(text, isError = false) {
    const outputDiv = document.getElementById('output');
    const span = document.createElement('span');
    span.style.color = isError ? 'red' : 'black';
    span.textContent = text;
    outputDiv.appendChild(span);
    outputDiv.scrollTop = outputDiv.scrollHeight; // Auto-scroll
    }
    // Handle messages from the extension host
    window.addEventListener('message', async event => {
    const message = event.data; // The JSON data sent from the extension
    switch (message.type) {
    case 'runCode':
    runPythonCode(message.code);
    break;
    // Add other message types as needed
    }
    });
    // Handle button click to run code from the textarea
    document.getElementById('run-button').addEventListener('click', () => {
    const code = document.getElementById('python-code').value;
    runPythonCode(code);
    });
    // Initial load
    loadPyodideAndRun();
    • acquireVsCodeApi() is a global function available in Webviews to get the messaging API.
    • pyodide.runPythonAsync is used for executing Python code, important for async operations like loading packages.
    • sys.stdout and sys.stderr are redirected within Pyodide to capture printed output and errors.

Implementing Extension Logic (src/extension.ts)#

Modify src/extension.ts to register a command that creates and manages the Webview panel.

  1. Import the vscode module.
  2. Register a command in the activate function.
  3. Inside the command handler:
    • Create a new vscode.WebviewPanel.
    • Set its html property by calling a function that loads index.html and replaces the placeholder URIs.
    • Handle messages received from the Webview using panel.webview.onDidReceiveMessage.
    • Optionally handle panel disposal using panel.onDidDispose.
import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
export function activate(context: vscode.ExtensionContext) {
console.log('Congratulations, extension "pyodide-webview-demo" is now active!');
let disposable = vscode.commands.registerCommand('pyodide-webview-demo.startConsole', () => {
// Create and show a new webview panel
const panel = vscode.window.createWebviewPanel(
'pyodideConsole', // Identifies the type of the webview. Used internally
'Pyodide Web Console', // Title of the panel displayed to the user
vscode.ViewColumn.One, // Editor column to show the new panel in.
{
// Enable javascript in the webview
enableScripts: true,
// Restrict the webview to only loading content from our extension's `media` directory.
localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
}
);
// Get the path to the media directory
const mediaPath = path.join(context.extensionPath, 'media');
// Get URIs for the webview
const scriptUri = panel.webview.asWebviewUri(vscode.Uri.file(path.join(mediaPath, 'script.js')));
const styleUri = panel.webview.asWebviewUri(vscode.Uri.file(path.join(mediaPath, 'style.css')));
// Load the HTML content
let htmlContent = fs.readFileSync(path.join(mediaPath, 'index.html'), 'utf8');
// Replace placeholders with actual URIs
htmlContent = htmlContent.replace('%scriptUri%', scriptUri.toString());
htmlContent = htmlContent.replace('%styleUri%', styleUri.toString());
// Set the HTML content
panel.webview.html = htmlContent;
// Handle messages from the webview
panel.webview.onDidReceiveMessage(
message => {
switch (message.type) {
case 'pythonResult':
// Handle results received from the webview's python execution
console.log('Python Result:', message.result);
// Example: Display in an output channel
// const outputChannel = vscode.window.createOutputChannel('Pyodide Output');
// outputChannel.appendLine('Result: ' + message.result);
// outputChannel.show();
return;
case 'pythonError':
// Handle errors
console.error('Python Error:', message.error);
// Example: Display error in an output channel
// const outputChannel = vscode.window.createOutputChannel('Pyodide Output');
// outputChannel.appendLine('Error: ' + message.error);
// outputChannel.show();
return;
}
},
undefined,
context.subscriptions
);
// Example: Send a message to the webview after it's created (optional)
// setTimeout(() => {
// panel.webview.postMessage({ type: 'runCode', code: 'print("Message from extension!")' });
// }, 2000);
});
context.subscriptions.push(disposable);
}
// This method is called when your extension is deactivated
export function deactivate() {}
  • vscode.window.createWebviewPanel creates the panel.
  • enableScripts: true is necessary for the JavaScript (script.js) to run within the Webview.
  • localResourceRoots is crucial for security, specifying which local directories the Webview is allowed to load resources from (like media).
  • panel.webview.asWebviewUri converts a local file path into a special URI (vscode-resource://... or similar) that the Webview can safely load.
  • panel.webview.onDidReceiveMessage sets up the handler for incoming messages from the Webview.

Testing the Extension#

  1. Press F5 in VS Code. This compiles the TypeScript extension and opens a new Extension Development Host window.
  2. In the Extension Development Host window, open the command palette (Ctrl+Shift+P or Cmd+Shift+P).
  3. Search for and run the command registered by the extension (e.g., “Pyodide Webview Demo: Start Console”).
  4. A new panel should open containing the HTML content. Once Pyodide loads (check the browser’s developer console for “Pyodide ready.”), you can type Python code in the text area and click “Run Python”.
  5. Output should appear in the <pre id="output"> block within the Webview.

Concrete Example: Enhancing the Console#

The basic console demonstrates the core communication and execution flow. This concept can be extended to more complex scenarios.

Example: Running a Python Script from the Workspace

Instead of just a textarea, the extension could allow running a Python script open in the editor.

  1. Extension Side:

    • Modify the command or create a new one (e.g., “Run Active Python File in Web Console”).
    • In the command handler, get the active text editor’s document and its content (vscode.window.activeTextEditor.document.getText()).
    • If the Webview panel is already open, send the script content to it via panel.webview.postMessage({ type: 'runCode', code: scriptContent }).
    • If the panel is not open, create it first, then send the code once it’s initialized (perhaps wait for a “Pyodide ready” message back from the Webview, or send it immediately and have the Webview queue it).
  2. Webview Side (script.js):

    • Ensure the window.addEventListener('message', ...) handler correctly processes the runCode message type.
    • The runPythonCode function already takes the code as an argument, so it can directly execute the received scriptContent.

This enhancement allows users to leverage the Webview’s Pyodide environment to run specific Python files, potentially within a controlled or sandboxed context provided by the Webview, separate from their main system’s Python installation. This is useful for running scripts that rely on specific Pyodide-compatible libraries or need to interact with a browser-like environment.

Considerations for Complex Examples:

  • Package Loading: Pyodide can load packages (await pyodide.loadPackage(...)). The extension could send messages to the Webview to install necessary packages before running a script.
  • Input: Handling user input (input()) in the Python code running in the Webview requires capturing the request in Pyodide, sending a message to the extension asking for input, and sending the user’s response back to Pyodide.
  • State: Managing state or persistent variables between Python executions requires careful handling of the Pyodide interpreter’s global scope or passing data explicitly.

Key Takeaways#

  • VS Code extensions can leverage Webviews to create rich, custom user interfaces using web technologies (HTML, CSS, JavaScript).
  • Pyodide enables running the Python interpreter and many Python libraries directly within the browser environment of a Webview.
  • This combination allows VS Code extensions to execute Python code client-side within an isolated sandbox, removing dependencies on a local Python installation or complex inter-process communication.
  • Communication between the extension host (Node.js) and the Webview (browser JS) is strictly through message passing (postMessage).
  • Local resources needed by the Webview must be explicitly exposed by the extension using webview.asWebviewUri for security.
  • This architecture is suitable for tasks like creating interactive consoles, visualizing data using browser-compatible Python libraries, running simple scripts in a controlled environment, or building educational tools within VS Code.
  • Performance, bundle size (due to Pyodide), and the asynchronous nature of communication and Python execution within the Webview are important considerations for complex applications.
Creating a VS Code Extension in Python with Webview and Pyodide
https://dev-resources.site/posts/creating-a-vs-code-extension-in-python-with-webview-and-pyodide/
Author
Dev-Resources
Published at
2025-06-30
License
CC BY-NC-SA 4.0