workerd includes experimental support for running Python code via Pyodide, a Python runtime compiled to WebAssembly. This enables you to write Workers in Python with access to the Workers API and Python packages.
Python support requires the experimental compatibility flag. This feature is not yet stable and may change in future versions.
Architecture
workerd’s Python support consists of several components:
- Pyodide: Python 3.11+ runtime compiled to WebAssembly
- Python stdlib: Standard library bundled as
python_stdlib.zip
- Package system: Support for installing and loading Python packages
- Memory snapshots: Fast startup via pre-initialized runtime snapshots
- Workers API: Python bindings for the Workers platform
Implementation location: src/workerd/api/pyodide/ and src/pyodide/
Basic usage
Configuration
Create a config.capnp file:
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .pythonWorker),
],
sockets = [
( name = "http",
address = "*:8080",
http = (),
service = "main"
),
]
);
const pythonWorker :Workerd.Worker = (
modules = [
(name = "worker.py", pythonModule = embed "worker.py"),
],
compatibilityDate = "2024-01-01",
compatibilityFlags = ["experimental", "python_workers"],
);
Python worker
Create a worker.py file:
from js import Response
from workers import WorkerEntrypoint
class Default(WorkerEntrypoint):
def fetch(self, request):
return Response.new("Hello from Python!")
Running
workerd serve config.capnp
Workers API bindings
Python workers have access to Workers APIs through the workers module:
Request handling
from workers import WorkerEntrypoint
from js import Response, Headers
class Default(WorkerEntrypoint):
def fetch(self, request):
# Access request properties
method = request.method
url = request.url
headers = request.headers
# Create response with headers
response_headers = Headers.new()
response_headers.set("Content-Type", "application/json")
return Response.new(
'{"message": "Hello"}',
headers=response_headers,
status=200
)
Using bindings
class Default(WorkerEntrypoint):
def fetch(self, request):
# Access environment bindings
kv = self.env.MY_KV_NAMESPACE
# Use KV store
value = kv.get("key")
kv.put("key", "value")
return Response.new(f"Stored value: {value}")
Package management
Installing packages
Pyodide includes a subset of the Python Package Index (PyPI). Packages are loaded from a bundle:
import micropip
# Install packages at runtime
await micropip.install("numpy")
import numpy as np
Vendoring packages
For production use, vendor your dependencies:
- Create a
requirements.txt:
numpy==1.24.0
requests==2.31.0
- Bundle packages in your config:
const pythonWorker :Workerd.Worker = (
modules = [
(name = "worker.py", pythonModule = embed "worker.py"),
],
compatibilityDate = "2024-01-01",
compatibilityFlags = ["experimental", "python_workers"],
bindings = [
(name = "requirements", json = "[\"numpy\", \"requests\"]"),
],
);
Memory snapshots
workerd uses memory snapshots to dramatically reduce Python worker startup time:
How snapshots work
- Baseline snapshot: Pyodide runtime initialized with stdlib
- Package snapshot: Baseline + your required packages imported
- Restore: New isolates start from snapshot instead of cold boot
Startup time comparison
| Startup type | Time |
|---|
| Cold start (no snapshot) | ~800ms |
| Baseline snapshot | ~200ms |
| Package snapshot | ~50ms |
Snapshot generation
Snapshots are generated automatically when:
- Building the worker for the first time
- Dependencies change
- Pyodide version updates
Implementation: src/pyodide/internal/snapshot.ts
Supported Python version
workerd uses Pyodide’s Python distribution:
- Python version: 3.11+ (depends on Pyodide release)
- Standard library: Full stdlib included
- C extensions: Limited to packages compiled for Pyodide/WebAssembly
Limitations
Python in workerd has several important limitations due to the WebAssembly sandbox:
No native I/O
# ❌ Does not work - no file system access
with open("file.txt", "r") as f:
data = f.read()
# ✅ Use Workers APIs instead
from js import fetch
response = await fetch("https://example.com/file.txt")
data = await response.text()
No native threads
# ❌ Does not work - no threading support
import threading
t = threading.Thread(target=worker_function)
t.start()
# ✅ Use async/await instead
import asyncio
await asyncio.gather(task1(), task2())
Package compatibility
Only packages compiled for Pyodide/WebAssembly are supported:
- ✅ Pure Python packages (most packages)
- ✅ Pyodide-compiled C extensions (numpy, pandas, etc.)
- ❌ Native extensions requiring system libraries
- ❌ Packages with native dependencies
Advanced features
Top-level entropy management
Pyodide patches random number generation during import to ensure determinism:
# During import: deterministic entropy
import random # Uses patched RNG
# During request: real entropy
class Default(WorkerEntrypoint):
def fetch(self, request):
# Real random values during request handling
value = random.random()
return Response.new(str(value))
Implementation: src/pyodide/internal/topLevelEntropy/
ASGI support
Run ASGI applications (FastAPI, Starlette, etc.):
from fastapi import FastAPI
from workers import WorkerEntrypoint
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello from FastAPI"}
class Default(WorkerEntrypoint):
async def fetch(self, request):
# ASGI adapter handles the request
from asgi import asgi_adapter
return await asgi_adapter(app, request)
Workflows integration
from workers.workflows import Workflow, Step
class MyWorkflow(Workflow):
@step
async def process_data(self, data):
# Process data in a durable workflow step
result = await expensive_operation(data)
return result
@step
async def send_notification(self, result):
# Send notification about result
await notify_user(result)
Minimize cold starts
- Pre-import packages: Import all dependencies at module level
- Use snapshots: Ensure package snapshots are generated
- Minimize dependencies: Only include required packages
Optimize hot paths
# ❌ Slow: Import in hot path
class Default(WorkerEntrypoint):
def fetch(self, request):
import numpy as np # Import on every request
return Response.new("OK")
# ✅ Fast: Import at module level
import numpy as np
class Default(WorkerEntrypoint):
def fetch(self, request):
# numpy already imported
return Response.new("OK")
Memory usage
# Monitor memory usage
import sys
size = sys.getsizeof(large_object)
# Clean up explicitly if needed
del large_object
import gc
gc.collect()
Debugging
Enable tracing
Set environment variable:
PYTHON_TRACE=1 workerd serve config.capnp
Inspect errors
import traceback
class Default(WorkerEntrypoint):
def fetch(self, request):
try:
result = risky_operation()
except Exception as e:
# Get full traceback
error_msg = traceback.format_exc()
return Response.new(error_msg, status=500)
Examples
See the workerd repository for complete examples:
samples/pyodide/ - Basic Python worker
samples/pyodide-fastapi/ - FastAPI application
samples/pyodide-langchain/ - LangChain integration
Package bundle updates
For information on building and updating Pyodide package bundles, see the pyodide-build-scripts repository.
Reference
- Pyodide documentation: https://pyodide.org/
- Python API location:
src/pyodide/internal/workers-api/
- C++ bindings:
src/workerd/api/pyodide/pyodide.{h,c++}
- TypeScript runtime:
src/pyodide/internal/python.ts