Skip to main content
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:
  1. Create a requirements.txt:
numpy==1.24.0
requests==2.31.0
  1. 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

  1. Baseline snapshot: Pyodide runtime initialized with stdlib
  2. Package snapshot: Baseline + your required packages imported
  3. Restore: New isolates start from snapshot instead of cold boot

Startup time comparison

Startup typeTime
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)

Performance tuning

Minimize cold starts

  1. Pre-import packages: Import all dependencies at module level
  2. Use snapshots: Ensure package snapshots are generated
  3. 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

Build docs developers (and LLMs) love