<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Serverless on zharif.my</title>
        <link>https://zharif.my/tags/serverless/</link>
        <description>Recent content in Serverless on zharif.my</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Thu, 15 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zharif.my/tags/serverless/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Python Workers on Cloudflare</title>
        <link>https://zharif.my/posts/cloudflare-workers-python/</link>
        <pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
        
        <guid>https://zharif.my/posts/cloudflare-workers-python/</guid>
        <description>&lt;img src="https://images.unsplash.com/photo-1526379095098-d400fd0bf935?w=800&amp;h=400&amp;fit=crop" alt="Featured image of post Python Workers on Cloudflare" /&gt;&lt;h2 id=&#34;the-critical-distinction&#34;&gt;The Critical Distinction
&lt;/h2&gt;&lt;p&gt;Cloudflare announced Python support. You think: &amp;ldquo;Finally, real Python on serverless!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Reality check: &lt;strong&gt;Pyodide ≠ CPython&lt;/strong&gt;. It&amp;rsquo;s CPython compiled to WebAssembly. The implications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No OS calls, no &lt;code&gt;os&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;pip install&lt;/code&gt; — packages must be pre-bundled&lt;/li&gt;
&lt;li&gt;Single-threaded execution&lt;/li&gt;
&lt;li&gt;HTTP via JavaScript &lt;code&gt;fetch&lt;/code&gt;, not &lt;code&gt;requests&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&amp;rsquo;t a problem — it&amp;rsquo;s just different. Understanding the constraints makes the difference between &amp;ldquo;why doesn&amp;rsquo;t this work&amp;rdquo; and &amp;ldquo;I know exactly what to use.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My use case&lt;/strong&gt;: Terraform Registry (~2K requests/day) + APT Repository (~500 requests/day). Both run on free tier.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pyodide bundles a subset of Python&amp;rsquo;s standard library and about 150+ packages (numpy, pandas, etc.). But arbitrary PyPI packages won&amp;rsquo;t work.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;pyodide-architecture&#34;&gt;Pyodide Architecture
&lt;/h2&gt;&lt;p&gt;The implications:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No OS&lt;/strong&gt; — there&amp;rsquo;s no Linux, no system calls, no &lt;code&gt;os&lt;/code&gt; module as you&amp;rsquo;d expect&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No pip&lt;/strong&gt; — you can&amp;rsquo;t &lt;code&gt;pip install requests&lt;/code&gt;. Packages must be pre-bundled&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No threads&lt;/strong&gt; — single-threaded execution model&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited stdlib&lt;/strong&gt; — not everything is compiled to WASM&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;the-template&#34;&gt;The Template
&lt;/h2&gt;&lt;p&gt;I built a template that handles the boilerplate. Here&amp;rsquo;s how to start:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Clone the template
git clone https://github.com/cloudflare/worker-python-template.git
cd worker-python-template

# Install dependencies
npm install

# Run locally
npx wrangler dev&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;project-structure&#34;&gt;Project Structure
&lt;/h2&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34;&gt;cloudflare-worker-python-template/
├── src/
│   └── entry.py          # Your worker code
├── tests/
│   └── test_worker.py    # Unit tests
├── wrangler.toml         # Worker configuration
├── pyproject.toml        # Python tooling
├── requirements.txt      # Pyodide packages
├── .github/
│   └── workflows/
│       └── deploy.yml    # CI/CD pipeline
└── README.md&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;the-entry-point&#34;&gt;The Entry Point
&lt;/h2&gt;&lt;p&gt;Every Worker needs an entry point. In Python Workers, it&amp;rsquo;s &lt;code&gt;on_fetch&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;# src/entry.py (from actual template)
import json
from urllib.parse import urlparse

async def on_fetch(request, env):
    &amp;#34;&amp;#34;&amp;#34;Handle incoming requests.&amp;#34;&amp;#34;&amp;#34;
    
    # Parse the URL path
    path = urlparse(request.url).path
    
    # Route to handlers
    if path == &amp;#34;/&amp;#34;:
        return Response.new(&amp;#34;Hello from Python Workers!&amp;#34;)
    
    elif path == &amp;#34;/health&amp;#34;:
        return Response.new(
            json.dumps({&amp;#34;status&amp;#34;: &amp;#34;ok&amp;#34;}),
            headers={&amp;#34;Content-Type&amp;#34;: &amp;#34;application/json&amp;#34;}
        )
    
    # 404 for everything else
    return Response.new(&amp;#34;Not Found&amp;#34;, status=404)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Simple, familiar, Pythonic.&lt;/p&gt;
&lt;h2 id=&#34;accessing-environment-variables&#34;&gt;Accessing Environment Variables
&lt;/h2&gt;&lt;p&gt;Just like in Node.js Workers, you access secrets and environment variables via the &lt;code&gt;env&lt;/code&gt; object:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;# src/entry.py
async def on_fetch(request, env):
    # String variables from wrangler.toml [vars]
    debug_mode = env.get(&amp;#34;DEBUG&amp;#34;, &amp;#34;false&amp;#34;)
    
    # Secrets (set via: npx wrangler secret put API_KEY)
    api_key = env.API_KEY
    
    # Use them
    if debug_mode == &amp;#34;true&amp;#34;:
        console.log(f&amp;#34;API Key loaded: {api_key[:4]}...&amp;#34;)
    
    return Response.new(f&amp;#34;API Key: {api_key[:4]}***&amp;#34;)&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;working-with-javascript-apis&#34;&gt;Working with JavaScript APIs
&lt;/h2&gt;&lt;p&gt;This is where Pyodide gets interesting. You can import JavaScript objects directly into Python:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;from js import console, fetch, Response, URL

# Use browser/Workers APIs
async def on_fetch(request, env):
    # fetch is available directly
    resp = await fetch(&amp;#34;https://api.github.com/users/your-username&amp;#34;)
    data = await resp.text()
    
    return Response.new(data, headers={&amp;#34;Content-Type&amp;#34;: &amp;#34;application/json&amp;#34;})&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;js&lt;/code&gt; module exposes global JavaScript objects. This is how you do HTTP requests, interact with the Cache API, use WebCrypto, etc.&lt;/p&gt;
&lt;h2 id=&#34;understanding-the-constraints&#34;&gt;Understanding the Constraints
&lt;/h2&gt;&lt;p&gt;This is critical. Python Workers aren&amp;rsquo;t Node.js Workers:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Aspect&lt;/th&gt;
          &lt;th&gt;Python Workers&lt;/th&gt;
          &lt;th&gt;Node.js Workers&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Package Manager&lt;/td&gt;
          &lt;td&gt;Pyodide bundles only&lt;/td&gt;
          &lt;td&gt;npm (everything)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Cold Start&lt;/td&gt;
          &lt;td&gt;~5-10ms&lt;/td&gt;
          &lt;td&gt;~1ms&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory&lt;/td&gt;
          &lt;td&gt;128 MB&lt;/td&gt;
          &lt;td&gt;128 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;CPU Time (Free)&lt;/td&gt;
          &lt;td&gt;10ms&lt;/td&gt;
          &lt;td&gt;10ms&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;CPU Time (Paid)&lt;/td&gt;
          &lt;td&gt;30s&lt;/td&gt;
          &lt;td&gt;50ms-30s&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Filesystem&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
          &lt;td&gt;None&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;available-packages&#34;&gt;Available Packages
&lt;/h3&gt;&lt;p&gt;Pyodide includes ~150+ packages out of the box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Standard Library&lt;/strong&gt;: &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;re&lt;/code&gt;, &lt;code&gt;urllib&lt;/code&gt;, &lt;code&gt;hashlib&lt;/code&gt;, &lt;code&gt;base64&lt;/code&gt;, &lt;code&gt;datetime&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;: &lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;pandas&lt;/code&gt;, &lt;code&gt;scipy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web&lt;/strong&gt;: (limited — use &lt;code&gt;fetch&lt;/code&gt; from JS instead of &lt;code&gt;requests&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;# This works
import json
import re
import hashlib
from urllib.parse import urlparse

# This does NOT work (not bundled)
# import requests  # ❌
# import httpx      # ❌
# import cryptography  # ❌&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For HTTP, use the JavaScript &lt;code&gt;fetch&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;from js import fetch

async def call_api(url):
    resp = await fetch(url)
    return await resp.json()&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;working-around-missing-packages&#34;&gt;Working Around Missing Packages
&lt;/h3&gt;&lt;p&gt;For things like cryptographic operations, use WebCrypto via JavaScript:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;from js import crypto, TextEncoder

async def hash_sha256(data: str) -&amp;gt; str:
    &amp;#34;&amp;#34;&amp;#34;Hash data using WebCrypto.&amp;#34;&amp;#34;&amp;#34;
    encoder = TextEncoder.new()
    encoded = encoder.encode(data)
    hash_buffer = await crypto.subtle.digest(&amp;#34;SHA-256&amp;#34;, encoded)
    return bytes(hash_buffer).hex()&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;configuration-wranglertoml&#34;&gt;Configuration (wrangler.toml)
&lt;/h2&gt;&lt;p&gt;The worker configuration lives in &lt;code&gt;wrangler.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-toml&#34;&gt;name = &amp;#34;my-worker&amp;#34;
main = &amp;#34;src/entry.py&amp;#34;
compatibility_date = &amp;#34;2026-04-25&amp;#34;

# Environment variables (non-sensitive)
[vars]
ENVIRONMENT = &amp;#34;production&amp;#34;
DEBUG = &amp;#34;false&amp;#34;

# KV Namespace for key-value storage
[[kv_namespaces]]
binding = &amp;#34;CACHE&amp;#34;
id = &amp;#34;abc123def456&amp;#34;

# D1 Database for SQL
[[d1_databases]]
binding = &amp;#34;DB&amp;#34;
database_name = &amp;#34;my-db&amp;#34;
database_id = &amp;#34;def456abc789&amp;#34;

# R2 Bucket for object storage
[[r2_buckets]]
binding = &amp;#34;ASSETS&amp;#34;
bucket_name = &amp;#34;my-assets&amp;#34;

# Deploy to specific environment
[env.staging]
name = &amp;#34;my-worker-staging&amp;#34;

[env.staging.vars]
ENVIRONMENT = &amp;#34;staging&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;development-workflow&#34;&gt;Development Workflow
&lt;/h2&gt;&lt;h3 id=&#34;local-development&#34;&gt;Local Development
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Start the dev server
npx wrangler dev

# Test with curl
curl http://localhost:8787/
# {&amp;#34;status&amp;#34;: &amp;#34;ok&amp;#34;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The dev server reloads on file changes. It&amp;rsquo;s fast and works well.&lt;/p&gt;
&lt;h3 id=&#34;testing&#34;&gt;Testing
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34;&gt;# tests/test_worker.py (pytest)
import pytest
from src.entry import on_fetch

class MockEnv:
    DEBUG = &amp;#34;false&amp;#34;
    API_KEY = &amp;#34;test-key&amp;#34;

class MockRequest:
    def __init__(self, url):
        self.url = url

def test_health_endpoint():
    request = MockRequest(&amp;#34;http://localhost/health&amp;#34;)
    response = on_fetch(request, MockEnv())
    
    assert response.status == 200

def test_root_endpoint():
    request = MockRequest(&amp;#34;http://localhost/&amp;#34;)
    response = on_fetch(request, MockEnv())
    
    assert response.status == 200
    assert &amp;#34;Hello&amp;#34; in response.body&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Run tests:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;pip install pytest
pytest -v&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;linting&#34;&gt;Linting
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;pip install ruff
ruff check src/ tests/
ruff format src/ tests/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The template includes CI that runs both tests and linting.&lt;/p&gt;
&lt;h2 id=&#34;cicd-pipeline&#34;&gt;CI/CD Pipeline
&lt;/h2&gt;&lt;p&gt;The included GitHub Actions workflow:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: &amp;#39;3.12&amp;#39;
      
      - name: Lint
        run: |
          pip install ruff
          ruff check src/ tests/
      
      - name: Test
        run: |
          pip install pytest
          pytest -v
      
      - name: Deploy
        uses: cloudflare/wrangler-action@v3
        with:
          api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;on-prem-deployment-with-workerd&#34;&gt;On-Prem Deployment with workerd
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where this gets powerful: you can run the same Python Worker locally using &lt;a class=&#34;link&#34; href=&#34;https://github.com/cloudflare/workerd&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;workerd&lt;/a&gt;, Cloudflare&amp;rsquo;s open-source Workers runtime.&lt;/p&gt;
&lt;h3 id=&#34;why-run-locally&#34;&gt;Why Run Locally?
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Development&lt;/strong&gt; — faster iteration than deploy-then-test&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt; — consistent environment for integration tests&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Air-gapped&lt;/strong&gt; — run in environments without internet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy&lt;/strong&gt; — keep traffic local for sensitive workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;docker-setup&#34;&gt;Docker Setup
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34;&gt;# docker-compose.yml
services:
  worker:
    image: cloudflare/workerd:latest
    ports:
      - &amp;#34;8787:8787&amp;#34;
    volumes:
      - ./config.workerd:/etc/workerd/config.capnp:ro
    cap_add:
      - SYS_ADMIN&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;the-config&#34;&gt;The Config
&lt;/h3&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-javascript&#34;&gt;// config.workerd (JavaScript format, not TOML)
export default {
  services: [
    {
      name: &amp;#34;my-worker&amp;#34;,
      script: readFile(&amp;#34;dist/worker.mjs&amp;#34;),
      bindings: {
        ENVIRONMENT: &amp;#34;development&amp;#34;,
        API_KEY: &amp;#34;dev-key&amp;#34;,
      }
    }
  ],
  sockets: [
    {
      address: &amp;#34;0.0.0.0:8787&amp;#34;,
      http: {
        endpoint: &amp;#34;0.0.0.0:8787&amp;#34;
      }
    }
  ]
};&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;building-for-workerd&#34;&gt;Building for workerd
&lt;/h3&gt;&lt;p&gt;The trick: Wrangler outputs JavaScript, but workerd needs its own format. The template handles this:&lt;/p&gt;
&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34;&gt;# Build for Cloudflare (default)
npx wrangler deploy

# Build for workerd (local)
npm run build:workerd&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This outputs a compatible &lt;code&gt;worker.mjs&lt;/code&gt; for local testing.&lt;/p&gt;
&lt;h2 id=&#34;real-world-usage&#34;&gt;Real-World Usage
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve built several production workers using this template:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;terraform-registry&lt;/strong&gt; — ~2K requests/day, handles module distribution&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;apt-repository&lt;/strong&gt; — ~500 requests/day, serves packages to 10+ machines&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;cloudflare-ddns&lt;/strong&gt; — Updates DNS records based on IP changes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All run on the free tier. All deploy in seconds. All can run locally.&lt;/p&gt;
&lt;h2 id=&#34;what-i-love&#34;&gt;What I Love
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python syntax&lt;/strong&gt; — feels like writing regular Python&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global distribution&lt;/strong&gt; — edge deployment out of the box&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero infra&lt;/strong&gt; — no servers, no scaling concerns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-prem option&lt;/strong&gt; — workerd for local/air-gapped needs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;what-most-people-get-wrong&#34;&gt;What Most People Get Wrong
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Pyodide = CPython&amp;rdquo;&lt;/strong&gt; — No OS, no pip, no threads. Use &lt;code&gt;js&lt;/code&gt; module for HTTP/WebCrypto.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Free tier is unlimited&amp;rdquo;&lt;/strong&gt; — 10ms CPU cap. Complex Python on free tier = timeouts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;Works locally = works on edge&amp;rdquo;&lt;/strong&gt; — Local dev uses Node, edge uses V8. Test with workerd.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;when-to-use-python-workers&#34;&gt;When to Use Python Workers
&lt;/h2&gt;&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Use Python Workers&lt;/th&gt;
          &lt;th&gt;Use Node.js Workers&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Python expertise&lt;/td&gt;
          &lt;td&gt;JavaScript expertise&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Data processing&lt;/td&gt;
          &lt;td&gt;I/O-heavy&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Simple logic&lt;/td&gt;
          &lt;td&gt;Complex async&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;~150 bundled packages needed&lt;/td&gt;
          &lt;td&gt;Full npm ecosystem&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;getting-started&#34;&gt;Getting Started
&lt;/h2&gt;&lt;p&gt;If you want to build your own Python Workers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use the &lt;a class=&#34;link&#34; href=&#34;https://developers.cloudflare.com/workers/languages/python&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Workers Python template&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Write your &lt;code&gt;on_fetch&lt;/code&gt; handler&lt;/li&gt;
&lt;li&gt;Deploy with &lt;code&gt;wrangler deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This post covers building workers with Python. In future posts, I&amp;rsquo;ll dive into specific patterns like handling async operations, using KV/D1/R2 bindings, and testing strategies for Workers.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
