Skip to content

Commit 82e2257

Browse files
authored
Update Python Worker docs to use default entrypoint. (#24392)
1 parent 3ac69c4 commit 82e2257

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1079
-935
lines changed

src/content/changelog/workers/2025-05-14-python-worker-durable-object.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ long-running applications which run close to your users. For more info see
1818
You can define a Durable Object in Python in a similar way to JavaScript:
1919

2020
```python
21-
from workers import DurableObject, Response, handler
21+
from workers import DurableObject, Response, WorkerEntrypoint
2222

2323
from urllib.parse import urlparse
2424

@@ -27,17 +27,17 @@ class MyDurableObject(DurableObject):
2727
self.ctx = ctx
2828
self.env = env
2929

30-
def on_fetch(self, request):
30+
def fetch(self, request):
3131
result = self.ctx.storage.sql.exec("SELECT 'Hello, World!' as greeting").one()
3232
return Response(result.greeting)
3333

34-
@handler
35-
async def on_fetch(request, env, ctx):
36-
url = urlparse(request.url)
37-
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
38-
stub = env.MY_DURABLE_OBJECT.get(id)
39-
greeting = await stub.fetch(request.url)
40-
return greeting
34+
class Default(WorkerEntrypoint):
35+
async def fetch(self, request, env, ctx):
36+
url = urlparse(request.url)
37+
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
38+
stub = env.MY_DURABLE_OBJECT.get(id)
39+
greeting = await stub.fetch(request.url)
40+
return greeting
4141
```
4242

4343
Define the Durable Object in your Wrangler configuration file:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Python Workers handlers now live in an entrypoint class
3+
description: We are changing how Python Workers are structured by default.
4+
products:
5+
- workers
6+
date: 2025-08-14
7+
---
8+
9+
import { WranglerConfig } from "~/components";
10+
11+
We are changing how Python Workers are structured by default. Previously, handlers were defined at the top-level of a module as `on_fetch`, `on_scheduled`, etc. methods, but now they live in an entrypoint class.
12+
13+
Here's an example of how to now define a Worker with a fetch handler:
14+
15+
```python
16+
from workers import Response, WorkerEntrypoint
17+
18+
class Default(WorkerEntrypoint):
19+
async def fetch(self, request, env, ctx):
20+
return Response("Hello World!")
21+
```
22+
23+
To keep using the old-style handlers, you can specify the `disable_python_no_global_handlers` compatibility flag in your wrangler file:
24+
25+
<WranglerConfig>
26+
27+
```toml
28+
compatibility_flags = [ "disable_python_no_global_handlers" ]
29+
```
30+
31+
</WranglerConfig>
32+
33+
Consult the [Python Workers documentation](/workers/languages/python/) for more details.

src/content/docs/d1/examples/query-d1-from-python-workers.mdx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,16 @@ The value of `binding` is how you will refer to your database from within your W
8181
To create a Python Worker, create an empty file at `src/entry.py`, matching the value of `main` in your Wrangler file with the contents below:
8282

8383
```python
84-
from workers import Response
84+
from workers import Response, WorkerEntrypoint
8585

86-
async def on_fetch(request, env):
87-
# Do anything else you'd like on request here!
86+
class Default(WorkerEntrypoint):
87+
async def fetch(self, request, env):
88+
# Do anything else you'd like on request here!
8889

89-
# Query D1 - we'll list all tables in our database in this example
90-
results = await env.DB.prepare("PRAGMA table_list").run()
91-
# Return a JSON response
92-
return Response.json(results)
90+
# Query D1 - we'll list all tables in our database in this example
91+
results = await env.DB.prepare("PRAGMA table_list").run()
92+
# Return a JSON response
93+
return Response.json(results)
9394

9495
```
9596

src/content/docs/durable-objects/get-started.mdx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,16 @@ export default {
185185
<TabItem label="Python" icon="seti:python">
186186

187187
```python
188-
from workers import handler, Response
188+
from workers import handler, Response, WorkerEntrypoint
189189
from urllib.parse import urlparse
190190

191-
@handler
192-
async def on_fetch(request, env, ctx):
193-
url = urlparse(request.url)
194-
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
195-
stub = env.MY_DURABLE_OBJECT.get(id)
196-
greeting = await stub.say_hello()
197-
return Response(greeting)
191+
class Default(WorkerEntrypoint):
192+
async def fetch(request, env, ctx):
193+
url = urlparse(request.url)
194+
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
195+
stub = env.MY_DURABLE_OBJECT.get(id)
196+
greeting = await stub.say_hello()
197+
return Response(greeting)
198198
```
199199

200200
</TabItem>
@@ -328,13 +328,13 @@ class MyDurableObject(DurableObject):
328328

329329
return result.greeting
330330

331-
@handler
332-
async def on_fetch(request, env, ctx):
333-
url = urlparse(request.url)
334-
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
335-
stub = env.MY_DURABLE_OBJECT.get(id)
336-
greeting = await stub.say_hello()
337-
return Response(greeting)
331+
class Default(WorkerEntrypoint):
332+
async def fetch(self, request, env, ctx):
333+
url = urlparse(request.url)
334+
id = env.MY_DURABLE_OBJECT.idFromName(url.path)
335+
stub = env.MY_DURABLE_OBJECT.get(id)
336+
greeting = await stub.say_hello()
337+
return Response(greeting)
338338
```
339339

340340
</TabItem>

src/content/docs/workers/examples/103-early-hints.mdx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export default {
116116

117117
```py
118118
import re
119-
from workers import Response
119+
from workers import Response, WorkerEntrypoint
120120

121121
CSS = "body { color: red; }"
122122
HTML = """
@@ -132,12 +132,14 @@ HTML = """
132132
</body>
133133
</html>
134134
"""
135-
def on_fetch(request):
136-
if re.search("test.css", request.url):
137-
headers = {"content-type": "text/css"}
138-
return Response(CSS, headers=headers)
139-
else:
140-
headers = {"content-type": "text/html","link": "</test.css>; rel=preload; as=style"}
135+
136+
class Default(WorkerEntrypoint):
137+
async def fetch(self, request):
138+
if re.search("test.css", request.url):
139+
headers = {"content-type": "text/css"}
140+
return Response(CSS, headers=headers)
141+
else:
142+
headers = {"content-type": "text/html","link": "</test.css>; rel=preload; as=style"}
141143
return Response(HTML, headers=headers)
142144
```
143145

src/content/docs/workers/examples/ab-testing.mdx

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -100,41 +100,42 @@ export default {
100100
```py
101101
import random
102102
from urllib.parse import urlparse, urlunparse
103-
from workers import Response, fetch
103+
from workers import Response, fetch, WorkerEntrypoint
104104

105105
NAME = "myExampleWorkersABTest"
106106

107-
async def on_fetch(request):
108-
url = urlparse(request.url)
109-
# Uncomment below when testing locally
110-
# url = url._replace(netloc="example.com") if "localhost" in url.netloc else url
111-
112-
# Enable Passthrough to allow direct access to control and test routes.
113-
if url.path.startswith("/control") or url.path.startswith("/test"):
114-
return fetch(urlunparse(url))
115-
116-
# Determine which group this requester is in.
117-
cookie = request.headers.get("cookie")
118-
119-
if cookie and f'{NAME}=control' in cookie:
120-
url = url._replace(path="/control" + url.path)
121-
elif cookie and f'{NAME}=test' in cookie:
122-
url = url._replace(path="/test" + url.path)
123-
else:
124-
# If there is no cookie, this is a new client. Choose a group and set the cookie.
125-
group = "test" if random.random() < 0.5 else "control"
126-
if group == "control":
127-
url = url._replace(path="/control" + url.path)
128-
else:
129-
url = url._replace(path="/test" + url.path)
130-
131-
# Reconstruct response to avoid immutability
132-
res = await fetch(urlunparse(url))
133-
headers = dict(res.headers)
134-
headers["Set-Cookie"] = f'{NAME}={group}; path=/'
135-
return Response(res.body, headers=headers)
136-
137-
return fetch(urlunparse(url))
107+
class Default(WorkerEntrypoint):
108+
async def fetch(self, request):
109+
url = urlparse(request.url)
110+
# Uncomment below when testing locally
111+
# url = url._replace(netloc="example.com") if "localhost" in url.netloc else url
112+
113+
# Enable Passthrough to allow direct access to control and test routes.
114+
if url.path.startswith("/control") or url.path.startswith("/test"):
115+
return fetch(urlunparse(url))
116+
117+
# Determine which group this requester is in.
118+
cookie = request.headers.get("cookie")
119+
120+
if cookie and f'{NAME}=control' in cookie:
121+
url = url._replace(path="/control" + url.path)
122+
elif cookie and f'{NAME}=test' in cookie:
123+
url = url._replace(path="/test" + url.path)
124+
else:
125+
# If there is no cookie, this is a new client. Choose a group and set the cookie.
126+
group = "test" if random.random() < 0.5 else "control"
127+
if group == "control":
128+
url = url._replace(path="/control" + url.path)
129+
else:
130+
url = url._replace(path="/test" + url.path)
131+
132+
# Reconstruct response to avoid immutability
133+
res = await fetch(urlunparse(url))
134+
headers = dict(res.headers)
135+
headers["Set-Cookie"] = f'{NAME}={group}; path=/'
136+
return Response(res.body, headers=headers)
137+
138+
return fetch(urlunparse(url))
138139
```
139140

140141
</TabItem> <TabItem label="Hono" icon="seti:typescript">

src/content/docs/workers/examples/accessing-the-cloudflare-object.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,15 @@ export default app;
9090

9191
```py
9292
import json
93-
from workers import Response
93+
from workers import Response, WorkerEntrypoint
9494
from js import JSON
9595

96-
def on_fetch(request):
97-
error = json.dumps({ "error": "The `cf` object is not available inside the preview." })
98-
data = request.cf if request.cf is not None else error
99-
headers = {"content-type":"application/json"}
100-
return Response(JSON.stringify(data, None, 2), headers=headers)
96+
class Default(WorkerEntrypoint):
97+
async def fetch(self, request):
98+
error = json.dumps({ "error": "The `cf` object is not available inside the preview." })
99+
data = request.cf if request.cf is not None else error
100+
headers = {"content-type":"application/json"}
101+
return Response(JSON.stringify(data, None, 2), headers=headers)
101102
```
102103

103104
</TabItem> </Tabs>

src/content/docs/workers/examples/aggregate-requests.mdx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,22 @@ export default app;
9393
</TabItem> <TabItem label="Python" icon="seti:python">
9494

9595
```py
96-
from workers import Response, fetch
96+
from workers import Response, fetch, WorkerEntrypoint
9797
import asyncio
9898
import json
9999

100-
async def on_fetch(request):
101-
# some_host is set up to return JSON responses
102-
some_host = "https://jsonplaceholder.typicode.com"
103-
url1 = some_host + "/todos/1"
104-
url2 = some_host + "/todos/2"
100+
class Default(WorkerEntrypoint):
101+
async def fetch(self, request):
102+
# some_host is set up to return JSON responses
103+
some_host = "https://jsonplaceholder.typicode.com"
104+
url1 = some_host + "/todos/1"
105+
url2 = some_host + "/todos/2"
105106

106-
responses = await asyncio.gather(fetch(url1), fetch(url2))
107-
results = await asyncio.gather(*(r.json() for r in responses))
107+
responses = await asyncio.gather(fetch(url1), fetch(url2))
108+
results = await asyncio.gather(*(r.json() for r in responses))
108109

109-
headers = {"content-type": "application/json;charset=UTF-8"}
110-
return Response.json(results, headers=headers)
110+
headers = {"content-type": "application/json;charset=UTF-8"}
111+
return Response.json(results, headers=headers)
111112
```
112113

113114
</TabItem> </Tabs>

src/content/docs/workers/examples/alter-headers.mdx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,27 +85,28 @@ export default {
8585
</TabItem> <TabItem label="Python" icon="seti:python">
8686

8787
```py
88-
from workers import Response, fetch
88+
from workers import Response, fetch, WorkerEntrypoint
8989

90-
async def on_fetch(request):
91-
response = await fetch("https://example.com")
90+
class Default(WorkerEntrypoint):
91+
async def fetch(self, request):
92+
response = await fetch("https://example.com")
9293

93-
# Grab the response headers so they can be modified
94-
new_headers = response.headers
94+
# Grab the response headers so they can be modified
95+
new_headers = response.headers
9596

96-
# Add a custom header with a value
97-
new_headers["x-workers-hello"] = "Hello from Cloudflare Workers"
97+
# Add a custom header with a value
98+
new_headers["x-workers-hello"] = "Hello from Cloudflare Workers"
9899

99-
# Delete headers
100-
if "x-header-to-delete" in new_headers:
101-
del new_headers["x-header-to-delete"]
102-
if "x-header2-to-delete" in new_headers:
103-
del new_headers["x-header2-to-delete"]
100+
# Delete headers
101+
if "x-header-to-delete" in new_headers:
102+
del new_headers["x-header-to-delete"]
103+
if "x-header2-to-delete" in new_headers:
104+
del new_headers["x-header2-to-delete"]
104105

105-
# Adjust the value for an existing header
106-
new_headers["x-header-to-change"] = "NewValue"
106+
# Adjust the value for an existing header
107+
new_headers["x-header-to-change"] = "NewValue"
107108

108-
return Response(response.body, headers=new_headers)
109+
return Response(response.body, headers=new_headers)
109110
```
110111

111112
</TabItem> <TabItem label="Hono" icon="seti:typescript">

src/content/docs/workers/examples/auth-with-headers.mdx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,21 @@ export default {
7878
</TabItem> <TabItem label="Python" icon="seti:python">
7979

8080
```py
81-
from workers import Response, fetch
81+
from workers import WorkerEntrypoint, Response, fetch
8282

83-
async def on_fetch(request):
84-
PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"
85-
PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"
83+
class Default(WorkerEntrypoint):
84+
async def fetch(self, request):
85+
PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"
86+
PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"
8687

87-
psk = request.headers[PRESHARED_AUTH_HEADER_KEY]
88+
psk = request.headers[PRESHARED_AUTH_HEADER_KEY]
8889

89-
if psk == PRESHARED_AUTH_HEADER_VALUE:
90-
# Correct preshared header key supplied. Fetch request from origin.
91-
return fetch(request)
90+
if psk == PRESHARED_AUTH_HEADER_VALUE:
91+
# Correct preshared header key supplied. Fetch request from origin.
92+
return fetch(request)
9293

93-
# Incorrect key supplied. Reject the request.
94-
return Response("Sorry, you have supplied an invalid key.", status=403)
94+
# Incorrect key supplied. Reject the request.
95+
return Response("Sorry, you have supplied an invalid key.", status=403)
9596
```
9697

9798
</TabItem> <TabItem label="Hono" icon="seti:typescript">

0 commit comments

Comments
 (0)