Python distribution for the browser and Node.js based on WebAssembly that enables running Python code with full JavaScript interoperability
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advanced functionality including canvas integration, worker support, interrupt handling, and archive operations.
HTML5 canvas integration for matplotlib and graphics libraries.
const canvas: {
setCanvas2D(canvas: HTMLCanvasElement): void;
getCanvas2D(): HTMLCanvasElement | undefined;
setCanvas3D(canvas: HTMLCanvasElement): void;
getCanvas3D(): HTMLCanvasElement | undefined;
};Set interrupt signal buffer for webworker communication.
function setInterruptBuffer(buffer: TypedArray): void;Parameters:
buffer - SharedArrayBuffer for interrupt signals between threadsManually check for pending interrupt signals.
function checkInterrupt(): void;Register Comlink for seamless worker proxy communication.
function registerComlink(Comlink: any): void;Parameters:
Comlink - Comlink library instanceExtract archive files (tar, zip, wheel) into the file system.
function unpackArchive(
buffer: TypedArray | ArrayBuffer,
format: string,
options?: {
extractDir?: string;
}
): void;Parameters:
buffer - Archive file dataformat - Archive format ('tar', 'zip', 'wheel', 'gztar', etc.)options.extractDir - Directory to extract to (default: current directory)POSIX error code constants for file system operations.
const ERRNO_CODES: {
[code: string]: number;
};// Set up canvas for matplotlib
const canvas = document.getElementById('matplotlib-canvas');
pyodide.canvas.setCanvas2D(canvas);
await pyodide.loadPackage(['matplotlib']);
pyodide.runPython(`
import matplotlib.pyplot as plt
import numpy as np
# Create sample plot
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
plt.figure(figsize=(8, 6))
plt.plot(x, y, label='sin(x)')
plt.plot(x, np.cos(x), label='cos(x)')
plt.legend()
plt.title('Trigonometric Functions')
plt.xlabel('x')
plt.ylabel('y')
plt.grid(True)
plt.show() # This will render to the canvas
`);// Set up 3D canvas
const canvas3d = document.getElementById('threejs-canvas');
pyodide.canvas.setCanvas3D(canvas3d);
// Register Three.js as a module
pyodide.registerJsModule('threejs', {
Scene: THREE.Scene,
PerspectiveCamera: THREE.PerspectiveCamera,
WebGLRenderer: THREE.WebGLRenderer,
BoxGeometry: THREE.BoxGeometry,
MeshBasicMaterial: THREE.MeshBasicMaterial,
Mesh: THREE.Mesh
});
pyodide.runPython(`
from threejs import Scene, PerspectiveCamera, WebGLRenderer
from threejs import BoxGeometry, MeshBasicMaterial, Mesh
# Create 3D scene
scene = Scene()
camera = PerspectiveCamera(75, 800/600, 0.1, 1000)
renderer = WebGLRenderer()
# Create geometry
geometry = BoxGeometry(1, 1, 1)
material = MeshBasicMaterial({'color': 0x00ff00})
cube = Mesh(geometry, material)
scene.add(cube)
camera.position.z = 5
# Render scene
renderer.render(scene, camera)
`);// Main thread
const sharedBuffer = new SharedArrayBuffer(4);
const interruptArray = new Int32Array(sharedBuffer);
// Send buffer to worker
worker.postMessage({ sharedBuffer });
// Worker code
self.onmessage = async function(e) {
if (e.data.sharedBuffer) {
const pyodide = await loadPyodide();
const interruptArray = new Int32Array(e.data.sharedBuffer);
// Set interrupt buffer
pyodide.setInterruptBuffer(interruptArray);
// Long running Python code
pyodide.runPython(`
import time
import signal
def signal_handler(signum, frame):
print(f"Received signal {signum}")
raise KeyboardInterrupt("Interrupted by signal")
# Install signal handler
signal.signal(signal.SIGINT, signal_handler)
# Long computation
for i in range(1000000):
# Check for interrupts periodically
if i % 10000 == 0:
print(f"Progress: {i}")
time.sleep(0.001)
`);
}
};
// Main thread - send interrupt
setTimeout(() => {
interruptArray[0] = 2; // SIGINT
}, 5000);// JavaScript-controlled interrupt checking
let shouldStop = false;
pyodide.registerJsModule('jscontrol', {
checkStop: () => shouldStop
});
// Start long-running Python process
const runLongTask = async () => {
pyodide.runPythonAsync(`
from jscontrol import checkStop
import time
for i in range(1000000):
if i % 1000 == 0:
if checkStop():
print(f"Task interrupted at iteration {i}")
break
print(f"Processing: {i}")
time.sleep(0.001)
`);
};
// Stop button handler
document.getElementById('stop-button').onclick = () => {
shouldStop = true;
};
// Start button handler
document.getElementById('start-button').onclick = () => {
shouldStop = false;
runLongTask();
};// Main thread
import * as Comlink from 'comlink';
const worker = new Worker('pyodide-worker.js');
const PyodideWorker = Comlink.wrap(worker);
// Worker (pyodide-worker.js)
import * as Comlink from 'comlink';
import { loadPyodide } from 'pyodide';
class PyodideService {
constructor() {
this.pyodide = null;
}
async init() {
this.pyodide = await loadPyodide();
this.pyodide.registerComlink(Comlink);
return 'Pyodide loaded in worker';
}
runCode(code) {
return this.pyodide.runPython(code);
}
async runCodeAsync(code) {
return await this.pyodide.runPythonAsync(code);
}
// Return Python functions as Comlink proxies
getPythonFunction(code) {
this.pyodide.runPython(code);
return this.pyodide.globals.get('exported_function');
}
}
Comlink.expose(new PyodideService());
// Usage in main thread
const service = await new PyodideWorker();
await service.init();
const result = await service.runCode('2 + 2');
console.log(result); // 4
// Get Python function proxy
const pythonFunc = await service.getPythonFunction(`
def exported_function(x, y):
return x ** 2 + y ** 2
`);
const funcResult = await pythonFunc(3, 4);
console.log(funcResult); // 25// Extract Python package archives
async function installCustomPackage(archiveUrl, format) {
try {
// Download archive
const response = await fetch(archiveUrl);
const buffer = await response.arrayBuffer();
// Extract to temporary directory
pyodide.FS.mkdir('/tmp/extract');
pyodide.unpackArchive(buffer, format, {
extractDir: '/tmp/extract'
});
// List extracted contents
const files = pyodide.FS.readdir('/tmp/extract');
console.log('Extracted files:', files);
// Move to site-packages if it's a Python package
if (files.includes('setup.py') || files.includes('pyproject.toml')) {
pyodide.runPython(`
import sys
import os
sys.path.insert(0, '/tmp/extract')
# Install package
os.chdir('/tmp/extract')
import subprocess
subprocess.run([sys.executable, 'setup.py', 'install'])
`);
}
} catch (error) {
console.error('Failed to install custom package:', error);
}
}
// Install from wheel file
await installCustomPackage('https://example.com/package.whl', 'wheel');
// Install from tar.gz
await installCustomPackage('https://example.com/package.tar.gz', 'gztar');// Process data archives
async function processDataArchive(archiveData, format) {
// Extract data archive
pyodide.unpackArchive(archiveData, format, {
extractDir: '/data'
});
// Process extracted data with Python
const results = pyodide.runPython(`
import os
import pandas as pd
data_files = []
for root, dirs, files in os.walk('/data'):
for file in files:
if file.endswith('.csv'):
file_path = os.path.join(root, file)
data_files.append(file_path)
# Load and combine CSV files
dataframes = []
for file_path in data_files:
df = pd.read_csv(file_path)
df['source_file'] = file_path
dataframes.append(df)
if dataframes:
combined_df = pd.concat(dataframes, ignore_index=True)
{
'total_rows': len(combined_df),
'columns': list(combined_df.columns),
'files_processed': len(data_files)
}
else:
{'error': 'No CSV files found'}
`);
return results;
}
// Usage with ZIP file containing CSV data
const zipData = await fetch('/data-archive.zip').then(r => r.arrayBuffer());
const results = await processDataArchive(zipData, 'zip');
console.log('Processing results:', results);// Use ERRNO codes for file system error handling
function safeFileOperation(operation) {
try {
return operation();
} catch (error) {
const errno = error.errno;
if (errno === pyodide.ERRNO_CODES.ENOENT) {
console.error('File or directory does not exist');
} else if (errno === pyodide.ERRNO_CODES.EACCES) {
console.error('Permission denied');
} else if (errno === pyodide.ERRNO_CODES.EEXIST) {
console.error('File already exists');
} else if (errno === pyodide.ERRNO_CODES.ENOSPC) {
console.error('No space left on device');
} else {
console.error(`File system error (${errno}):`, error.message);
}
return null;
}
}
// Safe file operations
const content = safeFileOperation(() =>
pyodide.FS.readFile('/nonexistent/file.txt', { encoding: 'utf8' })
);
const success = safeFileOperation(() =>
pyodide.FS.writeFile('/protected/file.txt', 'data')
);// Monitor Pyodide performance
class PyodideMonitor {
constructor() {
this.metrics = {
executionTimes: [],
memoryUsage: [],
packageLoadTimes: []
};
}
async timeExecution(code, description = 'Execution') {
const startTime = performance.now();
const startMemory = this.getMemoryUsage();
try {
const result = await pyodide.runPython(code);
const endTime = performance.now();
const endMemory = this.getMemoryUsage();
const executionTime = endTime - startTime;
const memoryDelta = endMemory - startMemory;
this.metrics.executionTimes.push({
description,
time: executionTime,
timestamp: new Date().toISOString()
});
this.metrics.memoryUsage.push({
description,
memoryDelta,
totalMemory: endMemory,
timestamp: new Date().toISOString()
});
console.log(`${description}: ${executionTime.toFixed(2)}ms, Memory: ${memoryDelta}KB`);
return result;
} catch (error) {
console.error(`${description} failed:`, error);
throw error;
}
}
getMemoryUsage() {
// Approximation - actual memory usage varies
return performance.memory?.usedJSHeapSize / 1024 || 0;
}
getReport() {
return {
averageExecutionTime: this.metrics.executionTimes.reduce((sum, entry) =>
sum + entry.time, 0) / this.metrics.executionTimes.length,
totalExecutions: this.metrics.executionTimes.length,
memoryMetrics: this.metrics.memoryUsage,
packageMetrics: this.metrics.packageLoadTimes
};
}
}
// Usage
const monitor = new PyodideMonitor();
await monitor.timeExecution(`
import numpy as np
arr = np.random.random((1000, 1000))
result = np.sum(arr)
result
`, 'NumPy Random Sum');
await monitor.timeExecution(`
import pandas as pd
df = pd.DataFrame({'A': range(10000), 'B': range(10000, 20000)})
result = df.groupby('A').sum()
len(result)
`, 'Pandas GroupBy');
console.log('Performance Report:', monitor.getReport());Enable or disable debug mode for enhanced error messages at a performance cost.
function setDebug(debug: boolean): boolean;Parameters:
debug - If true, enable debug mode; if false, disable debug modeReturns: The previous value of the debug flag
Usage Example:
// Enable debug mode for development
const previousDebugState = pyodide.setDebug(true);
console.log('Debug mode enabled, previous state was:', previousDebugState);
try {
// Your code that might produce complex errors
pyodide.runPython('some_complex_python_code()');
} catch (error) {
console.error('Enhanced error info:', error);
} finally {
// Restore previous debug state
pyodide.setDebug(previousDebugState);
}Access the complete lockfile used to load the current Pyodide instance, containing package metadata and dependency information.
const lockfile: Lockfile;Usage Example:
// Access lockfile information
const lockfile = pyodide.lockfile;
console.log('Pyodide version:', lockfile.info.version);
console.log('Python version:', lockfile.info.python);
console.log('Available packages:', Object.keys(lockfile.packages));
// Get information about a specific package
const numpyInfo = lockfile.packages['numpy'];
if (numpyInfo) {
console.log('NumPy version:', numpyInfo.version);
console.log('NumPy dependencies:', numpyInfo.depends);
}Get the base URL used for resolving relative package paths in the lockfile.
const lockfileBaseUrl: string | undefined;Usage Example:
const baseUrl = pyodide.lockfileBaseUrl;
console.log('Package base URL:', baseUrl);
// This is useful for understanding where packages are loaded from
if (baseUrl) {
console.log('Packages are loaded from:', baseUrl);
} else {
console.log('Using default package locations');
}Install with Tessl CLI
npx tessl i tessl/npm-pyodide