1---
2title: Python Workers
3description: Deploy Cloudflare Python Workers by pointing main at a .py file — alchemy uploads the modules, vendors pyproject.toml dependencies with uv, and serves them locally through workerd's built-in Pyodide.
4---
5
6Cloudflare [Python Workers](https://developers.cloudflare.com/workers/languages/python/)
7(open beta) run CPython inside the Workers runtime via Pyodide. In
8alchemy, a Python Worker is a regular `Cloudflare.Worker` whose `main`
9points at a `.py` file. The infrastructure is still defined in
10TypeScript. Only the runtime code is Python.
11
12There is no bundling step: the entry and every sibling `.py` module
13upload as-is, the `python_workers` compatibility flag is added
14automatically, and dependencies are vendored from `pyproject.toml`
15with [uv](https://docs.astral.sh/uv/).
16
17:::note
18Python Workers are always **async-style** Workers: they take no inline
19Effect implementation (the Effect runtime is a JavaScript bundle and
20cannot run inside Pyodide). Declare bindings with the `env` prop and
21read them from `self.env` in Python.
22:::
23
24## Deploy a Python Worker
25
26Point `main` at the Python entry and declare bindings with `env`:
27
28```typescript
29// alchemy.run.ts
30import * as Alchemy from "alchemy";
31import * as Cloudflare from "alchemy/Cloudflare";
32import * as Effect from "effect/Effect";
33
34export default Alchemy.Stack(
35 "MyPythonApp",
36 {
37 providers: Cloudflare.providers(),
38 state: Cloudflare.state(),
39 },
40 Effect.gen(function* () {
41 const cache = yield* Cloudflare.KV.Namespace("Cache");
42
43 const worker = yield* Cloudflare.Worker("Worker", {
44 main: "./src/worker.py",
45 env: {
46 CACHE: cache,
47 GREETING: "Hello from Python!",
48 },
49 });
50
51 return { url: worker.url.as<string>() };
52 }),
53);
54```
55
56The handler lives in the entry module as a class extending
57`WorkerEntrypoint`. Bindings and env vars appear on `self.env` under
58the names given in the `env` prop:
59
60```python
61# src/worker.py
62from workers import Response, WorkerEntrypoint
63
64
65class Default(WorkerEntrypoint):
66 async def fetch(self, request):
67 cached = await self.env.CACHE.get("greeting")
68 return Response(cached or self.env.GREETING)
69```
70
71Sibling `.py` files next to the entry upload alongside it and are
72importable by their relative path:
73
74```python
75# src/util.py
76def slugify(value: str) -> str:
77 return value.lower().replace(" ", "-")
78```
79
80```python
81# src/worker.py
82from util import slugify
83```
84
85## Dependencies with pyproject.toml
86
87Put a `pyproject.toml` next to the entry module. On deploy, alchemy
88vendors `[project.dependencies]` with uv and uploads them under
89`python_modules/`, the same layout Wrangler and
90[pywrangler](https://github.com/cloudflare/workers-py) use:
91
92```toml
93# src/pyproject.toml
94[project]
95name = "my-worker"
96version = "0.1.0"
97requires-python = ">=3.13"
98dependencies = [
99 "fastapi",
100 "httpx",
101 "humanize",
102]
103```
104
105```python
106# src/worker.py
107import humanize
108from workers import Response, WorkerEntrypoint
109
110
111class Default(WorkerEntrypoint):
112 async def fetch(self, request):
113 return Response(f"{humanize.intcomma(1234567)} requests served")
114```
115
116Under the hood, alchemy runs the same steps `pywrangler sync` does:
117
1181. `uv pip compile` resolves the dependencies against the
119 [Pyodide wheel index](https://index.pyodide.org) for the runtime's
120 Python version into a `pylock.toml` (prebuilt wheels only, since
121 Pyodide-platformed wheels can't be built locally).
1222. `uv venv` + `uv pip install --no-build` install the locked wheels
123 into an `emscripten-wasm32` cross-venv.
1243. The venv's `site-packages` becomes the uploaded `python_modules/`
125 directory.
126
127The managed `workers-runtime-sdk` package (the `workers` Python
128module) is always vendored alongside your own dependencies. Results
129are cached under `.alchemy/python/` and only re-vendored when
130`pyproject.toml` changes.
131
132:::caution
133[uv](https://docs.astral.sh/uv/getting-started/installation/) (>= 0.8.10)
134must be installed for dependency vendoring. Workers without a
135`pyproject.toml` deploy without uv. Only pure-Python packages,
136packages with `emscripten-wasm32` wheels, and
137[packages built into Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)
138resolve, and HTTP clients must be async (`httpx`, `aiohttp`, or JS
139`fetch` via the FFI).
140:::
141
142### ASGI apps (FastAPI)
143
144The runtime ships an `asgi` module that serves any ASGI application,
145so FastAPI (with pydantic validation) works out of the box. Bindings
146and env vars reach your routes through `request.scope["env"]`:
147
148```python
149# src/worker.py
150import asgi
151from fastapi import FastAPI, Request
152from pydantic import BaseModel
153from workers import WorkerEntrypoint
154
155app = FastAPI()
156
157
158class Item(BaseModel):
159 name: str
160 quantity: int
161
162
163@app.get("/")
164async def root():
165 return {"message": "Hello from FastAPI on Workers"}
166
167
168@app.get("/env")
169async def env(request: Request):
170 return {"deployment": request.scope["env"].DEPLOYMENT}
171
172
173@app.post("/items")
174async def create_item(item: Item):
175 return {"name": item.name, "total": item.quantity * 2}
176
177
178class Default(WorkerEntrypoint):
179 async def fetch(self, request):
180 return await asgi.fetch(app, request, self.env)
181```
182
183```toml
184# src/pyproject.toml
185[project]
186name = "my-api"
187version = "0.1.0"
188requires-python = ">=3.13"
189dependencies = ["fastapi"]
190```
191
192Compiled-extension packages with Pyodide wheels (numpy, pydantic-core,
193and the rest of the
194[Pyodide package set](https://pyodide.org/en/stable/usage/packages-in-pyodide.html))
195vendor the emscripten-wasm32 binary wheel matching the runtime's ABI.
196There is no build step on your side.
197
198### Bring your own python_modules
199
200If the entry's directory already contains a `python_modules/`
201directory, produced by `pywrangler sync` or any other tool that
202emits Wrangler's vendored layout, alchemy uploads it byte-for-byte
203and never invokes uv. This is the escape hatch for teams that manage
204Python dependencies outside of alchemy.
205
206## Python version
207
208The runtime's Python version follows Cloudflare's compatibility
209rules, and alchemy targets the matching uv interpreter and wheel
210index automatically:
211
212| Python | Selected when |
213| ------ | ------------- |
214| 3.13 | `compatibility.date >= "2025-09-29"` (alchemy's default date) or the `python_workers_20250116` flag |
215| 3.12 | earlier dates, or the `no_python_workers_20250116` flag |
216
217```typescript
218const worker = yield* Cloudflare.Worker("Worker", {
219 main: "./src/worker.py",
220 // pin Python 3.12
221 compatibility: { flags: ["no_python_workers_20250116"] },
222});
223```
224
225## Local dev
226
227`alchemy dev` serves Python Workers from local
228[workerd](https://github.com/cloudflare/workerd), which embeds Pyodide.
229The same module graph that would upload runs locally, bindings
230included. Edits to any `.py` file under the entry's directory (or to
231`pyproject.toml`, which re-vendors) restart the local Worker.
232
233## Notes
234
235- Python Workers are in **open beta** on Cloudflare's side; the
236 `python_workers` compatibility flag is required and added for you.
237- Handlers are class-based (`class Default(WorkerEntrypoint)`); the
238 legacy top-level `on_fetch` style requires an extra compatibility
239 flag and is not recommended.
240- Cold starts are mitigated server-side: Cloudflare snapshots the
241 Worker's memory after top-level imports at deploy time. There is
242 nothing to configure.
243- Vendored `python_modules/` files count toward the Worker size limit
244 (3 MiB free / 10 MiB paid, gzipped).
245- Durable Objects, Workflows, and the rest of the binding surface are
246 reachable from Python via `self.env`; see Cloudflare's
247 [Python Workers docs](https://developers.cloudflare.com/workers/languages/python/)
248 for the Python-side API.