Published: 2025-07-22 15:03:00-06:00

Cloudflare Python Workers (beta): Idiosyncracies I

Background

It's been quite some time since I actually have felt like writing - and there's been tons of change in my life that has me back to the point where I actually have felt like working on my own projects in my free time, not just grabbing spare minutes to lose myself in a book or pick up some retro jrpg and flub along with that.

As I've meandered away from "everything AWS" I started looking at Cloudflare - when I was doing independent consulting, and for a few friends, I'd used it for CDN/DDOS protection and never had any real issues.

For a new project, I decided to try out a combination of Cloudflare Pages and Workers - pages are the equivalent of other PaaS offerings like Vercel, Netlify, or AWS Amplify. Workers are the equivalent of Vercel Functions, Netlify Functions, or AWS Lambdas. I also decided to stick with python as my development language, as I'm used to it being a first class citizen on most platforms.

Disabusing the User of a Silly Notion

Python support on Cloudflare is in beta, but that's not just because it's relatively new - it's because python support is in a Web Assembly (WASM) sandbox that relies on pyodide and there are things that make the experience... special. I'm not used to it taking a week to crank out even moderately complex python functionality, so believe me when I say that this was a challenge, but not an insurmountable one. The issues I ran into were a combination of the WASM sandbox, Cloudflare design choices, and good, old fashioned bugs.

No Cheat Skills

Currently python for workers doesn't support typing, so no quick hints in your IDE of choice. Second, there's no testing harness for them either, so if you prefer to make sure your code works before sticking it out in the wild, you're limited to doing it the old fashioned way. Ruff complains so much that I had disable typechecking for all the python files in the project. Logging is another fun one - most senior software developers will wax philosophical about the fact that one should not "do print debugging" - meaning using print statments to dump out information about the script or program running. This can be handled through the logging configuration, however.

Starting Down the Path

All Cloudflare Workers (workers from here on) have a common entry point. For python workers this is function named on_fetch in a file named entry.py, but what wasn't clear at the time was how this worked in general. When I used the cloudflare-create-cli, it gave me the option to select python as a language, which I did, and used the 'Hello, World' example - and then reading the python worker docs I discovered that the cli had handled most of the configuration needed in wranlger.jsonc: defining the entry file and setting compatability flag for python_workers.

entry.py looks like this after following that path:

from workers import Response, handler

@handler
async def on_fetch(request, env):
    return Response("Hello World!")

At the time, the python docs were missing the wrapper function, and looked like this:

from workers import Response

def on_fetch(request):
    return Response("Hello World!")

This was corrected among the PRs I've submitted related to the experience I had, but I was already a be it leery that what I got out of the box was different than what I could expect from the documentation. And don't worry, no foreshadowing required: it was.

Test Runs With Hurl

I started with running wrangler locally, and I've made the decision to use bun/bunx instead of npm/npmx - so anywhere the docs reference that, I replaced. First things first - local test of the example using hurl!

Start it up:

(project dir)$ bunx wrangler dev
 --cut--
 [wrangler:info] Ready on http://localhost:8787

Check it!

(project dir)$ echo 'GET http://localhost:8787' | hurl
Hello World!

Excellent! I updated it next to follow the Request example and post some simple information. So quick hurl file:

POST http://localhost:8787
{"name": "John"}

Let's give it a whirl!

(project dir)$ hurl test.hurl
Error: PythonError: Traceback (most recent call last):
  File "/session/metadata/entry.py", line 5, in on_fetch
    name = (await request.json()).name
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'name'

    at async Object.fetch ((project dir)/node_modules/miniflare/dist/src/workers/core/entry.worker.js:4345:22)

Ok, so what's different? well in our version we have our @handler wrapper - the example doesn't have that. Does commenting it out get us a functional request?

(project dir)$ $ hurl test.hurl
Hello john!

It works! But dot notation isn't very pythonic, and we know what's wrong when we wrap the handler function so I fixed that up, uncommented the handler, and changed dot notation for the property to be the key for dict.

But why!?

I glossed over at the beginning, but our python runtime is not perfect. The request parameter we're getting is actually a proxy for a JavaScript request object - and this will be important going forward. There are a ton of limitations that I will run into over the course of my week with this project. Some of them are straightforward, some are less so. Almost all are because we're not really using python - we're using metapython, python interpreted within a limited scope inside a WASM sandbox and limited by that sandbox and the fact that Pyodide can't fully replicate python's standard library because of the sandboxing.

Storing Data

Part of the work I needed to do was store simple data, and Cloudflare Workers KV seemed like it would fit the part. I figured I culd use the KV bindings and make things easy on myself. I created a KV collection (named KV_COLLECTION) and added it to my wrangler.jsonc then gave it a try.

Nothing. Nada. Zilch.

KV metrics? Nothing. What in the hell? No writes at all? I tried with a direct wrangler command, because maybe something was wrong with the collection:

(project dir)$ bunx wrangler kv key list --binding KV_COLLECTION
[]
(project dir)$ bunx wrangler kv key put testkey testvalue --binding KV_COLLECTION

 ⛅️ wrangler 4.25.1
───────────────────
Resource location: local
Use --remote if you want to access the remote instance.

Writing the value "testvalue" to key "testkey" on namespace <namespace_id>
(project dir)$ $ bunx wrangler kv key list --binding KV_COLLECTION
[
  {
    "name": "testkey"
  }
]

So no, no issue with the KV... let's go back to the docs - ah, right, the function call env.KV_COLLECTION.put("name", name) I have in my code is actually a javascript promise, which in turn becomes a python coroutine - both of which need to be awaited. Fixing that I get reads and writes as expected.

Part 1 Wrap-up

At just a few hundred words, this first entry doesn't encompass the headdesking frustration, the dozens of tabs, the forgetting async basics, and general feeling of impotence I felt at times. It doesn't encompass my first PR trying to address things that didn't seem "pythonic" or the digging in to the pyodide docs and source to try and understand what limitations were pyodide, or if there were others implemented by the Cloudflare team. Next up I'll set up and initial routing in my worker!